본문으로 건너뛰기

ACID, 격리 수준, Redo/Undo — DB 트랜잭션 정리 (DB 총정리 2/4)

DB 총정리 둘째 편은 트랜잭션이다. ACID, 격리 수준, 복구(Redo/Undo)는 따로 외우면 셋이지만 사실 한 묶음이다. ACID는 트랜잭션이 지켜야 할 약속이고, 격리 수준은 그중 I(격리성)를 성능과 맞바꾸는 다이얼이며, Redo/Undo는 A(원자성)와 D(지속성)를 장애 속에서도 지키는 장치다.

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

트랜잭션은 논리적으로 하나인 작업 단위다. 계좌 이체가 늘 예제로 나오는 데는 이유가 있다. A 잔고를 빼는 일과 B 잔고를 늘리는 일은 둘 다 되거나 둘 다 안 되어야 한다.

계좌 이체
START TRANSACTION;
UPDATE account SET balance = balance - 200000 WHERE id = 'A';
UPDATE account SET balance = balance + 200000 WHERE id = 'B';
COMMIT; -- 여기까지 오기 전에는 언제든 ROLLBACK으로 전부 취소할 수 있다

A · Atomicity 원자성

전부 되거나 전부 안 된다. 중간에 죽으면 한 일을 모두 되돌린다. DBMS가 Undo 로그로 지킨다.

C · Consistency 일관성

트랜잭션 앞뒤로 제약 조건과 비즈니스 규칙이 깨지지 않는다. 무엇이 규칙인지는 개발자가 정한다.

I · Isolation 격리성

동시에 돌아도 서로 없는 것처럼 동작한다. 완벽하게 하면 느려서 격리 수준으로 타협한다.

D · Durability 지속성

커밋한 것은 정전이 나도 남는다. DBMS가 Redo 로그(WAL)로 지킨다.

A와 D는 DBMS가 알아서 해 준다. 하지만 C는 무엇이 일관된 상태인지 개발자가 제약 조건과 검증 로직으로 정의해야 하고, I는 개발자가 격리 수준을 골라야 한다. “트랜잭션으로 감쌌으니 안전하겠지”가 통하지 않는 곳이 바로 C와 I다.

격리 수준: 이상 현상 세 가지와 다이얼 네 단계

격리성을 완벽하게, 즉 트랜잭션을 하나씩 차례로 실행한 것과 똑같이 보장하면 동시성이 사라진다. 그래서 SQL 표준은 어떤 이상 현상까지 참을지를 네 단계로 정해 두었다. 이상 현상부터 보자.

  • Dirty Read: 다른 트랜잭션이 아직 커밋하지 않은 값을 읽는다. 그 트랜잭션이 롤백하면 나는 존재한 적 없는 값을 읽은 셈이다.
  • Non-repeatable Read: 한 트랜잭션 안에서 같은 row를 두 번 읽었는데 값이 다르다. 그 사이에 다른 트랜잭션이 UPDATE하고 커밋했다.
  • Phantom Read: 같은 조건으로 두 번 조회했는데 처음엔 없던 row가 나타난다. 그 사이에 다른 트랜잭션이 INSERT하고 커밋했다.
격리 수준↓ 엄격할수록 느리다DirtyReadNon-repeatablePhantomReadREAD UNCOMMITTED발생발생발생READ COMMITTEDPostgreSQL · Oracle 기본차단발생발생REPEATABLE READMySQL(InnoDB) 기본차단차단발생*SERIALIZABLE차단차단차단* 표준은 허용하지만 InnoDB는 대부분 막는다
아래로 갈수록 막는 이상 현상이 늘고, 락과 검증 비용도 는다

기본값이 DBMS마다 다르다는 게 은근한 함정이다. MySQL(InnoDB)은 REPEATABLE READ, PostgreSQL과 Oracle은 READ COMMITTED가 기본이다. 같은 코드도 DB를 바꾸면 다르게 동작할 수 있다.

표의 별표도 DBMS마다 사정이 다르다. InnoDB의 REPEATABLE READ는 일반 SELECT에는 스냅샷을, FOR UPDATE 같은 잠금 읽기에는 갭 락(next-key lock)을 써서 phantom을 대부분 막는다. PostgreSQL의 REPEATABLE READ도 스냅샷 격리라 phantom이 보이지 않는다.

요즘 DBMS는 이 문제를 락만으로 풀지 않는다. 주력은 MVCC(다중 버전 동시성 제어)로, 트랜잭션마다 자기 시점의 스냅샷을 보여 준다. 읽기가 쓰기를 막지 않고 쓰기가 읽기를 막지 않는 이유가 이것이다.

🧪 직접 해보기

