데이터베이스 학습 노트 목차

트랜잭션 — ACID·격리수준·이상현상

계좌 A에서 B로 만 원을 옮긴다 — A에서 빼고, B에 더한다. 이 둘 사이에서 서버가 죽으면? 돈이 증발한다. **트랜잭션(transaction)**은 여러 작업을 하나의 논리 단위로 묶어, 전부 되거나 전부 안 되게 만드는 약속이다. 그 약속의 네 글자가 ACID다. 그리고 여러 트랜잭션이 동시에 돌 때 서로 얼마나 간섭을 허용할지가 격리수준이다 — 이 둘이 DB 신뢰성의 심장이다. (출처: Silberschatz DBSC 7e Ch 17 Transactions / PostgreSQL 17 — Transaction Isolation.)

ACID — 트랜잭션의 네 가지 약속

  • 원자성(Atomicity)전부 아니면 전무. 중간에 실패하면 모두 롤백. (구현: undo 로그/WAL — [08 회복]에서.)
  • 일관성(Consistency) — 트랜잭션 전후로 DB 제약(무결성·FK·CHECK)이 유지된다. 나머지 셋이 받쳐주는 결과적 속성.
  • 독립성(Isolation) — 동시 트랜잭션이 서로 안 보이는 것처럼 보인다. 어디까지 보장하냐가 격리수준.
  • 지속성(Durability)커밋된 것은 장애가 나도 살아남는다. (구현: WAL을 디스크에 먼저 flush.)
BEGIN;
  UPDATE account SET balance = balance - 10000 WHERE id = 'A';
  UPDATE account SET balance = balance + 10000 WHERE id = 'B';
COMMIT;          -- 둘 다 확정. 사이에서 죽으면 ROLLBACK 되어 A도 원복.

원자성과 일관성은 한 트랜잭션 안의 이야기다. 진짜 어려운 건 여러 트랜잭션이 동시에 돌 때의 독립성이다.

동시성이 만드는 이상현상

격리를 약하게 하면 성능은 오르지만 아래 현상들이 샌다. 격리수준은 *"어떤 현상까지 막느냐"*로 정의된다.

다이어그램 로딩 중…
  • Dirty Read(오손 읽기)커밋 안 된 남의 변경을 읽음. 롤백되면 존재한 적 없는 값을 본 셈.
  • Non-repeatable Read(반복 불가 읽기) — 같은 행을 두 번 읽었는데 값이 다름(사이에 남이 커밋).
  • Phantom Read(유령 읽기) — 같은 조건으로 두 번 조회했는데 행 수가 다름(사이에 남이 INSERT/DELETE).
  • Lost Update(갱신 손실) — 두 트랜잭션이 읽고-고쳐-쓰기를 겹쳐 한쪽 변경이 덮여 사라짐.
  • Write Skew(쓰기 왜곡) — 각자 다른 행을 보고 결정했는데 합쳐보면 제약이 깨지는 미묘한 경우(예: 당직자 최소 1명 규칙).

표준 격리수준 — 무엇을 막나

ANSI SQL은 네 단계를 정의하고, 위로 갈수록 안전하지만 동시성은 낮아진다.

격리수준DirtyNon-repeatablePhantom
READ UNCOMMITTED가능가능가능
READ COMMITTED막음가능가능
REPEATABLE READ막음막음가능(표준상)
SERIALIZABLE막음막음막음

표준은 현상을 막는 최소 기준만 정하고 구현은 DB마다 다르다 — 이 부분이 실무에서 가장 자주 어긋나는 지점이다.

PostgreSQL vs MySQL — 기본값과 구현이 다르다

