사전 ACID
개념

ACID

gabury1고친 사람 github-actions[bot]

ACID 는 데이터베이스가 작업 한 묶음에 대해 지켜 주는 네 가지 약속입니다. 한 계좌에서 돈을 빼고 다른 계좌에 넣는 이체처럼 여러 단계가 한 뜻을 이루는 작업이 그런 묶음입니다. 이 약속이 있으면 그런 작업을 맡겨도 어중간하게 끝난 상태가 남지 않습니다. 개발자는 도중의 실패와 남의 끼어들기를 일일이 수습하는 코드를 쓰지 않아도 됩니다.

쉽고 빠른 이해

ACID 는 데이터베이스에 맡긴 일이 어그러져도 데이터가 이상해지지 않게 하는 네 약속입니다. 계좌이체에서 「내 통장에서만 돈이 빠진 상태」가 안 남는 것이 그 결과입니다.

이게 없으면 그 뒤치다꺼리를 앱 코드가 직접 해야 합니다. 서버가 중간에 죽으면 반쯤 바뀐 값을 손으로 찾아 고쳐야 합니다. 같은 데이터를 두 요청이 함께 건드리면 한쪽이 한 일이 조용히 사라집니다.

네 약속은 이렇게 나뉩니다.

  1. 다 되거나 아예 안 된 것으로 끝납니다
  2. 끝난 결과는 정해 둔 규칙을 어기지 않습니다
  3. 같이 도는 작업끼리 서로의 중간 결과를 못 봅니다
  4. 끝났다고 답한 것은 전원이 나가도 남습니다

대가는 속도와 범위입니다. 되돌릴 준비를 해 두고 남의 접근을 막느라 처리가 늦어집니다. 그리고 이 약속은 데이터베이스 한 대 안에서만 통합니다. 여러 대에 걸친 일은 따로 챙겨야 합니다.

여러 곳을 함께 바꿔야 뜻이 맞는 일에 씁니다. 한 행만 바꾸는 쓰기에는 필요 없습니다.

상세

은행 창구에 이체를 맡기는 장면을 떠올리면 됩니다. 창구 직원은 양쪽 통장을 같이 처리합니다. 옆 창구에서 벌어지는 일과 섞지 않습니다. 끝낸 일은 장부에 적어 둡니다.

맡긴 사람은 그 과정을 지켜보고 있을 필요가 없습니다.

ACID 는 Atomicity, Consistency, Isolation, Durability 네 낱말의 첫 글자를 모은 이름입니다. 우리말로는 원자성, 일관성, 격리성, 지속성입니다. 데이터베이스는 트랜잭션 하나마다 이 넷을 지킵니다. 트랜잭션은 여러 읽기와 쓰기를 한 묶음으로 처리하는 단위입니다.

계좌 하나에서 100을 빼고 다른 계좌에 100을 더하는 이체가 그런 묶음입니다. 두 쓰기는 같이 반영되거나 같이 취소되어야 뜻이 맞습니다.

네 글자와 그 약속

네 글자는 각각 한 가지 약속을 집니다. 아래 표가 그 짝입니다.

글자 성질 약속
A 원자성 다 되거나 아예 안 된 것으로 끝난다
C 일관성 끝난 결과가 정해 둔 규칙을 어기지 않는다
I 격리성 같이 도는 트랜잭션끼리 중간 결과를 못 본다
D 지속성 끝났다고 답한 결과는 장애 뒤에도 남는다

넷은 서로 다른 질문에 답합니다.

원자성

원자성은 여러 단계로 된 트랜잭션이 전부 일어나거나 전혀 일어나지 않은 것으로 보이게 하는 성질입니다. 앞의 이체에서 한쪽 잔액만 줄어든 상태가 안 남는 것이 이 성질입니다.

트랜잭션이 끝나는 길은 둘입니다. 한 일을 한꺼번에 반영하는 것이 커밋이고, 한 일을 전부 취소해 원래 값으로 되돌리는 것이 롤백입니다.

도중에 서버가 꺼져 커밋도 롤백도 못 받은 트랜잭션이 남을 수 있습니다. 그런 트랜잭션은 데이터베이스가 다시 켜질 때 되돌립니다. 그래서 반쯤 된 결과는 장애 뒤에도 안 남습니다.

일관성

일관성은 끝난 트랜잭션이 정해 둔 규칙을 어기지 않는다는 약속입니다. 잔액이 음수가 되면 안 된다거나, 주문 행이 가리키는 회원 번호가 실제로 있어야 한다는 것이 그런 규칙입니다.

이런 규칙 중 데이터베이스에 미리 적어 둘 수 있는 것을 제약 조건이라고 합니다. 규칙을 어기는 쓰기가 들어오면 데이터베이스가 그 트랜잭션을 실패로 끝냅니다.

네 성질 가운데 일관성만 절반이 애플리케이션 몫입니다. 데이터베이스는 자기가 아는 규칙만 지켜 줍니다. 적립금을 결제 금액에 정해진 비율로 준다는 업무 규칙은 코드가 맞게 짜야 지켜집니다.