세션 두 개로 Non-repeatable Read 보기

MySQL 콘솔을 두 개 열고 번호 순서대로 실행한다. 지금 격리 수준은 SELECT @@transaction_isolation;로 확인한다(PostgreSQL은 SHOW transaction_isolation;).

세션 A
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
SELECT balance FROM account WHERE id = 'A'; -- ① 1000000
SELECT balance FROM account WHERE id = 'A'; -- ③ 800000, 같은 트랜잭션인데 값이 바뀌었다
COMMIT;
세션 B
UPDATE account SET balance = balance - 200000 WHERE id = 'A'; -- ② autocommit 으로 바로 커밋

세션 A를 REPEATABLE READ로 바꿔 다시 해 보면 ③에서도 ①과 같은 값이 나온다. 첫 읽기 때 잡은 스냅샷을 트랜잭션이 끝날 때까지 계속 보기 때문이다.

복구: 정전이 나도 커밋은 살아남는다

DB는 데이터를 바꿀 때 디스크에 바로 쓰지 않는다. 성능 때문에 메모리(버퍼 풀)에서 고치고, 디스크에는 나중에 몰아서 반영한다. 그 사이에 서버가 죽으면 어떻게 될까. 이 틈을 로그가 메운다.

원칙은 WAL(Write-Ahead Logging), 데이터보다 로그를 먼저 디스크에 쓰는 것이다. 로그에는 두 가지 정보가 담긴다.

  • Redo 로그: 바꾼 뒤의 값. 커밋했지만 아직 디스크에 반영되지 않은 변경을 다시 재생하는 데 쓴다.
  • Undo 로그: 바꾸기 전의 값. 커밋하지 못하고 죽은 트랜잭션의 변경을 되돌리는 데 쓴다. InnoDB와 Oracle에서는 MVCC가 보여 주는 예전 버전도 여기서 나온다. PostgreSQL은 예전 버전을 테이블 안에 그대로 남기는 방식이라 Undo 로그가 따로 없다.
체크포인트장애T1할 일 없음COMMITT2Redo로 다시 쓴다COMMITT3Undo로 되돌린다① Redo: 체크포인트부터 로그를 재생해 장애 직전으로② Undo: 커밋 못 한 트랜잭션을 되돌린다
장애가 난 순간을 기준으로, 재시작하면 Redo로 앞으로 감고 Undo로 되돌린다

체크포인트는 주기적으로 버퍼의 변경을 디스크에 반영하고 표시를 남기는 일이다. 덕분에 재시작할 때 로그를 처음부터가 아니라 마지막 체크포인트부터만 재생하면 된다. 재시작하면 먼저 Redo로 장애 직전 상태를 다시 만들고(커밋한 건 반드시 살린다, 지속성), 그다음 커밋하지 못한 트랜잭션을 Undo로 되돌린다(흔적을 남기지 않는다, 원자성).

그래서 COMMIT이 성공했다는 건 데이터가 디스크에 다 써졌다는 뜻이 아니라 Redo 로그가 디스크에 안전하게 남았다는 뜻이다. 그거면 충분하다. 언제 죽어도 로그로 다시 만들 수 있다. 커밋 응답이 로그 fsync 한 번으로 끝나니 빠르기까지 하다. WAL이 진지한 DB는 물론 Kafka 같은 시스템의 심장이기도 한 이유다.

단, 이 보장은 설정에 달려 있다. MySQL의 innodb_flush_log_at_trx_commit이 기본값 1이면 커밋마다 로그를 fsync한다. 빨라지자고 0으로 바꾸면 MySQL이 죽기만 해도, 2로 바꾸면 OS가 죽거나 전원이 나갈 때 최근 약 1초치 커밋을 잃을 수 있다. PostgreSQL의 synchronous_commit = off도 같은 거래다.

정리

  • ACID에서 A와 D는 DBMS가 로그로, C와 I는 개발자가 챙긴다.
  • 격리 수준은 이상 현상을 어디까지 허용할지 고르는 다이얼이다. 내 DB의 기본값부터 확인한다 (MySQL은 REPEATABLE READ, PostgreSQL은 READ COMMITTED).
  • 돈이 걸린 구간은 격리 수준이 아니라 명시적 락으로 지킨다.
  • WAL은 로그를 먼저, 데이터를 나중에 쓴다. 장애 뒤에는 커밋한 것은 Redo, 못 한 것은 Undo.

다음 편은 설계 이야기다. 정규화를 왜 하고 어디서 멈출지, 비즈니스 로직을 ORM에 둘지 Stored Procedure에 둘지를 다룬다.

GitHub에서 고치기
esc

최근 글

↑↓ 이동↵ 열기검색 페이지에서 보기 ›