ACID 트랜잭션 — 송금과 매출 리포트로 이해하는 관계형 DB 의 네 가지 성질

2026. 10. 16. 21:40ㆍ카테고리 없음

강의 정리 · Hussein Nasser 「Relational Database ACID Transactions」

ACID 트랜잭션 — 송금과 매출 리포트로 이해하는 관계형 DB 의 네 가지 성질

원자성·격리성·일관성·지속성, 그리고 "무엇을 포기할 수 있나"

ACID 는 Postgres·MySQL·SQL Server·Oracle 같은 관계형 DB 가 지키는 네 가지 성질, 원자성(Atomicity)·일관성(Consistency)·격리성(Isolation)·지속성(Durability)입니다. 이 42분짜리 강의는 은행 송금과 매출 리포트 두 예시로 각 성질을 설명하고, NoSQL 이 그중 일관성을 내려놓은 이유까지 다룹니다. 강사는 순서를 일부러 A → I → C → D 로 바꿉니다. 원자성과 격리성을 알아야 일관성이 자연스럽게 이해되기 때문입니다.

1. 트랜잭션 — 쿼리를 한 묶음으로

트랜잭션은 쿼리 하나 이상을 묶은 작업 단위입니다. BEGIN 으로 시작하고, 끝에서 COMMIT(확정) 하거나 ROLLBACK(취소) 합니다. 이름은 "거래"에서 왔습니다. 돈을 주고 물건을 받는 거래가 둘 다 일어나거나 아예 안 일어나야 하듯, 묶인 쿼리도 함께 성공하거나 함께 취소돼야 하기 때문입니다.

강의의 예시는 계좌 ID 와 잔액 두 칸뿐인 표입니다. 1번 계좌에 1000달러, 2번 계좌에 500달러가 있고, 1번에서 2번으로 100달러를 보냅니다.

-- 여기부터 한 묶음(트랜잭션)이라고 DB 에 알린다
BEGIN;
-- 1번 계좌에 보낼 100달러가 있는지 잔액을 확인한다
SELECT balance FROM account WHERE id = 1;
-- 1번 계좌에서 100달러를 뺀다 (1000 → 900, 아직 확정 전이라 "더러운" 변경)
UPDATE account SET balance = balance - 100 WHERE id = 1;
-- 2번 계좌에 100달러를 더한다 (500 → 600)
UPDATE account SET balance = balance + 100 WHERE id = 2;
-- 두 변경을 함께 확정해 디스크에 남긴다
COMMIT;

2. 원자성 — 전부 되거나, 전부 안 되거나

원자(atom)처럼 더 쪼갤 수 없다는 뜻입니다. 묶인 쿼리가 모두 성공해야 하고, 하나라도 실패하면 전부 되돌립니다.

원자성이 없다면 어떻게 될까요. 1번에서 100달러를 뺀 직후 DB 가 꺼지고, 재시작해 보니 1번은 900, 2번은 여전히 500 입니다. 100달러가 허공으로 사라졌습니다. 원자성이 있으면 재시작할 때 끝나지 않은 트랜잭션을 통째로 되돌려 1000/500 원래 상태로 복구합니다.

3. 격리성 — 동시에 돌 때 남의 중간 상태를 보나

진행 중인 트랜잭션이 다른 진행 중인 트랜잭션의 변경을 볼 수 있는지를 정하는 성질입니다. 강사는 이걸 "개발자가 가장 꼭 이해해야 할 성질"이라고 강조합니다. 격리가 부족하면 읽기 이상 현상이 생기고, 이를 막으려고 DB 는 격리 수준이라는 손잡이를 제공합니다. 어디까지 돌릴지는 개발자가 정합니다.

4. 읽기 이상 현상 네 가지