여러 대의 서버가 한 시스템처럼 도는 구성을 분산 시스템이라고 합니다. 같은 낱말이 거기서는 다른 뜻으로 쓰입니다. 그쪽 일관성은 여러 대가 같은 값을 보여 주는가를 말합니다. 두 뜻은 이름만 같고 다루는 문제가 다릅니다.

격리성

격리성은 같이 도는 트랜잭션끼리 서로의 중간 결과를 못 보게 하는 약속입니다. 이 약속이 온전히 지켜지면 각 트랜잭션은 자기 혼자 도는 것처럼 보입니다.

없으면 무슨 일이 나는지 한 장면으로 보겠습니다. 재고 행을 읽어 1을 빼고 다시 쓰는 요청 둘이 겹친 경우입니다.

sequenceDiagram
    participant A as 요청 A
    participant R as 재고 행
    participant B as 요청 B
    A->>R: 읽기
    R-->>A: 10
    B->>R: 읽기
    R-->>B: 10
    A->>R: 9 를 쓴다
    B->>R: 9 를 쓴다
    Note over R: 둘이 팔았는데 하나만 줄었다

둘이 하나씩 팔았으니 재고는 8이어야 합니다. 그림의 결과는 9입니다. 한쪽이 쓴 값이 다른 쪽의 쓰기에 덮여 사라졌습니다. 이렇게 사라지는 갱신을 갱신 손실이라고 부릅니다.

격리성을 온전히 지키면 이 장면이 안 생깁니다. 대신 값이 비쌉니다. 그래서 데이터베이스는 어디까지 지킬지를 고를 수 있게 해 두었고, 그 단계를 격리 수준이라고 합니다.

지속성

지속성은 커밋에 성공했다고 답한 결과가 그 뒤에 사라지지 않는다는 약속입니다. 답을 받은 직후 전원이 나가도 그 데이터는 남아 있어야 합니다.

지키는 방법은 기록을 먼저 남기는 것입니다. 무엇을 어떻게 바꿀지 로그 파일에 먼저 적고, 그 기록이 디스크에 닿은 뒤에 커밋 성공을 답합니다. 이 방식을 WAL(Write-Ahead Logging, 선행 기록)이라고 합니다.

로그가 있으면 다시 켤 때 정리할 수 있습니다. 커밋 기록까지 남은 트랜잭션은 끝까지 반영하고, 커밋 기록이 없는 트랜잭션은 되돌립니다.

네 성질이 맡는 때

네 약속은 트랜잭션의 서로 다른 때를 맡습니다. 진행 중에는 격리성이 남의 눈을 막습니다. 끝날 때는 원자성과 일관성이 결과를 판정합니다. 끝난 뒤는 지속성이 지킵니다.

flowchart TD
    R["진행 중 — 격리성"] --> Q{"모든 단계가 성공했나"}
    Q -->|예| C["커밋 — 원자성 · 일관성"]
    Q -->|아니오| B["롤백 — 원자성"]
    C --> D["장애 뒤에도 남음 — 지속성"]
    B --> N["아무 일도 없던 상태"]

그림에서 볼 것은 끝이 둘뿐이라는 점입니다. 전부 반영된 쪽이거나, 아무 일도 없던 것과 같은 쪽입니다.

코드에서 트랜잭션의 경계를 긋는 법

애플리케이션이 할 일은 트랜잭션의 시작과 끝을 알려 주는 것입니다. 잔액이 1000인 계좌에서 100을 옮기는 이체를 SQL(Structured Query Language, 구조화 질의 언어)로 적으면 이렇게 됩니다.

SQL
BEGIN;
UPDATE acct SET bal = bal - 100 WHERE id = 1;
UPDATE acct SET bal = bal + 100 WHERE id = 2;
COMMIT;
SELECT bal FROM acct WHERE id = 1;  -- 900

BEGIN 과 COMMIT 사이의 두 문장이 한 트랜잭션입니다. 마지막 조회가 900을 돌려주는 것은 두 문장이 함께 확정됐다는 뜻입니다.

둘째 문장이 실패하면 애플리케이션이 COMMIT 대신 ROLLBACK 을 보냅니다. 그러면 첫 문장까지 취소됩니다. 연결이 끊겨 COMMIT 이 아예 오지 않은 트랜잭션은 데이터베이스가 스스로 되돌립니다.

격리성을 깎아 쓰는 까닭

네 성질 가운데 실무에서 낮춰 쓰는 것은 격리성 하나입니다. 온전한 격리는 트랜잭션을 한 줄로 세워 처리하는 것과 같습니다. 그만큼 동시에 처리하는 양이 줍니다.

격리 수준은 아래 현상 가운데 무엇까지 허용할지로 나뉩니다. 수준이 낮을수록 더 많이 허용됩니다.

