본문 바로가기
TYLER SONGBlog
← Blog index
CS학습6 min read

ACID: 트랜잭션이 데이터 정합성을 지키는 네 가지 성질

ACID는 데이터베이스 트랜잭션(Transaction)이 보장해야 하는 원자성(Atomicity), 일관성(Consistency), 격리성(Isolation), 지속성(Durability)을 가리킨다. 각 성질은 로그, 락, 다중 버전 동시성 제어(MVCC) 같은 구체적 메커니즘으로 구현된다. 이 글에서는 PostgreSQL 기준으로 동작 원리와 SQL 예

By 송민성

1. 개념

ACID는 트랜잭션(Transaction)이 지켜야 할 네 가지 성질의 머리글자다. 트랜잭션은 하나의 논리적 작업 단위로 묶인 SQL 연산의 집합이다.

성질영어의미
원자성Atomicity트랜잭션 안의 연산은 전부 반영되거나 전부 취소된다
일관성Consistency트랜잭션 전후에 무결성 제약 조건이 유지된다
격리성Isolation동시에 실행되는 트랜잭션이 서로의 중간 상태를 간섭하지 않는다
지속성Durability커밋(COMMIT)된 결과는 장애가 나도 사라지지 않는다

2. 왜 사용하는가

계좌 이체를 예로 들면, A 계좌에서 출금하고 B 계좌에 입금하는 두 연산 사이에 서버가 죽을 수 있다. 트랜잭션이 없다면 돈이 사라지거나 복제된 상태로 데이터가 남는다. 동시에 여러 요청이 같은 행을 읽고 쓸 때 발생하는 경합(race condition)도 마찬가지다.

ACID는 애플리케이션 코드가 장애와 동시성 문제를 일일이 처리하지 않아도 되도록, 그 책임을 DB 엔진이 지게 하는 계약이다.

3. 동작 원리

원자성: 롤백과 로그 PostgreSQL은 변경 내용을 먼저 선행 기록 로그(WAL, Write-Ahead Log)에 기록한다. 트랜잭션이 중간에 실패하면 해당 트랜잭션의 변경은 가시(visible)하지 않게 처리된다. PostgreSQL은 MVCC 구조상 기존 행 버전을 남겨두고 새 버전을 만들기 때문에, 롤백된 트랜잭션이 만든 버전은 단순히 무효로 취급된다.

일관성: 제약 조건 PRIMARY KEY, FOREIGN KEY, CHECK, UNIQUE, NOT NULL 같은 제약 조건을 DB가 검사한다. 위반하면 트랜잭션이 실패한다. 다만 "업무 규칙상 올바른 상태"까지 DB가 알아서 보장하지는 않는다. 제약 조건으로 표현한 규칙만 보장되며, 나머지는 애플리케이션 설계의 몫이다.

격리성: MVCC와 격리 수준 PostgreSQL은 다중 버전 동시성 제어(MVCC, Multi-Version Concurrency Control)를 사용해 읽기가 쓰기를 막지 않도록 한다. 각 트랜잭션은 스냅샷(snapshot) 기준으로 행 버전의 가시성을 판단한다. 격리 수준(isolation level)은 SQL 표준에서 네 단계로 정의된다.

  • READ UNCOMMITTED
  • READ COMMITTED
  • REPEATABLE READ
  • SERIALIZABLE

PostgreSQL의 기본값은 READ COMMITTED이며, READ UNCOMMITTED를 지정해도 READ COMMITTED처럼 동작한다. REPEATABLE READ는 스냅샷 격리(Snapshot Isolation)로 구현되고, SERIALIZABLE은 직렬화 가능 스냅샷 격리(SSI)로 구현된다.

지속성: WAL과 fsync 커밋 시점에 해당 WAL 레코드가 디스크에 플러시(flush)된 뒤에야 클라이언트에 성공이 반환된다. 데이터 파일 자체는 나중에 기록되어도 되며, 장애 후에는 WAL을 재생(replay)해 복구한다.

4. 코드 예제

sql
-- 준비 DROP TABLE IF EXISTS accounts; CREATE TABLE accounts ( id INT PRIMARY KEY, owner TEXT NOT NULL, balance INT NOT NULL CHECK (balance >= 0) ); INSERT INTO accounts VALUES (1, 'alice', 1000), (2, 'bob', 500); -- 1) 원자성: 정상 이체 BEGIN; UPDATE accounts SET balance = balance - 300 WHERE id = 1; UPDATE accounts SET balance = balance + 300 WHERE id = 2; COMMIT; SELECT * FROM accounts ORDER BY id; -- alice 700, bob 800 -- 2) 일관성 + 원자성: CHECK 위반 시 트랜잭션 전체 실패 BEGIN; UPDATE accounts SET balance = balance + 2000 WHERE id = 2; UPDATE accounts SET balance = balance - 2000 WHERE id = 1; -- ERROR: CHECK 위반 ROLLBACK; -- 오류 후에는 트랜잭션이 중단 상태이므로 ROLLBACK만 가능 SELECT * FROM accounts ORDER BY id; -- 여전히 alice 700, bob 800

