트랜잭션의 ACID와 DBMS의 복구 전략: UNDO, REDO, WAL
ACID 속성을 짚고, DBMS가 버퍼 관리 정책과 로그로 원자성과 지속성을 보장하는 방식을 정리한다.
시리즈 · Database5 / 8
트랜잭션과 ACID
트랜잭션은 데이터베이스의 상태를 변화시키는 작업의 단위다. 여기서 단위는 SQL 한 문장이 아니라, 사람이 정한 기준에 따라 묶은 여러 문장의 집합이다.
- Atomicity(원자성): 트랜잭션은 모두 반영되거나, 전혀 반영되지 않아야 한다.
- Consistency(일관성): 트랜잭션 수행 전과 후에 데이터베이스가 지켜야 할 제약이 동일하게 유지되어야 한다.
- Isolation(격리성): 동시에 실행되는 트랜잭션은 서로의 연산에 끼어들 수 없고, 완료되기 전의 결과를 다른 트랜잭션이 참조할 수 없다. 주로 locking으로 구현한다.
- Durability(지속성): 성공적으로 완료된 트랜잭션의 결과는 직후에 시스템이 죽더라도 유실되지 않아야 한다. 주로 logging으로 구현한다.
아래는 DBMS가 원자성과 지속성을 실제로 어떻게 보장하는지에 대한 정리다.
페이지와 버퍼 관리자
DBMS는 크게 질의 처리기(Query Processor)와 저장 시스템(Storage System)으로 나눌 수 있다. MySQL이라면 InnoDB, MyISAM이 저장 시스템에 해당한다.

저장 시스템은 데이터를 고정 길이의 페이지로 저장하고, 디스크 입출력도 페이지 단위로 한다. 이 페이지들을 메모리에서 관리하는 모듈이 (페이지) 버퍼 관리자다.
UNDO와 STEAL 정책
수정된 페이지는 버퍼 교체 알고리즘에 따라 언제든 디스크에 출력될 수 있다. 아직 완료되지 않은 트랜잭션이 수정한 페이지도 예외가 아니므로, 문제가 생기면 그 변경을 원상복구해야 한다. 이것이 UNDO다.
수정된 페이지를 디스크에 쓰는 시점에 따라 정책이 나뉜다.
- STEAL: 수정된 페이지를 언제든지 디스크에 쓸 수 있다.
- NO-STEAL: 수정된 페이지를 최소한 트랜잭션 종료 시점(EOT)까지는 버퍼에 유지한다.
NO-STEAL이면 UNDO를 메모리 버퍼에 대해서만 하면 되니 매력적이지만, 매우 큰 버퍼가 필요하다. 그래서 거의 모든 DBMS가 STEAL 정책을 채택하고, 필연적으로 UNDO 로깅과 복구가 따라온다.
REDO와 FORCE 정책
커밋한 트랜잭션의 수정은 어떤 경우에도 유지되어야 한다. 이번에는 커밋 시점에 무엇을 디스크에 쓰느냐로 정책이 나뉜다.
- FORCE: 수정한 모든 페이지를 커밋 시점에 디스크에 반영한다. 이미 디스크에 있으므로 REDO가 필요 없다.
- NO-FORCE: 커밋 시점에 페이지를 디스크에 반영하지 않는다. 커밋한 내용이 디스크에 없을 수 있으므로 REDO가 필요하다.
거의 모든 DBMS가 NO-FORCE 정책을 사용한다.
로그
UNDO와 REDO를 위해 가장 널리 쓰이는 구조가 로그다.
- 로그는 로그 레코드의 연속이며, 데이터베이스의 모든 갱신 작업을 기록한다.
- 덧붙이는(append) 방식으로 기록된다.
- 각 로그 레코드는 LSN(Log Sequence Number) 또는 LSA(Log Sequence Address)라는 고유 식별자를 가진다.
로깅 방식
| 방식 | 기록 내용 | UNDO / REDO |
|---|---|---|
| 물리적 상태 로깅 | 갱신 이전 이미지와 이후 이미지 모두 | 이전 이미지로 대체 / 이후 이미지 반영 |
| 물리적 전이 로깅 | 이전·이후 이미지의 XOR 차이 | 차이를 적용 |
| 논리적 전이 로깅(오퍼레이션 로깅) | 결과 값이 아닌 수행한 연산 자체 | 역연산 수행 / 연산 재수행 |
물리적 상태 로깅이 가장 기본적인 방법이다. 논리적 로깅은 a = a + 1을 수행했을 때 0과 1을 남기는 대신 연산 자체를 기록하므로 로그 레코드의 크기가 크게 줄어든다.
논리적 로깅은 물리적으로 복구하기 어려운 자료 구조에도 유리하다. B-tree, B+-tree는 분할과 병합으로 레코드의 위치가 계속 바뀌어, 로깅 시점과 복구 시점의 물리적 위치가 같다는 보장이 없다. 물리적 로그로는 복구가 까다롭지만 논리적 로그로는 비교적 쉽게 복구할 수 있다.
다만 UNDO와 REDO는 멱등성을 보장해야 하는데, a++ 같은 연산을 복구 과정에서 여러 번 반복 적용하면 멱등성이 깨질 수 있다는 점을 주의해야 한다.
WAL
로그를 쓰는 순서에는 원칙이 있다.
- 업데이트가 데이터베이스에 써지기 전에 UNDO 정보가 먼저 로그에 써져야 한다. 이 원칙이 WAL(Write Ahead Logging)이다.
- 트랜잭션이 정상 종료 처리되려면 REDO 정보가 적어도 커밋 시점에는 로그에 써져 있어야 한다.
로그 쓰기 비용과 그룹 커밋
로그 레코드가 유실되면 DB를 완전히 복구할 수 없으므로 로그는 안전하게 써야 한다. 그래서 write 호출에 더해 fsync를 호출한다.
문제는 fsync가 매우 느린 연산이라는 점이다. 커밋하려는 트랜잭션은 자신의 로그가 파일에 써질 때까지, 즉 fsync가 끝날 때까지 기다려야 한다.
이를 완화하는 방법이 그룹 커밋(group commit)이다. 트랜잭션의 커밋 요청을 하나씩 처리하지 않고 모아서 한꺼번에 처리해 디스크 출력 횟수를 줄인다. 커밋 성능을 극대화하기 위해 지속성을 일부 포기하는 방식도 있다.
MySQL InnoDB가 언두 로그와 리두 로그를 실제로 어떻게 쓰는지는 Real MySQL 8.0 Ch.4 — InnoDB 스토리지 엔진 아키텍처에 정리했다.
참고
- A First Course in Database Systems - Ullman, Widom
- DBMS는 어떻게 트랜잭션을 관리할까? - NAVER D2
- 트랜잭션(Transaction)이란? - Mommoo