트랜잭션은 백엔드 개발의 일상이다. 주문 생성, 결제 처리, 재고 차감 — 여러 연산을 묶어서 “전부 성공 아니면 전부 실패” 로 다루는 단위다. ACID 4글자가 그 보장의 정체지만, 4글자가 실제로 무엇을 보장하는지는 표면적으로만 이해되기 쉽다.
특히 C 와 I 가 자주 오해된다. C 는 “DB 가 알아서 일관성을 지켜준다” 로 단순화되고, I 는 단순 on/off 처럼 다뤄진다. ACID 각 속성이 보장하는 것과 보장하지 않는 것을 살펴본다.
A (Atomicity)
한 트랜잭션의 모든 연산이 다 반영되거나, 하나도 반영되지 않는다. 중간 실패 시 그때까지의 변경은 전체 롤백된다.
주문 생성 트랜잭션을 예로 들자. 주문 행 삽입, 재고 차감, 결제 기록 세 연산이 한 트랜잭션 안에서 묶인다. 결제 기록 단계에서 에러가 나면 주문 행도 재고 차감도 모두 없던 일이 된다. “주문은 들어갔는데 결제는 안 됐다” 같은 부분 상태가 존재하지 않는다.
DB 는 일반적으로 undo log 로 이 보장을 구현한다. 트랜잭션이 변경할 때마다 이전 값 을 따로 기록해두고, 롤백이 필요하면 그 값으로 되돌린다. commit 이 끝나면 undo log 는 의미가 없어진다.
이 글에서 다루는 atomicity 는 단일 DB 안에서의 보장이다. 분산 환경의 atomicity — 여러 DB 또는 외부 서비스를 묶는 — 는 2PC 나 saga 같은 별도 메커니즘이 필요하고, 이 시리즈의 범위 밖이다.
C (Consistency)
가장 자주 오해되는 속성이다. “DB 가 알아서 일관성을 지켜준다” 는 표현은 절반만 맞다.
C 가 보장하는 영역은 DB constraint 다. primary key 의 유일성, foreign key 의 참조 무결성, check constraint 의 조건, NOT NULL, unique index 같은 것들. 트랜잭션이 commit 되는 시점에 이 constraint 들이 깨졌다면 DB 는 commit 을 거부한다. 이 부분은 DB 가 자동으로 지킨다.
애플리케이션 invariant 는 다른 이야기다. “주문 금액의 합이 결제 금액과 같아야 한다”, “재고는 음수가 될 수 없다”, “환불 시 원거래의 상태가 ‘paid’ 여야 한다” 같은 비즈니스 규칙은 DB constraint 로 표현되지 않거나 표현해도 부족한 경우가 많다.
재고 음수 방지는 CHECK (stock >= 0) 같은 constraint 로 가능하지만, “환불 가능 여부 판단” 처럼 여러 행과 상태를 조합해야 하는 규칙은 애플리케이션 코드가 책임진다. 트랜잭션 안에서 이 규칙들을 어떻게 검증하고 어떤 순서로 검증하는지는 모두 애플리케이션의 책임이다.
C 의 ‘C’ 는 일관된 상태로의 전이 를 보장한다는 의미다. 무엇이 ‘일관됨’ 인지는 DB constraint 와 애플리케이션 invariant 가 함께 정의하고, DB 는 그중 constraint 부분만 강제한다. 이 책임 분담을 의식하지 않으면 “DB 가 알아서 지켜줄 줄 알았다” 식의 버그가 나온다.
I (Isolation)
동시에 실행되는 트랜잭션 사이의 보이는 영역 을 제어한다. 트랜잭션 T1 이 실행되는 도중 T2 가 같은 데이터를 건드릴 때, 서로의 중간 상태를 얼마나 볼 수 있는가의 문제다.
완벽한 격리는 모든 트랜잭션이 순차적으로 실행된 것과 동일한 결과를 보장한다. 정확성 측면에서 가장 강한 보장이다. 그런데 그 보장은 동시성을 거의 죽인다. 한 번에 하나의 트랜잭션만 실행할 수 있다면 처리량이 급격히 떨어진다.
그래서 RDB 는 격리를 단순 on/off 가 아니라 단계 로 제공한다. “어떤 anomaly 까지 허용할 것인가” 의 정책 선택이다. 더 강한 격리는 더 적은 anomaly 를 허용하지만 더 큰 동시성 비용을 동반한다.
I 의 핵심은 그 단계가 왜 존재하는지에 있다. 정확성과 동시성이 충돌하는 영역이라는 점이 본질이고, 시리즈의 다음 글에서 4단계 Isolation Level 과 각 단계가 막는 anomaly 를 깊게 다룬다.
D (Durability)
commit 이 완료된 트랜잭션의 결과는 시스템 장애가 발생해도 살아남는다. 정전, OS crash, 프로세스 강제 종료 같은 어떤 형태의 실패에도 commit 된 데이터는 유실되지 않는다.
일반적으로 WAL (Write-Ahead Log) 로 구현된다. 변경 내용을 메인 데이터 파일에 쓰기 전에 별도의 로그 파일에 먼저 기록하고 fsync 로 디스크에 강제 동기화한다. 시스템 재시작 후 그 로그를 재생(replay) 하면 commit 된 상태가 복구된다.
D 는 commit 시점 의 영속성을 보장한다. commit 이전 단계에서 죽으면 그 트랜잭션은 적용되지 않은 것이고, 이건 A 와 자연스럽게 연결된다. 두 보장이 함께 “확실히 끝났거나, 아예 없었거나” 의 양극단만 남긴다.
WAL 의 구체적 동작 — 체크포인트, 로그 재활용, 복구 알고리즘 — 은 그 자체로 큰 주제고, 이 시리즈에서는 D 가 무엇을 보장하는가의 개념 수준에서 멈춘다.
정리
A 와 D 는 비교적 단순한 보장이다. 모두 적용 or 아무것도 적용 안 함 (A), commit 이후 영구 (D). 구현 디테일은 복잡해도 보장 자체는 명확하다.
C 는 책임이 분담 되어 있다. DB constraint 가 한쪽을, 애플리케이션 invariant 가 다른 쪽을 책임진다. 이 경계를 의식하지 않으면 트랜잭션 안에서 보장된다고 믿었던 규칙이 깨지는 버그가 나온다.
I 는 정책 선택 이다. 완벽한 격리는 비싸고, 더 약한 격리는 anomaly 를 허용한다. 단순 on/off 가 아니라 4단계가 존재하는 이유 — 정확성과 동시성의 충돌 — 이 시리즈 전체의 메시지다.
다음 글에서는 그 4단계가 각각 어떤 anomaly 를 막고 어떤 anomaly 를 허용하는지를 본다.