격리성은 세션 두 개로 확인한다.

sql
-- 세션 A BEGIN ISOLATION LEVEL REPEATABLE READ; SELECT balance FROM accounts WHERE id = 1; -- 700 -- 세션 B (별도 접속) BEGIN; UPDATE accounts SET balance = 0 WHERE id = 1; COMMIT; -- 세션 A SELECT balance FROM accounts WHERE id = 1; -- 여전히 700 (스냅샷 유지) UPDATE accounts SET balance = balance - 100 WHERE id = 1; -- ERROR: could not serialize access due to concurrent update ROLLBACK;

동시 갱신 충돌을 막는 또 다른 방법은 행 잠금이다.

sql
BEGIN; SELECT balance FROM accounts WHERE id = 1 FOR UPDATE; -- 해당 행 잠금 UPDATE accounts SET balance = balance - 100 WHERE id = 1; COMMIT;

5. 시간 복잡도 또는 성능 특성

ACID 보장에는 비용이 따른다. 정확한 수치는 하드웨어와 설정에 좌우되므로 경향만 정리한다.

  • 지속성 비용: 커밋마다 WAL을 디스크에 동기화(fsync)하므로 커밋 지연 시간은 저장 장치의 쓰기 지연에 크게 영향을 받는다. 여러 트랜잭션의 커밋을 묶어 한 번에 플러시하는 그룹 커밋(group commit)이 이 비용을 완화한다.
  • 격리 수준과 충돌: 격리 수준이 높을수록 직렬화 실패(serialization failure)로 인한 재시도가 늘 수 있다.
  • 락 경합: FOR UPDATE처럼 행을 잠그는 방식은 같은 행에 대한 동시 요청을 대기시켜 처리량을 낮출 수 있다.
  • 긴 트랜잭션: PostgreSQL에서 오래 열린 트랜잭션은 죽은 행 버전의 정리(VACUUM)를 막아 테이블 비대화(bloat)를 유발할 수 있다.

6. 실무 사용 사례

  • 결제·주문 처리: 재고 차감, 결제 기록, 주문 생성을 하나의 트랜잭션으로 묶는다.
  • 중복 방지: UNIQUE 제약과 INSERT ... ON CONFLICT로 동시 요청에서도 중복 데이터를 막는다.
  • 재시도 로직: REPEATABLE READ 이상을 쓸 때는 직렬화 실패(SQLSTATE 40001)와 교착 상태(deadlock, 40P01) 시 트랜잭션 전체를 재시도하도록 구현한다.
  • 복제·백업: WAL은 복구뿐 아니라 스트리밍 복제(streaming replication)에도 쓰인다.

7. 주의할 점

  • 자동 커밋(autocommit): 명시적으로 BEGIN하지 않으면 각 문장이 별도 트랜잭션으로 커밋된다. 두 개의 UPDATE를 따로 실행하면 원자성이 없다.
  • 트랜잭션 안의 외부 호출: 트랜잭션을 연 채 HTTP 호출 등 느린 외부 작업을 하면 락과 커넥션을 오래 점유한다. 외부 시스템의 변경은 롤백되지도 않는다.
  • 기본 격리 수준 오해: READ COMMITTED에서는 같은 트랜잭션 안에서도 문장마다 다른 스냅샷을 보므로, "읽고 나서 쓰는" 패턴은 갱신 손실(lost update)이 생길 수 있다. FOR UPDATE, 원자적 UPDATE ... SET x = x + n, 또는 더 높은 격리 수준으로 대응한다.
  • `synchronous_commit = off`: 성능은 오르지만 장애 시 최근 커밋이 유실될 수 있다. 데이터 파일이 손상되지는 않지만 지속성 보장은 약해진다.
  • ACID와 CAP의 C는 다르다: ACID의 일관성은 무결성 제약 유지이고, CAP 정리의 일관성(Consistency)은 분산 노드 간 최신 값 읽기를 뜻한다.
  • 분산 환경: 여러 DB나 서비스에 걸친 작업은 단일 DB 트랜잭션으로 보장되지 않는다. 2단계 커밋(2PC)이나 사가(Saga) 패턴 같은 별도 설계가 필요하다.

8. 핵심 정리

  • ACID는 원자성, 일관성, 격리성, 지속성의 약자이며 트랜잭션의 신뢰성 계약이다.
  • 원자성과 지속성은 WAL과 롤백 처리로, 격리성은 MVCC와 락으로, 일관성은 제약 조건으로 뒷받침된다.
  • PostgreSQL 기본 격리 수준은 READ COMMITTED이고, 동시 갱신이 문제되는 로직에는 락이나 상위 격리 수준을 명시적으로 선택해야 한다.
  • 높은 격리 수준을 쓴다면 직렬화 실패에 대한 재시도 로직이 필수다.
  • 트랜잭션은 짧게 유지하고, 여러 시스템에 걸친 정합성은 ACID만으로 해결되지 않는다는 점을 기억한다.