트랜잭션 격리 수준 — Read Uncommitted부터 Serializable까지 실전 가이드
두 사람이 동시에 같은 계좌에서 돈을 출금하면 어떻게 될까?
트랜잭션 격리 수준은 동시에 실행되는 트랜잭션들이 서로를 얼마나 "볼 수 있는가" 를 결정합니다. 공부하다 보니 개념 자체보다 **MySQL과 PostgreSQL이 왜 기본값이 다른지 **, ** 실무에서 어떤 걸 선택해야 하는지 **가 더 중요한 포인트였습니다.
ACID 빠르게 복습
- Atomicity: 트랜잭션은 전부 성공하거나 전부 실패
- Consistency: 트랜잭션 전후로 데이터 무결성 유지
- Isolation: 동시에 실행되는 트랜잭션이 서로 간섭하지 않음
- Durability: 커밋된 데이터는 시스템 장애에도 보존
격리 수준은 이 중 Isolation 을 어느 정도로 보장할 것인가에 대한 설정입니다.
격리 수준별 문제 현상
먼저 격리 수준이 낮을 때 발생할 수 있는 세 가지 문제를 알아야 합니다.
Dirty Read
아직 커밋되지 않은 다른 트랜잭션의 변경을 읽는 것
TX1: UPDATE accounts SET balance = 0 WHERE id = 1; -- 커밋 전
TX2: SELECT balance FROM accounts WHERE id = 1; -- 0을 읽음
TX1: ROLLBACK; -- 원래 값으로 복구
→ TX2는 존재한 적 없는 데이터를 읽은 셈
Non-repeatable Read
같은 트랜잭션 내에서 같은 행을 두 번 읽었는데 ** 값이 달라진** 것
TX1: SELECT balance FROM accounts WHERE id = 1; -- 1000원
TX2: UPDATE accounts SET balance = 500 WHERE id = 1; COMMIT;
TX1: SELECT balance FROM accounts WHERE id = 1; -- 500원 (!!)
→ 같은 트랜잭션인데 결과가 다름
Phantom Read
같은 범위 쿼리를 두 번 실행했는데 ** 행의 수가 달라진** 것
TX1: SELECT COUNT(*) FROM orders WHERE status = 'pending'; -- 5건
TX2: INSERT INTO orders (status) VALUES ('pending'); COMMIT;
TX1: SELECT COUNT(*) FROM orders WHERE status = 'pending'; -- 6건 (!!)
→ 유령처럼 새 행이 나타남
4가지 격리 수준
낮은 격리 → 높은 격리 순서입니다.
1. Read Uncommitted
- 커밋되지 않은 데이터도 읽을 수 있음
- Dirty Read, Non-repeatable Read, Phantom Read 모두 발생
- 실무 사용: 거의 없음. 로깅이나 실시간 모니터링 대시보드처럼 정확도가 중요하지 않은 경우에만
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
2. Read Committed
- ** 커밋된 데이터만 읽음** → Dirty Read 방지
- Non-repeatable Read, Phantom Read는 발생 가능
- PostgreSQL의 기본값
- 실무 사용: 대부분의 일반 CRUD 애플리케이션
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
3. Repeatable Read
- ** 트랜잭션 시작 시점의 스냅샷 **을 기준으로 읽음 → Non-repeatable Read 방지
- Phantom Read는 이론적으로 발생 가능 (MySQL InnoDB는 Gap Lock으로 방지)
- MySQL InnoDB의 기본값
- 실무 사용: 재고 관리, 금액 계산 등 정합성이 중요한 시스템
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
4. Serializable
- 트랜잭션을 ** 직렬로 실행한 것과 동일한 결과** 보장
- 모든 읽기 문제 방지
- 실무 사용: 좌석 예매, 티켓 발권 등 절대적 정확성이 필요한 경우만
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
격리 수준별 문제 발생 여부
| 격리 수준 | Dirty Read | Non-repeatable Read | Phantom Read |
|---|---|---|---|
| Read Uncommitted | O | O | O |
| Read Committed | X | O | O |
| Repeatable Read | X | X | △ (MySQL은 방지) |
| Serializable | X | X | X |
MySQL vs PostgreSQL 기본값 차이
이 부분이 꽤 중요합니다.
| MySQL InnoDB | PostgreSQL | |
|---|---|---|
| 기본 격리 수준 | Repeatable Read | Read Committed |
| MVCC | 언두 로그 기반 | 튜플 버전 관리 |
| Phantom Read 방지 | Gap Lock으로 RR에서도 방지 | Serializable에서만 완전 방지 |
PostgreSQL의 Read Committed가 MySQL의 Read Committed보다 "안전하다"는 얘기를 들을 수 있는데, 이는 PostgreSQL의 MVCC 구현이 읽기 시 락 없이도 일관된 스냅샷을 제공하기 때문입니다.
MVCC와의 관계
MVCC(Multi-Version Concurrency Control) 는 격리 수준을 구현하는 핵심 메커니즘입니다.
기본 원리:
- 데이터를 수정할 때 기존 버전을 보존하고 새 버전을 만듦
- 읽기 트랜잭션은 자신의 시점에 맞는 버전을 읽음
- 읽기와 쓰기가 서로를 블로킹하지 않음
시점 1: balance = 1000 (버전 1)
TX1 시작 → TX1은 버전 1을 봄
시점 2: TX2가 balance = 500으로 수정 (버전 2 생성)
TX1: SELECT balance → 여전히 1000 (버전 1을 읽음)
MySQL은 언두 로그(Undo Log) 에 이전 버전을 저장하고, PostgreSQL은 튜플 자체에 버전 정보 를 저장합니다.
격리 수준별 실무 사용 시나리오
Read Uncommitted
사용처: 거의 없음
예외: 대량 데이터 실시간 카운터, 대략적인 통계 대시보드
이유: 정확도보다 속도가 중요한 극소수 상황
Read Committed (PostgreSQL 기본)
사용처: 일반적인 웹 애플리케이션, CRUD API
예: 게시판, 쇼핑몰 상품 목록, 사용자 프로필 조회
이유: 대부분의 경우 충분한 일관성, 높은 동시성
Repeatable Read (MySQL 기본)
사용처: 한 트랜잭션 내 데이터 일관성이 중요한 시스템
예: 재고 관리, 주문 처리, 정산 배치
이유: 같은 트랜잭션 내에서 항상 같은 값을 읽어야 할 때
Serializable
사용처: 절대적 정확성이 필요한 소수 시나리오
예: 좌석 예매, 수강 신청, 한정판 구매
이유: 동시성을 희생하더라도 데이터 정합성이 최우선
주의: 처리량이 크게 떨어지므로 전체 시스템이 아닌 특정 기능에만 적용
격리 수준과 동시성의 트레이드오프
격리 수준 ↑ → 데이터 일관성 ↑ → 동시성(처리량) ↓
격리 수준 ↓ → 데이터 일관성 ↓ → 동시성(처리량) ↑
- Serializable에서 Read Committed로 낮추면 처리량이 수 배 향상될 수 있음
- 하지만 데이터 정합성 문제가 발생할 수 있으므로 애플리케이션 레벨에서 보완 필요
실무 팁
1. 대부분은 DB 기본값으로 충분합니다
MySQL → Repeatable Read, PostgreSQL → Read Committed. 이미 범용적인 선택입니다.
2. 격리 수준을 전역으로 바꾸지 마세요
-- ❌ 전체 세션 변경
SET GLOBAL TRANSACTION ISOLATION LEVEL SERIALIZABLE;
-- ✅ 필요한 트랜잭션만 변경
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN;
-- 좌석 예매 로직
COMMIT;
3. 락과 격리 수준을 혼동하지 마세요
- 격리 수준: 읽기 시 어떤 데이터가 보이는가
- 락: 쓰기 시 다른 트랜잭션의 접근을 어떻게 제어하는가
SELECT ... FOR UPDATE는 격리 수준과 별개로 명시적 락을 거는 것
정리
- 격리 수준은 "동시 트랜잭션이 서로를 얼마나 볼 수 있는가"를 결정
- Dirty Read → Non-repeatable Read → Phantom Read 순으로 격리 수준이 올라갈수록 방지
- MySQL 기본: Repeatable Read, PostgreSQL 기본: Read Committed
- MVCC 덕분에 읽기와 쓰기가 서로 블로킹하지 않음
- Serializable은 필요한 기능에만 선택적으로 적용
격리 수준을 올리는 것보다, 애플리케이션 로직에서 동시성 문제를 해결하는 것이 실무에서는 더 흔합니다.
SELECT ... FOR UPDATE, 낙관적 락, 비관적 락 같은 기법과 함께 사용하세요.