🤖 AI 생성 콘텐츠 — 이 글은 AI가 자료를 수집하고 초안을 작성했으며, 발행 전 누슘 운영자가 검수했습니다.
트랜잭션 격리 수준은 동시에 실행되는 DB 작업이 서로의 중간 상태를 얼마나 보게 할지 정합니다. 네 단계의 차이와 PostgreSQL에서 주의할 점을 재고 예제로 풉니다.
재고가 하나 남았는데 두 사람이 동시에 결제합니다. 둘 다 “재고 1”을 읽고 주문을 완료하면 시스템은 두 개를 팔아버립니다. 코드는 한 줄씩 보면 멀쩡합니다. 문제는 동시에 섞여 실행된 순서입니다. 트랜잭션 격리 수준은 이 섞임을 어디까지 허용할지 정하는 약속입니다.
목차
- 트랜잭션 격리는 무엇을 막나?
- 네 가지 격리 수준은 어떻게 다른가?
- PostgreSQL에서는 무엇이 특별한가?
- 실무에서는 어떤 수준을 골라야 하나?
트랜잭션 격리는 무엇을 막나?
SQL 표준과 PostgreSQL 문서는 대표적인 이상 현상을 설명합니다.
- 더티 리드: 다른 트랜잭션이 아직 커밋하지 않은 값을 읽습니다.
- 반복 불가능 읽기: 같은 행을 다시 읽었더니 다른 트랜잭션의 커밋 때문에 값이 바뀝니다.
- 팬텀 리드: 같은 조건으로 다시 검색했더니 행 집합이 달라집니다.
- 직렬화 이상: 모든 트랜잭션을 하나씩 어떤 순서로 실행한 결과와도 맞지 않는 최종 상태가 생깁니다.
격리를 높이면 이런 현상을 더 많이 막습니다. 대신 대기, 충돌, 재시도 비용이 커질 수 있습니다. 방음벽을 두껍게 세울수록 서로 방해는 덜 하지만 출입 절차가 복잡해지는 것과 비슷합니다.

네 가지 격리 수준은 어떻게 다른가?
| 격리 수준 | 핵심 감각 | 가능한 문제 |
|---|---|---|
| Read Uncommitted | 남의 미완성 작업도 볼 수 있음 | 더티 리드 포함 |
| Read Committed | 문장 시작 전 커밋된 값만 봄 | 반복 불가능 읽기·팬텀·직렬화 이상 |
| Repeatable Read | 트랜잭션 동안 안정된 스냅샷 | DB 구현에 따라 직렬화 이상 |
| Serializable | 하나씩 실행한 것과 같은 효과 보장 | 충돌 시 실패·재시도 필요 |
Serializable은 “무조건 한 줄로 세워 실행한다”는 뜻은 아닙니다. 데이터베이스는 가능한 작업을 동시에 처리하되, 결과가 어떤 직렬 순서와 같도록 보장합니다. 보장할 수 없는 충돌이 생기면 한 트랜잭션을 실패시키고 애플리케이션이 재시도하게 할 수 있습니다.
PostgreSQL에서는 무엇이 특별한가?
PostgreSQL의 기본 격리 수준은 Read Committed입니다. 일반 SELECT는 쿼리가 시작되기 전에 커밋된 데이터의 스냅샷을 봅니다. 같은 트랜잭션 안에서도 다음 SELECT가 시작될 때 다른 커밋을 볼 수 있어 결과가 달라질 수 있습니다.
또 하나의 반전이 있습니다. PostgreSQL에서는 Read Uncommitted를 요청해도 내부적으로 Read Committed처럼 동작합니다. MVCC 구조에 맞춘 선택입니다. PostgreSQL의 Repeatable Read는 SQL 표준이 허용할 수 있는 팬텀 리드도 막지만, 직렬화 이상 가능성까지 없애지는 않습니다.
시퀀스 값도 주의해야 합니다. PostgreSQL 문서는 serial 등에 쓰이는 시퀀스 변경이 다른 트랜잭션에 즉시 보이고, 트랜잭션이 롤백돼도 되돌아가지 않는다고 설명합니다. 자동 증가 번호가 중간에 비는 것은 이상 현상이 아니라 정상 동작일 수 있습니다.

실무에서는 어떤 수준을 골라야 하나?
모든 요청을 Serializable로 올리는 게 정답은 아닙니다. 데이터 불변식과 충돌 빈도를 기준으로 고릅니다.
- 단순 조회와 일반 CRUD는 기본 수준으로 충분한지 확인합니다.
- “재고는 음수가 되면 안 된다” 같은 불변식은 원자적 UPDATE, 제약조건, 행 잠금으로 먼저 표현합니다.
- 여러 행과 테이블을 함께 판단하는 복잡한 규칙은 Serializable을 검토합니다.
- 직렬화 실패와 데드락은 정상적인 동시성 결과로 보고 안전한 재시도를 구현합니다.
- 결제처럼 외부 부작용이 있는 작업은 재시도 시 중복 실행되지 않게 멱등성 키를 함께 사용합니다.
격리 수준은 버그를 자동으로 없애는 스위치가 아닙니다. 어떤 데이터 규칙을 지켜야 하는지 먼저 쓰고, 그 규칙을 DB 제약과 트랜잭션으로 나누는 게 출발점입니다.
자주 묻는 질문 (FAQ)
Q. PostgreSQL 기본 격리 수준은 무엇인가요? A. Read Committed입니다. 각 쿼리는 시작 시점의 커밋된 데이터를 보므로 같은 트랜잭션의 연속 조회 결과가 달라질 수 있습니다.
Q. Serializable이면 재시도 코드가 필요 없나요? A. 반대입니다. 직렬화 가능한 결과를 만들 수 없는 충돌이 생기면 데이터베이스가 트랜잭션을 중단할 수 있으므로 애플리케이션이 전체 트랜잭션을 재시도해야 합니다.
Q. Repeatable Read는 팬텀 리드를 허용하나요? A. SQL 표준 표에서는 허용될 수 있지만 PostgreSQL 구현은 팬텀 리드를 허용하지 않습니다. 다만 직렬화 이상 가능성은 남으므로 제품별 문서를 확인해야 합니다.
Q. 자동 증가 ID가 롤백 후 비어도 문제인가요? A. PostgreSQL 시퀀스는 트랜잭션 롤백으로 되돌아가지 않습니다. ID의 연속성을 업무 규칙으로 사용하지 말고 고유 식별자 역할로 보는 편이 안전합니다.
마치며
동시성 버그는 코드 한 줄보다 “두 요청이 어떤 순서로 겹쳤나”에서 생깁니다. 격리 수준의 이름을 외우기보다 더티 리드, 반복 불가능 읽기, 팬텀, 직렬화 이상을 실제 업무 규칙에 대입해 보세요.
여러분이 겪은 가장 까다로운 동시성 버그는 무엇이었나요? 누슘 댓글에서 재현 방법과 해결책을 나눠 주세요.
참고자료
- PostgreSQL 공식 문서 — Transaction Isolation
- PostgreSQL 공식 문서 — Explicit Locking
- PostgreSQL 공식 문서 — Concurrency Control
태그: #데이터베이스 #트랜잭션 #격리수준 #PostgreSQL #동시성 #MVCC #Serializable #ReadCommitted

댓글을 남기려면 로그인이 필요합니다.
로그인하고 댓글 남기기