예시 표는 판매 기록입니다. 상품1 은 10개×5달러 = 50, 상품2 는 20개×4달러 = 80, 합계 130. 나는 "상품별 매출 조회 → 합계 조회" 두 쿼리를 한 트랜잭션으로 묶어 매출 리포트를 만들고, 그 사이 직원 A 가 판매를 입력합니다.

  • 더러운 읽기(Dirty Read) — A 가 상품1 을 5개 더 팔았다고 입력만 하고 확정은 안 했는데, 내 합계가 그 값을 읽어 155 가 나옵니다. 위 목록은 50 + 80 인데 합계는 155 인 엉터리 리포트이고, A 가 취소하면 155 는 존재한 적 없는 숫자가 됩니다.
  • 반복 불가 읽기(Non-repeatable Read) — 이번엔 A 가 확정까지 합니다. 정상 값이라 더러운 읽기는 아니지만, 내 트랜잭션 안에서 처음엔 130, 다음엔 155 로 같은 질문에 답이 달라집니다. "같은 걸 왜 두 번 읽냐" 싶지만 실제 코드에선 자주 생긴다고 합니다.
  • 유령 읽기(Phantom Read) — A 가 기존 값은 안 건드리고 상품3(매출 10)을 새로 추가합니다. 내 합계에 상품3 이 끼어들어 130 이어야 할 게 140 이 됩니다. 기존 행은 잠글 수 있지만 아직 없는 행은 잠글 수 없어 가장 막기 어렵습니다.
  • 갱신 손실(Lost Update) — 강사가 덧붙인 항목입니다. 나와 A 가 둘 다 재고 10 을 읽고, 나는 1개 팔아 9 로, A 는 2개 팔아 8 로 저장합니다. 실제론 7 이어야 할 재고가 마지막 저장값 8 로 남습니다.

다만 이게 늘 고쳐야 할 오류는 아닙니다. 은행 잔액처럼 틀리면 안 되는 곳에선 막아야 하지만, 인스타그램 좋아요 수처럼 조금 틀려도 되는 곳에선 막을수록 느려지니 일부러 허용하기도 합니다.

5. 격리 수준 네 단계

같은 상황(A 가 상품1 을 5개 더 판매 입력)에서 단계별로 내 리포트가 어떻게 보이는지 정리하면 이렇습니다.

격리 수준 내 리포트에 보이는 것
Read Uncommitted A 가 확정하기도 전에 합계 155. 사실상 격리 없음, 네 문제 모두 생김.
Read Committed 각 쿼리가 그 쿼리 실행 시점에 확정된 값만 봄. 확정 전엔 130, 확정 순간부터 다음 조회는 155. 더러운 읽기만 막음. 대부분 DB 의 기본값.
Repeatable Read 트랜잭션 시작 시점에 확정된 값만 봄. 리포트가 끝날 때까지 계속 130. 반복 불가 읽기까지 막음.
Serializable 트랜잭션을 한 줄로 차례차례 실행한 것처럼 처리. 겹치면 한쪽을 기다리게 하거나 실패시킴. 네 문제 모두 막지만 가장 느림.

표 1. 아래로 갈수록 안전하고, 그만큼 느립니다.

Repeatable Read 의 구현 방식은 두 가지입니다.

  • 버전 방식 — 시작할 때 "버전 0" 이라는 사진을 찍고, 다른 트랜잭션이 버전 1·2·3 을 만들어도 나는 버전 0 만 읽습니다.
  • 잠금 방식 — 읽는 행에 공유 잠금(shared lock)을 걸어, 수정하려는 쪽이 배타 잠금(exclusive lock)을 못 걸게 막습니다. 남의 수정이 막히므로 대체로 버전 방식이 낫다고 합니다.

강사는 위키백과의 격리 수준 표가 완전히 정확하진 않다고 덧붙입니다. 버전 방식으로 구현하면 Repeatable Read 에서도 유령 읽기를 피할 수 있기 때문입니다. 단계는 트랜잭션마다 BEGIN 뒤에 SET TRANSACTION ISOLATION LEVEL ... 을 써서 고릅니다.

6. 일관성 ① — 데이터의 일관성

강사는 일관성을 데이터의 일관성과 읽기의 일관성 둘로 나눕니다. 데이터의 일관성은 "송금 전후 돈 총합은 같다", "좋아요 숫자 = 실제로 누른 사람 수" 처럼 사용자가 정한 규칙이 지켜지는 것이고, 외래키·기본키 같은 참조 무결성으로 강제합니다.

핵심은 원자성과 격리성이 깨지면 일관성도 깨진다는 점입니다. 원자성이 없어 100달러가 증발했고(900/500), 격리성이 없어 리포트가 130 과 155 로 어긋났습니다.

