두 사람이 동시에 같은 계좌에서 돈을 출금하면 어떻게 될까?

트랜잭션 격리 수준은 동시에 실행되는 트랜잭션들이 서로를 얼마나 "볼 수 있는가" 를 결정합니다. 공부하다 보니 개념 자체보다 **MySQL과 PostgreSQL이 왜 기본값이 다른지 **, ** 실무에서 어떤 걸 선택해야 하는지 **가 더 중요한 포인트였습니다.

ACID 빠르게 복습

  • Atomicity: 트랜잭션은 전부 성공하거나 전부 실패
  • Consistency: 트랜잭션 전후로 데이터 무결성 유지
  • Isolation: 동시에 실행되는 트랜잭션이 서로 간섭하지 않음
  • Durability: 커밋된 데이터는 시스템 장애에도 보존

격리 수준은 이 중 Isolation 을 어느 정도로 보장할 것인가에 대한 설정입니다.

격리 수준별 문제 현상

먼저 격리 수준이 낮을 때 발생할 수 있는 세 가지 문제를 알아야 합니다.

Dirty Read

아직 커밋되지 않은 다른 트랜잭션의 변경을 읽는 것

PLAINTEXT
TX1: UPDATE accounts SET balance = 0 WHERE id = 1;  -- 커밋 전
TX2: SELECT balance FROM accounts WHERE id = 1;      -- 0을 읽음
TX1: ROLLBACK;                                        -- 원래 값으로 복구
→ TX2는 존재한 적 없는 데이터를 읽은 셈

Non-repeatable Read

같은 트랜잭션 내에서 같은 행을 두 번 읽었는데 ** 값이 달라진** 것

PLAINTEXT
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

같은 범위 쿼리를 두 번 실행했는데 ** 행의 수가 달라진** 것

PLAINTEXT
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 모두 발생
  • 실무 사용: 거의 없음. 로깅이나 실시간 모니터링 대시보드처럼 정확도가 중요하지 않은 경우에만
SQL
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

2. Read Committed

  • ** 커밋된 데이터만 읽음** → Dirty Read 방지
  • Non-repeatable Read, Phantom Read는 발생 가능
  • PostgreSQL의 기본값
  • 실무 사용: 대부분의 일반 CRUD 애플리케이션
SQL
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;

3. Repeatable Read

  • ** 트랜잭션 시작 시점의 스냅샷 **을 기준으로 읽음 → Non-repeatable Read 방지
  • Phantom Read는 이론적으로 발생 가능 (MySQL InnoDB는 Gap Lock으로 방지)
  • MySQL InnoDB의 기본값
  • 실무 사용: 재고 관리, 금액 계산 등 정합성이 중요한 시스템
SQL
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;

4. Serializable

  • 트랜잭션을 ** 직렬로 실행한 것과 동일한 결과** 보장
  • 모든 읽기 문제 방지
  • 실무 사용: 좌석 예매, 티켓 발권 등 절대적 정확성이 필요한 경우만
SQL
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

격리 수준별 문제 발생 여부

격리 수준Dirty ReadNon-repeatable ReadPhantom Read
Read UncommittedOOO
Read CommittedXOO
Repeatable ReadXX△ (MySQL은 방지)
SerializableXXX

MySQL vs PostgreSQL 기본값 차이

이 부분이 꽤 중요합니다.

MySQL InnoDBPostgreSQL
기본 격리 수준Repeatable ReadRead Committed
MVCC언두 로그 기반튜플 버전 관리
Phantom Read 방지Gap Lock으로 RR에서도 방지Serializable에서만 완전 방지

PostgreSQL의 Read Committed가 MySQL의 Read Committed보다 "안전하다"는 얘기를 들을 수 있는데, 이는 PostgreSQL의 MVCC 구현이 읽기 시 락 없이도 일관된 스냅샷을 제공하기 때문입니다.

MVCC와의 관계

MVCC(Multi-Version Concurrency Control) 는 격리 수준을 구현하는 핵심 메커니즘입니다.

기본 원리:

  • 데이터를 수정할 때 기존 버전을 보존하고 새 버전을 만듦
  • 읽기 트랜잭션은 자신의 시점에 맞는 버전을 읽음
  • 읽기와 쓰기가 서로를 블로킹하지 않음
PLAINTEXT
시점 1: balance = 1000 (버전 1)
TX1 시작 → TX1은 버전 1을 봄
시점 2: TX2가 balance = 500으로 수정 (버전 2 생성)
TX1: SELECT balance → 여전히 1000 (버전 1을 읽음)

MySQL은 언두 로그(Undo Log) 에 이전 버전을 저장하고, PostgreSQL은 튜플 자체에 버전 정보 를 저장합니다.

격리 수준별 실무 사용 시나리오

Read Uncommitted

PLAINTEXT
사용처: 거의 없음
예외: 대량 데이터 실시간 카운터, 대략적인 통계 대시보드
이유: 정확도보다 속도가 중요한 극소수 상황

Read Committed (PostgreSQL 기본)

PLAINTEXT
사용처: 일반적인 웹 애플리케이션, CRUD API
예: 게시판, 쇼핑몰 상품 목록, 사용자 프로필 조회
이유: 대부분의 경우 충분한 일관성, 높은 동시성

Repeatable Read (MySQL 기본)

PLAINTEXT
사용처: 한 트랜잭션 내 데이터 일관성이 중요한 시스템
예: 재고 관리, 주문 처리, 정산 배치
이유: 같은 트랜잭션 내에서 항상 같은 값을 읽어야 할 때

Serializable

PLAINTEXT
사용처: 절대적 정확성이 필요한 소수 시나리오
예: 좌석 예매, 수강 신청, 한정판 구매
이유: 동시성을 희생하더라도 데이터 정합성이 최우선
주의: 처리량이 크게 떨어지므로 전체 시스템이 아닌 특정 기능에만 적용

격리 수준과 동시성의 트레이드오프

PLAINTEXT
격리 수준 ↑  →  데이터 일관성 ↑  →  동시성(처리량) ↓
격리 수준 ↓  →  데이터 일관성 ↓  →  동시성(처리량) ↑
  • Serializable에서 Read Committed로 낮추면 처리량이 수 배 향상될 수 있음
  • 하지만 데이터 정합성 문제가 발생할 수 있으므로 애플리케이션 레벨에서 보완 필요

실무 팁

1. 대부분은 DB 기본값으로 충분합니다

MySQL → Repeatable Read, PostgreSQL → Read Committed. 이미 범용적인 선택입니다.

2. 격리 수준을 전역으로 바꾸지 마세요

SQL
-- ❌ 전체 세션 변경
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, 낙관적 락, 비관적 락 같은 기법과 함께 사용하세요.

댓글 로딩 중...