트랜잭션 격리 수준을 언제 올려야 하는가 — 실제로 겪는 이상 현상
격리 수준의 이론적 정의보다 어떤 증상이 나올 때 무엇을 바꿔야 하는지를 정리한다.
정의보다 증상으로
격리 수준은 표로 외우기 쉽지만, 실무에서 필요한 것은 어떤 증상이 나올 때 무엇을 바꾸는가이다.
| 증상 | 이름 | 막는 수준 |
|---|---|---|
| 커밋 안 된 값을 읽음 | 더티 리드 | READ COMMITTED 이상 |
| 같은 행을 두 번 읽었는데 값이 다름 | 논리피터블 리드 | REPEATABLE READ 이상 |
| 같은 조건으로 두 번 조회했는데 행이 늘어남 | 팬텀 리드 | SERIALIZABLE (구현에 따라 다름) |
기본값 확인부터
DB마다 기본 격리 수준이 다르다.
- PostgreSQL: READ COMMITTED
- MySQL (InnoDB): REPEATABLE READ
- SQL Server: READ COMMITTED
같은 코드가 DB에 따라 다르게 동작할 수 있는 지점이다. 이식할 때 먼저 확인한다.
흔한 실제 문제 — 읽고 나서 쓰기
격리 수준으로 자주 착각하는 상황이 있다.
```
- 재고를 읽는다 (10개)
- 애플리케이션에서 계산한다 (10 - 1 = 9)
- 9로 업데이트한다
```
두 요청이 동시에 오면 둘 다 10을 읽고 둘 다 9를 쓴다. 2개가 팔렸는데 재고는 1개만 줄어든다.
이 문제는 격리 수준을 올려도 완전히 해결되지 않는다. READ COMMITTED에서는 그대로 발생하고, REPEATABLE READ에서도 구현에 따라 다르다.
해결 방식:
1. 조건부 UPDATE (원자적 연산)
```sql
UPDATE items SET stock = stock - 1
WHERE id = ? AND stock >= 1;
```
영향받은 행이 0이면 재고가 없던 것이다. 읽기-계산-쓰기가 한 문장으로 합쳐져 경합이 사라진다.
2. 명시적 락
```sql
SELECT stock FROM items WHERE id = ? FOR UPDATE;
```
다른 트랜잭션이 대기하게 된다. 처리량은 떨어지지만 복잡한 계산이 필요할 때 쓴다.
3. 낙관적 잠금
버전 컬럼을 두고 업데이트 시 확인한다.
```sql
UPDATE items SET stock = ?, version = version + 1
WHERE id = ? AND version = ?;
```
충돌 시 재시도 로직이 필요하다.
격리 수준을 올릴 때의 대가
SERIALIZABLE로 올리면 이상 현상은 사라지지만:
- 락 경합이 늘어 대기가 생긴다
- 직렬화 실패로 트랜잭션이 롤백되는 경우가 생긴다 → 재시도 로직이 필수
- 처리량이 떨어진다
특히 두 번째를 준비하지 않고 격리 수준만 올리면, 간헐적 실패가 늘어난 것처럼 보인다.
트랜잭션 범위
격리 수준보다 먼저 볼 것이 트랜잭션의 길이다.
```
나쁨: 외부 호출이 트랜잭션 안에 있음
BEGIN
UPDATE ...
외부 API 호출 (수 초) ← 이 동안 락 유지
UPDATE ...
COMMIT
```
트랜잭션이 길수록 락 유지 시간이 길고 경합이 커진다. 외부 호출·파일 처리·긴 계산은 트랜잭션 밖으로 뺀다.
판단 순서
- 문제가 실제로 관찰되는가 (로그·데이터 불일치)
- 그 문제가 격리 수준으로 해결되는 종류인가
- 원자적 연산으로 바꿀 수 있는가 → 대개 이쪽이 낫다
- 안 되면 락 또는 격리 수준 상향 + 재시도
3번을 먼저 검토하지 않고 격리 수준부터 올리는 경우가 흔한데, 조건부 UPDATE 한 줄로 끝나는 문제가 많다.
최종 수정 2026-08-28