인스타그램 예시도 나옵니다. 사진 표에 "좋아요 수" 칸을 따로 두는 건 매번 좋아요 목록을 세는 것보다 빠르기 때문인데, 이 숫자와 실제 목록은 어긋날 수 있습니다. 500만 좋아요가 실제 목록과 정확히 맞을 리 없고, 유튜브 구독자를 "6.2천" 으로 반올림해 보여주는 것도 정확성보다 성능을 택한 결과입니다. 결국 요구사항을 보고 무엇을 포기할 수 있는지 판단하는 게 엔지니어의 일이라는 게 강의의 메시지입니다.

7. 일관성 ② — 읽기의 일관성과 NoSQL

읽기의 일관성은 "방금 확정한 값을 다음 사람이 바로 읽을 수 있나" 입니다. 서버가 한 대면 문제없습니다. 하지만 사용자가 많아져 DB 한 대로 감당이 안 되면, 원본 DB 내용을 계속 베껴 두는 복사본(replica) 서버를 두고 읽기를 나눠 맡깁니다. 쓰기는 원본에만, 읽기는 복사본에서 하는데, 베끼는 데 네트워크 지연이 있어 그 사이 복사본을 읽은 사람은 옛날 값을 봅니다.

그림 1. 원본에 확정한 값이 복사본까지 복제되는 동안, 복사본을 읽는 사용자는 옛 값을 볼 수 있습니다. NoSQL 은 이 상태를 처음부터 받아들이고 노드를 쉽게 늘리는 쪽을 택했습니다.

그래서 강사는 "이 문제는 관계형·NoSQL 모두 겪는다" 고 강하게 말합니다. MySQL·Postgres 처럼 탄탄한 DB 라도 서버를 이렇게 나누는 순간 똑같이 생깁니다. 유튜브도 처음엔 DB 한 대였다가 여러 대로 늘리고 Vitess(MySQL 위 샤딩 도구)를 만들었고, 독일과 일본 사용자가 같은 영상 조회수를 다르게 보는 건 서로 다른 복사본을 읽기 때문입니다.

관계형 DB 는 1970년대에 "큰 서버 한 대" 를 전제로 설계돼 수평 확장이 원래 어렵습니다. NoSQL 은 "어차피 일관성이 없잖아" 라며 일관성을 내려놓는 대신 노드를 쉽게 붙여 확장할 수 있게 했습니다. 기다리면 결국 최신값이 된다는 "최종적 일관성(eventual consistency)" 을 강사는 마케팅 용어라고 꼬집습니다. 관계형도 NoSQL 도 똑같이 겪는 현상이라는 겁니다.

8. 지속성 — 확정했으면 꺼져도 남는다

확정한 트랜잭션은 꺼져도 날아가지 않는 저장장치에 남아야 하고, 재시작 후에도 값이 그대로 있어야 합니다. Redis·Memcached 같은 메모리 DB 는 이걸 덜어 빠름을 택한 예이고, 잠깐 보관하는 캐시 용도라면 날아가도 괜찮습니다.

강사는 새로 나온 DB 들을 "ACID 중 하나를 빼서 만든 것" 으로 봅니다. Redis 는 지속성을, Cassandra·MongoDB 는 일관성을 덜어냈고, 이와 함께 스키마·참조 무결성·보조 인덱스 같은 규칙도 느슨해졌다고 합니다. SQL 과 NoSQL 의 본격 비교는 다음 영상으로 넘기며 강의가 끝납니다.

정리

  • 트랜잭션은 쿼리 묶음이고 BEGIN~COMMIT/ROLLBACK 으로 경계를 정한다.
  • 원자성 — 중간에 꺼져도 반쪽 상태를 남기지 않고 통째로 되돌린다.
  • 격리성 — 더러운 읽기·반복 불가 읽기·유령 읽기·갱신 손실을 격리 수준으로 막고, 높일수록 느려진다.
  • 일관성 — 원자성과 격리성의 결과이며, 서버를 나누면 관계형 DB 도 최신값을 늦게 읽는다.
  • 지속성 — 확정한 값은 전원이 꺼져도 남는다. 무엇을 포기할지는 요구사항이 정한다.

출처: Hussein Nasser, 「Relational Database ACID Transactions (Explained by Example)」, YouTube, 42:43.

더 많은 글 → dinsightlab.com

 

Passive

미국 지수 투자자를 위한 금융 데이터 분석 사이트

dinsightlab.com