[Huge Traffic Handling] 트랜잭션 격리 수준 — 동시성과 정합성 사이의 선택

Raha·2026년 4월 19일

Huge Traffic Handling

목록 보기
6/9

들어가며

지난 글에서 @Transactional의 동작 원리와 전파 방식을 살펴봤다.
트랜잭션이 "하나의 논리적 작업 단위"라는 건 알겠는데, 여러 트랜잭션이 동시에 실행되면 어떤 일이 벌어질까?

이번 글에서는 아래 질문들을 다룬다.

  • 동시에 100명이 같은 데이터에 접근하면 무슨 일이 생기는가?
  • Dirty Read, Non-Repeatable Read, Phantom Read는 무엇인가?
  • 격리 수준 4단계는 각각 어떤 문제를 막아주는가?

1. 동시성 문제 — Race Condition

재고가 10개인 상품을 100명이 동시에 주문한다고 가정해보자.

트랜잭션 A → 재고 조회: 10개
트랜잭션 B → 재고 조회: 10개  (아직 A가 커밋 전)
트랜잭션 A → 재고 차감 후 커밋: 3개
트랜잭션 B → 재고 차감 후 커밋: 3개

두 트랜잭션 모두 재고가 충분하다고 판단했기 때문에 실제로는 재고가 7개 팔렸지만 DB엔 3개만 남는다.

이처럼 여러 트랜잭션이 같은 데이터를 동시에 읽고 쓰면서 발생하는 문제를 Race Condition(경쟁 상태) 이라고 한다.

이를 막으려면 "얼마나 엄격하게 트랜잭션을 격리할 것인가"를 결정해야 한다. 그 옵션이 바로 트랜잭션 격리 수준(Transaction Isolation Level) 이다.


2. 격리 수준 4단계

격리 수준은 낮을수록 성능이 좋고, 높을수록 데이터 정합성이 강하다. 정합성과 성능은 트레이드오프 관계다.

Read Uncommitted  →  Read Committed  →  Repeatable Read  →  Serializable
      성능 우선                                                정합성 우선

3. Dirty Read — 확정되지 않은 데이터를 읽는 문제

개념

트랜잭션 A가 데이터를 수정했지만 아직 COMMIT하지 않은 상태에서, 트랜잭션 B가 그 변경된 값을 읽어버리는 현상이다.

트랜잭션 A: 재고 10 → 5 수정 (아직 COMMIT 전)
트랜잭션 B: 재고 조회 → 5 읽음  ← Dirty Read 발생
트랜잭션 A: ROLLBACK → 재고 다시 10으로 복원
트랜잭션 B: 존재하지 않았던 '5'를 기반으로 잘못된 판단

트랜잭션 B는 언제든 사라질 수 있는 유령 데이터를 신뢰한 셈이다.

해결 — Read Committed

COMMIT된 데이터만 읽도록 보장한다. 트랜잭션 A가 수정 중이라면, B는 수정 전 마지막으로 COMMIT된 값을 읽는다.


4. Non-Repeatable Read — 같은 데이터가 다르게 읽히는 문제

개념

하나의 트랜잭션 안에서 같은 SELECT를 두 번 실행했을 때, 그 사이에 다른 트랜잭션이 값을 수정하고 COMMIT하여 결과가 달라지는 현상이다.

트랜잭션 A: 재고 조회 → 10개
트랜잭션 B: 재고를 5로 수정 후 COMMIT
트랜잭션 A: 재고 재조회 → 5개  ← 같은 트랜잭션인데 값이 달라짐

Read Committed는 "쿼리 실행 시점의 최신 COMMIT 데이터"를 읽기 때문에 이 문제를 막지 못한다.

해결 — Repeatable Read

트랜잭션이 시작될 때 스냅샷을 찍어두고, 트랜잭션이 끝날 때까지 그 스냅샷만 바라본다. 다른 트랜잭션이 아무리 수정하고 COMMIT해도, 내 트랜잭션은 시작 시점의 데이터를 일관되게 읽는다.

MySQL InnoDB는 MVCC(Multi-Version Concurrency Control) 기술로 이 스냅샷을 구현한다. 락 없이도 읽기 일관성을 보장할 수 있는 이유다.


5. Phantom Read — 없던 행이 나타나는 문제

개념

하나의 트랜잭션 안에서 같은 조건으로 SELECT를 두 번 실행했을 때, 다른 트랜잭션이 새로운 행을 INSERT하고 COMMIT하여 첫 번째엔 없던 행이 두 번째 조회에서 나타나는 현상이다.

트랜잭션 A: WHERE stock > 10 조회 → 2개
트랜잭션 B: stock=15인 신상품 INSERT 후 COMMIT
트랜잭션 A: 같은 조건 재조회 → 3개  ← 유령 행 등장

Repeatable Read의 스냅샷은 기존 행의 변경은 막아주지만, 스냅샷 이후에 새로 추가된 행은 막지 못한다.

Non-Repeatable Read vs Phantom Read

Non-Repeatable ReadPhantom Read
문제특정 행의 이 바뀜조회 결과의 행 개수가 바뀜
원인다른 트랜잭션의 UPDATE/DELETE다른 트랜잭션의 INSERT

해결 — Serializable

SELECT 쿼리의 조건 범위 자체에 잠금을 건다. WHERE stock > 10 범위에 해당하는 INSERT 자체를 차단하여 Phantom Read를 원천적으로 막는다.

단, 동시 처리 성능이 크게 저하되므로 극히 제한적인 경우에만 사용한다.


6. 정리

격리 수준Dirty ReadNon-Repeatable ReadPhantom Read특징
Read Uncommitted발생발생발생거의 사용 안 함
Read Committed방지발생발생Oracle, PostgreSQL 기본값
Repeatable Read방지방지발생MySQL 기본값
Serializable방지방지방지성능 저하, 제한적 사용

실무에서는 대부분 Read Committed 또는 Repeatable Read 중에서 선택한다.

  • 상품 조회, 목록 페이징처럼 단순 읽기 → Read Committed
  • 주문 처리, 재고 차감처럼 정합성이 중요한 경우 → Repeatable Read

마치며

격리 수준은 "얼마나 엄격하게 트랜잭션을 격리할 것인가"에 대한 선택이다. 정합성을 높이면 성능이 떨어지고, 성능을 높이면 정합성이 위험해진다.

다음 글에서는 격리 수준만으로 해결되지 않는 동시성 문제를 낙관적 락과 비관적 락으로 어떻게 해결하는지 다룬다.

profile
Backend Developer | Aspiring Full-Stack Enthusiast

0개의 댓글