현상 무엇을 겪게 되나
갱신 손실 내가 쓴 값이 남의 쓰기에 덮여 사라진다
더티 리드 아직 커밋 안 된 남의 값을 읽는다
반복 불가능한 읽기 같은 질의를 두 번 했는데 값이 달라진다
팬텀 읽기 조건에 걸리는 행 집합이 달라진다

데이터베이스는 MVCC(Multi-Version Concurrency Control, 다중 버전 동시성 제어)로 이 사이를 받칩니다. 쓰는 쪽이 새 버전을 만들고 읽는 쪽은 옛 버전을 읽습니다. 읽기가 쓰기를 기다리지 않아도 되므로 격리를 어느 정도 지키면서 동시 처리를 살립니다.

ACID 가 약속하지 않는 것

세 가지는 이 약속 밖이라 따로 챙겨야 합니다.

첫째는 여러 저장소에 걸친 일입니다. 두 대나 서비스 여러 개에 걸치면 한쪽만 커밋되는 반쪽 결과가 다시 생깁니다. 이것을 푸는 고전적인 방법이 2단계 커밋입니다. 모두에게 준비됐냐고 먼저 묻고, 전부 예라고 답할 때만 함께 커밋하는 방식입니다.

둘째는 같은 요청을 두 번 보내는 경우입니다. 원자적인 이체라도 두 번 보내면 두 번 빠집니다. 같은 요청을 여러 번 보내도 결과가 한 번 보낸 것과 같은 성질을 멱등성이라고 합니다. 재시도를 안전하게 하려면 이것을 따로 챙겨야 합니다.

셋째는 업무 규칙입니다. 일관성이 지켜 주는 것은 데이터베이스에 적어 둔 규칙까지입니다. 코드가 잘못 계산해 넣은 값은 그 규칙을 어기지 않는 한 저장됩니다.

여러 대에 흩어진 데이터와 BASE

데이터를 여러 대에 나눠 두면 네 약속을 한꺼번에 지키기 어려워집니다. 서버 사이의 연결이 끊긴 동안에도 응답을 주려면, 모든 대가 같은 값을 보여 주는 것을 잠시 포기해야 합니다.

그래서 반대쪽을 고른 설계에는 따로 이름이 붙었습니다. BASE(Basically Available, Soft state, Eventually consistent, 대체로 가용 · 굳지 않은 상태 · 결국 일관)는 일단 응답하고 값은 시간이 지나면서 맞춰 간다는 쪽입니다.

시간이 지나면 모든 대가 같은 값으로 모인다는 약속을 결과적 일관성이라고 합니다. 최종 일관성이라고도 부릅니다.

둘 중 하나를 시스템 전체에 골라야 하는 것은 아닙니다. 잔액과 결제는 ACID 쪽에 두고, 조회수나 활동 기록은 BASE 쪽에 두는 식으로 데이터마다 갈라 쓰는 경우가 많습니다.

챙겨야 하는 때

여러 곳을 함께 바꿔야 뜻이 맞는 일이면 트랜잭션으로 묶습니다. 주문을 넣으면서 재고를 줄이는 일, 이체, 글과 첨부 목록을 같이 저장하는 일이 그렇습니다.

한 행만 바꾸는 쓰기는 묶을 필요가 없습니다. 데이터베이스는 문장 하나를 이미 원자적으로 처리합니다.

트랜잭션은 짧게 둡니다. 트랜잭션이 길수록 잠금이 오래 이어지고 다른 요청이 그만큼 기다립니다. 메일 발송이나 외부 서비스 호출처럼 되돌릴 수 없는 일은 트랜잭션 안에 넣어도 롤백되지 않습니다.

관련 항목

ACID 를 이루는 네 성질

원자성 · 일관성 · 격리성 · 지속성

ACID 가 걸리는 처리 단위와 명령

트랜잭션 · 커밋 · 롤백 · 세이브포인트 · 오토커밋 · SQL

ACID 를 지키려고 데이터베이스가 쓰는 장치

WAL · 언두 로그 · 리두 로그 · 체크포인트 · 락 · MVCC · 스냅샷 격리 · 제약 조건 · 장애 복구

격리성을 낮췄을 때 나타나는 이상 현상

격리 수준 · 더티 리드 · 반복 불가능한 읽기 · 팬텀 읽기 · 갱신 손실 · 쓰기 스큐 · 직렬화 가능성

ACID 와 맞세워지는 설계 원칙

BASE · CAP · 결과적 일관성 · 강한 일관성 · 분산 시스템

여러 저장소에 걸쳐 원자성을 얻는 방법

2단계 커밋 · 사가 · 보상 트랜잭션 · 분산 트랜잭션 · 트랜잭셔널 아웃박스 · 멱등성 · 재시도

ACID 를 내세우는 데이터베이스

PostgreSQL · MySQL · SQLite · InnoDB · Oracle Database

다른 이름: Atomicity Consistency Isolation Durability · ACID 성질 · ACID 속성 · 애시드