PostgreSQL (MVCC·스냅샷 기반):

  • 기본값은 Read Committed. 매 *문장(statement)*마다 새 스냅샷 → 문장 단위로 커밋된 데이터만 본다.
  • Read Uncommitted를 요청해도 Read Committed로 동작(dirty read를 아예 안 함).
  • Repeatable Read = 스냅샷 격리(Snapshot Isolation) — 트랜잭션 시작 시점 스냅샷을 끝까지 봐서 phantom까지 막는다(표준보다 강함). 단 write skew는 남는다.
  • Serializable = SSI(Serializable Snapshot Isolation) — 직렬화 이상을 탐지하면 한쪽을 롤백(could not serialize 에러)시켜 write skew까지 막는다. 애플리케이션은 재시도 준비가 필요.

MySQL InnoDB:

  • 기본값은 Repeatable Read. 갭 락(gap lock)/넥스트키 락으로 phantom도 상당히 막는다.
  • Read Committed도 흔히 쓰이며(복제·락 경합 줄이려고), 락 잡는 범위가 달라진다.

그래서 *"Repeatable Read"*라는 같은 이름이어도 PostgreSQL(스냅샷)과 MySQL(락+갭락)의 동작과 실패 양상이 다르다. 내 DB의 기본값과 구현을 아는 게 먼저다. (출처: PostgreSQL Transaction Isolation / MySQL InnoDB Isolation Levels.)

Lost Update를 막는 법 — 낙관 vs 비관

읽고-고쳐-쓰는 흐름의 갱신 손실격리수준만으론 부족할 때가 많다. 두 전략이 있다.

-- 비관적 락(pessimistic): 읽을 때 잠가 버림
BEGIN;
SELECT stock FROM product WHERE id = 42 FOR UPDATE;   -- 행 잠금
UPDATE product SET stock = stock - 1 WHERE id = 42;
COMMIT;
-- 낙관적 락(optimistic): 버전 칼럼으로 충돌 감지
UPDATE product SET stock = stock - 1, version = version + 1
WHERE id = 42 AND version = 7;     -- 영향 행 0이면 누가 먼저 바꾼 것 → 재시도
  • 비관적 락(SELECT ... FOR UPDATE)충돌이 잦을 때. 먼저 잠그고 작업. 락 경합·데드락 주의.
  • 낙관적 락(version 칼럼)충돌이 드물 때. 잠그지 않고 쓸 때 버전이 그대로인지 확인, 어긋나면 재시도. 동시성이 높다.

수량 누적처럼 현재값 기반 연산은 아예 SET stock = stock - 1처럼 DB가 원자적으로 계산하게 하면 읽기-쓰기 경합 자체를 줄인다.

실무 원칙 — 트랜잭션은 짧게, 격리는 필요한 만큼

  • 트랜잭션은 짧게. 트랜잭션이 길면 락·스냅샷을 오래 쥐어 동시성을 망치고, PostgreSQL에선 오래된 스냅샷이 VACUUM을 막아 테이블이 부푼다([05 MVCC]).
  • 외부 호출(HTTP·메일)을 트랜잭션 안에 넣지 마라. 느린 I/O가 DB 락을 인질로 잡는다.
  • 격리수준은 기본(Read Committed)에서 시작하고, 갱신 손실·write skew가 문제인 특정 지점만 FOR UPDATE/버전/Serializable로 올린다.
  • Serializable·낙관적 락을 쓰면 재시도 로직이 짝이다 — 직렬화 실패·버전 충돌은 정상 흐름으로 다뤄야 한다.

한 줄 요약 — ACID는 전부-또는-전무 + 동시성 격리 + 영속. 이상현상(dirty/non-repeatable/phantom/lost update/write skew)을 어디까지 막느냐가 격리수준이고, PostgreSQL 기본 Read Committed·RR은 스냅샷, MySQL 기본 RR은 갭락으로 구현이 다르다. 갱신 손실은 비관/낙관 락으로 따로 막는다.

(출처: Silberschatz DBSC 7e Ch 17·18 / PostgreSQL 17 Transaction Isolation / MySQL 8.4 InnoDB Isolation.)

데이터 모델링·ERD — 개념·논리·물리 설계동시성 제어 — 2PL·MVCC·락·데드락(PostgreSQL·MySQL)