데이터베이스 3편 - 트랜잭션, 정확도와 처리량 사이의 조약
Published: May 22, 2026
어느 이커머스의 플래시 세일. 한정판 상품 100개가 1초 만에 매진되었다. 하지만 실제로는 100개의 재고에 387명이 결제되는 시스템 오류가 발생했다. 대용량 트래픽이 동시에 몰렸을 때, 모든 사용자가 남은 재고를 1개(
stock=1)로 동시에 읽고 결제를 진행했기 때문이다.2022년 10월 13일에 발생했던 구글 빅쿼리(BigQuery) 장애도 이와 같다. 평소와 동일한 코드였지만 트래픽이 잠시 늘어난 순간 전체 처리량이 마비되었다. 데이터 오차를 막기 위해 세운 안전장치(락)가 역효과를 낸 것이다. 하나의 요청이 다른 요청을 기다리고, 그 요청이 또 다른 요청을 기다리는 교착상태(데드락)에 빠져 락이 풀리지 않았다.
Reference: E-Commerce Race Condition (2026)
Reference: Production Postmortem Collection (danluu)
두 시스템이 무너진 양상은 정반대지만, 본질은 같은 곳을 가리킨다. 같은 데이터를 여러 사용자가 동시에 건드리는 순간, 시스템은 ‘데이터 오염’과 ‘시스템 마비’라는 실패와 마주한다.
하나는 데이터가 깨지는 것이다. 재고 100개에 387명이 결제되고, 재고가 음수로 떨어진다. 데이터가 허용된 규칙 안에서 정확도를 유지하지 못하는 문제. 이것이 정합성(Consistency) 실패다.
다른 하나는 시스템이 멈추는 것이다. 데이터 자체는 멀쩡하지만, 모든 요청이 서로를 기다리며 아무것도 진행하지 못한다. 여러 작업이 지연 없이 함께 처리되지 못하는 문제. 바로 동시성(Concurrency) 실패다.
이 둘은 같은 자원을 두고 경쟁하는 관계다. 정확도를 보장하려면 처리 속도가 떨어지고, 처리량을 늘리면 데이터가 정확하지 않다.
동시성과 정합성의 균형을 잡는 트랜잭션
섹션 제목: “동시성과 정합성의 균형을 잡는 트랜잭션”문제를 한 장면으로 좁혀보자.
마지막 남은 재고 1개가 있다. 사용자 A가 stock = 1을 읽는다. 사용자 B도 stock = 1을 읽는다. 둘 다 재고가 남아 있다고 판단하고 결제를 진행한다. 각자의 행동만 보면 둘 다 정상이다. 하지만 두 요청이 동시에 실행되는 순간, 시스템은 1개의 재고를 2명에게 팔아버린다.
[ 남은 재고 수량 = 1 ]
사용자 A ──▶ "재고 있음" 판단 ──┐ ├──▶ 동시 실행 ──▶ 시스템 정합성 X사용자 B ──▶ "재고 있음" 판단 ──┘이것이 핵심 문제다. 각 요청은 정상인데, 동시에 실행되면 시스템 전체가 비정상이 된다. 합리적인 행동들이 겹쳐졌을 때 전체가 무너지는 구조. 이 모순을 방치하면 시스템은 트래픽이 몰릴 때마다 같은 혼란을 반복한다.
문제는 요청의 로직이 아니라, 여러 요청이 같은 데이터를 동시에 공유한다는 것 이다. 읽기와 수정 사이에 다른 작업이 끼어들면, 데이터는 더 이상 하나의 일관된 상태를 유지하지 못한다.
그래서 데이터베이스가 개입한다. 여러 작업을 하나의 묶음으로 감싸서, 그 안에서는 다른 작업이 끼어들 수 없도록 통제하는 조약을 만든다. 이 조약의 이름이 트랜잭션(Transaction) 이다.
트랜잭션은 정합성과 동시성 사이의 균형을 다루는 조약이다. 모든 작업을 자유롭게 풀어두면 데이터가 깨지고, 모든 작업을 하나씩 줄 세우면 시스템이 멈춘다. 트랜잭션은 그 사이에서 각 작업에 임시적 독점권을 부여하는 방식으로 두 가치 사이의 균형을 잡는 통제 단위다.
ACID — 조약의 네 가지 규칙
섹션 제목: “ACID — 조약의 네 가지 규칙”그렇다면 트랜잭션은 정확히 무엇을 막으려는 걸까.
데이터베이스는 동시에 몰려드는 작업 속에서도 데이터의 신뢰를 유지해야 한다. 중간 상태가 노출되거나, 작업끼리 서로 간섭하거나, 이미 끝난 결과가 사라지면 시스템은 더 이상 같은 데이터를 바라볼 수 없게 된다.
ACID는 데이터베이스가 이런 사고를 막기 위해 세운 네 가지 안전 규칙이다.
원자성(Atomicity), 일관성(Consistency), 지속성(Durability)은 비교적 명확하다. 중간 상태를 남기지 않고, 규칙 위반 데이터를 저장하지 않으며, 확정된 결과를 잃지 않는 것이다. 송금 중 돈이 증발하거나 재고가 음수가 되는 불상사를 막는 고정 규제다.
하지만 **격리성(Isolation)**은 다르다. 진행 중인 작업의 간섭을 막는 이 규칙은 강하게 보장할수록 동시에 처리할 수 있는 작업 수가 줄어든다. 그리고 바로 여기서, 정합성과 동시성의 협상이 시작된다.
격리 수준 — 동시성과 정합성의 줄다리기
섹션 제목: “격리 수준 — 동시성과 정합성의 줄다리기”격리성은 결국 같은 데이터를 동시에 얼마나 많은 작업에게 열어둘 것인가의 문제다. 너무 자유롭게 열어두면 잘못된 데이터를 읽고, 너무 엄격하게 잠그면 시스템 전체가 느려진다. 격리 수준은 이 둘 사이에서 데이터베이스가 준비한 단계별 타협안이자, 조약 비용이 점점 비싸지는 과정이다.
동시성 우선▲│ Read Uncommitted│ └─ 미확정 데이터 허용││ Read Committed│ └─ 확정 데이터만 조회││ Repeatable Read│ └─ 동일 스냅샷 유지││ Serializable│ └─ 순차 실행 강제│└────────────────────────────────────────────────────────▶ 정합성 우선- Read Uncommitted — 동시성 ≫ 정합성 가장 느슨한 단계다. 아직 확정되지 않은 데이터까지 그대로 읽게 둔다. 처리량은 높지만, 나중에 취소될 수도 있는 값을 근거로 의사결정을 하게 된다. 동시성을 극대화한 대신, 데이터 신뢰를 거의 포기한 상태다.
- Read Committed — 동시성 > 정합성 다른 트랜잭션이 확정(Commit)한 데이터만 읽을 수 있다. PostgreSQL과 Oracle의 기본값이며, 실무에서 가장 널리 쓰이는 절충안이다. 대신 같은 트랜잭션 안에서도 다시 조회하면 값이 달라질 수 있다. 최소한의 안전성만 확보한 단계다.
하지만 문제가 남는다. 같은 트랜잭션 안에서 다시 조회했는데 값이 달라질 수 있다. 다른 사용자의 수정이 중간에 끼어들기 때문이다.
- Repeatable Read — 동시성 < 정합성 트랜잭션이 처음 읽은 상태를 끝까지 유지한다. 중간에 다른 사용자가 데이터를 바꿔도, 작업 도중 기준이 흔들리지 않도록 만든다. 대신 더 많은 버전 관리와 충돌 비용을 감수해야 한다.
여기서도 완전히 끝난 건 아니다. 트랜잭션이 시작된 이후 새로 추가된 데이터가 조회 결과에 끼어드는 문제, 즉 팬텀 리드 같은 예외는 여전히 남는다.
- Serializable — 동시성 ≪ 정합성 아예 모든 작업이 순서대로 실행된 것처럼 강제한다. 정합성은 가장 안전하지만, 동시에 처리할 수 있는 작업 수는 급격히 줄어든다. 데이터 오류를 막기 위해 사실상 동시성을 희생하는 수준의 통제다.
격리 수준의 선택은 기술 문제가 아니다. 잘못된 데이터가 더 위험한가, 느린 시스템이 더 위험한가에 대한 비즈니스 판단이다.
금융·회계처럼 작은 오차도 치명적인 영역은 높은 격리 수준을 감수할 가치가 있다. 반면 SNS 피드나 추천 시스템처럼 약간의 시차가 큰 문제가 되지 않는 서비스는 동시성을 우선하는 편이 훨씬 효율적이다.
MVCC — 동시성과 정합성의 공존
섹션 제목: “MVCC — 동시성과 정합성의 공존”여기까지 들으면 한 가지 의문이 생긴다. 정합성을 지키려면 다른 트랜잭션의 접근을 막아야 한다. 가장 단순한 해법은 락을 걸어 다른 작업을 기다리게 만드는 것이지만, 이는 곧 처리량의 폭락을 의미한다.
데이터베이스는 두 요구를 동시에 만족시키는 방법을 찾아냈다. 정합성이 요구하는 안전(진행 중인 작업 감추기)은 과거의 버전으로 해결하고, 동시성이 요구하는 속도(기다림 없애기)는 새로운 버전으로 해결한다. 즉, 읽기는 쓰기를 막지 않고, 쓰기는 읽기를 막지 않는다.
PostgreSQL과 MySQL InnoDB의 **MVCC(Multi-Version Concurrency Control)**가 바로 이 구조다. 데이터를 덮어쓰는 대신 새 버전을 추가하는 방식이다. 새 버전으로 쓰기 작업이 진행되는 동안 읽기 작업은 기존 버전을 읽고 지나가므로, 같은 데이터에 접근하면서도 대기 없이 처리가 가능하다.
결국 데이터베이스는 하나의 자원에 버전이라는 개념을 부여하여, 동일한 시점에 발생하는 물리적 충돌을 구조적으로 우회했다.
락 — 조약을 집행하는 손
섹션 제목: “락 — 조약을 집행하는 손”정합성과 동시성의 충돌은 결국 여러 트랜잭션이 같은 자원을 동시에 건드리는 순간 발생한다. 락(Lock) 은 데이터베이스가 그 충돌을 통제하기 위해 사용하는 장치다. 다시 말하면 ACID와 격리 수준이 조약 문서라면, 락은 그 조약을 현장에서 집행하는 손이다.
그런데 데이터베이스는 이 손을 움직이는 방식에서 두 가지 입장으로 갈라진다. 충돌을 처음부터 막아설 것인가, 아니면 마지막 순간에만 확인할 것인가.
충돌이 빈번할 거라는 입장 — 비관적 락
섹션 제목: “충돌이 빈번할 거라는 입장 — 비관적 락”비관적 락은 충돌을 기정사실로 본다. 정합성을 먼저 지키기 위해 데이터에 접근하는 순간 락을 걸어 다른 트랜잭션의 진입을 시작 전에 차단한다. 데이터 선점을 위한 대기 시간과 처리량 저하가 모두 선불 청구서로 발행된다. 동시성을 양보하는 가격이 매번 명확히 청구되는 구조다.
예를 들어, 재고 100개에 1만 건의 주문이 몰리는 선착순 타임 세일을 떠올려보자. 이때 정합성을 양보하면 재고보다 더 많이 판매하는 사태가 벌어지므로, 비관적 락은 1만 건을 한 줄로 세운다. 첫 100건이 재고를 가져가고, 나머지는 순서대로 재고 없음을 확인한 뒤 거절된다. 정합성을 확실히 지켜낸 대가로 시스템 전체가 줄의 속도로 느려지는 비용을 치른다.
충돌이 드물 거라는 입장 — 낙관적 락
섹션 제목: “충돌이 드물 거라는 입장 — 낙관적 락”낙관적 락은 충돌이 거의 일어나지 않을 것이라 가정한다. 동시성을 먼저 확보하기 위해 작업을 자유롭게 열어둔 뒤, 커밋 직전에 “내가 처음 읽은 값이 그대로인가”를 사후 검증한다. 값이 바뀌지 않았다면 통과시키고, 그사이 수정되었다면 작업을 실패 처리한 뒤 재시도한다.
비용 구조는 철저한 후불제다. 충돌이 실제로 발생했을 때만 재시도 비용을 내므로, 충돌이 드물수록 청구서는 0에 수렴한다. 협업 문서 편집처럼 여러 사람이 동시에 같은 자리를 고칠 확률이 낮은 무대가 대표적이다. 발생하지도 않을 충돌을 방어하기 위해 모두에게 줄을 세우는 과잉 통제 비용을 제거한다.
비관적 락은 정합성을 먼저 보장하고 동시성 비용을 상시 지불한다. 반대로 낙관적 락은 동시성을 먼저 확보하고, 충돌이 실제로 발생했을 때만 비용을 낸다.
결국 어느 쪽이 옳은가는 단순하다. 충돌이 얼마나 자주 일어나는가.
데드락 — 조약으로 파생된 교착 상태
섹션 제목: “데드락 — 조약으로 파생된 교착 상태”조약은 정합성과 동시성 사이의 균형을 약속한다. 하지만 조약을 너무 충실히 지키면, 그 충실함 자체가 역설적으로 새로운 충돌을 만든다.
트랜잭션 A와 B를 보자. A는 자기에게 필요한 자원에 락을 걸고, 다음 자원이 풀리기를 정직하게 기다린다. B도 똑같다. 자기 자원에 락을 걸고, 다음을 기다린다. 비관적 락의 정직한 절차다. 어느 트랜잭션도 규칙을 어기지 않았다.
문제는 둘이 서로의 다음 자원 을 잡고 있다는 것이다. A는 B가 풀어주기를 기다리고, B는 A가 풀어주기를 기다린다. 둘 다 멈춘다. 어느 쪽도 잘못된 작업을 하지 않았는데, 시스템은 마비된다. 이것이 데드락이다.
┌─────────────────┐ │ 자원 1 (상호배제) │ └─────────────────┘ ▲ │ 점유 (비선점) │ │ 대기 │ ▼ ┌─────┴─────┐ ┌────┴─────┐ │ 트랜잭션 A │ │ 트랜잭션 B │ │ (점유 대기) │ │ (점유 대기) │ └─────┬─────┘ └────┬─────┘ 대기 ▲ │ 점유 (비선점) │ ▼ ┌─────────────────┐ │ 자원 2 (상호배제) │ └─────────────────┘
전체가 하나로 맞물린 닫힌 고리 ──► [순환 대기] ──► 데드락데드락은 특별한 버그가 아니다. 오히려 정합성을 지키기 위해 서로 기다리도록 만든 구조가, 어느 순간 서로를 영원히 붙잡아버리면서 생기는 역설이다.
그래서 데이터베이스는 이 상태를 그대로 두지 않는다. 백그라운드에서 누가 누구를 기다리는가 를 계속 추적하다가, 순환 구조가 발견되면 한 트랜잭션을 강제로 종료시킨다. PostgreSQL의 deadlock detector가 하는 작업이 예시이다.
흥미로운 점은 여기서 데이터베이스가 일부 조약을 포기한다는 사실이다. 전체 시스템을 멈추게 두느니, 한 트랜잭션을 희생시켜 흐름 전체를 다시 움직이게 만드는 것이다.
정합성을 지키기 위해 만든 장치가, 마지막 순간에는 시스템 전체의 생존을 위해 스스로 일부를 깨뜨린다.
합성의 오류, 모두가 옳았는데 실패하는 이유
섹션 제목: “합성의 오류, 모두가 옳았는데 실패하는 이유”지금까지 등장한 충돌들은 사실 서로 다른 문제가 아니다.
재고 꼬임, 격리 수준의 이상 현상, 데드락까지 모두 같은 패턴 위에서 발생한다. 하나의 트랜잭션 기준에서는 허용되던 행동이, 동시에 실행되는 순간 시스템 전체에서는 예상하지 못한 결과로 변하는 것이다.
1936년, 영국의 경제학자 존 메이너드 케인즈가 한 가지 인지적 함정에 이름을 붙였다. 합성의 오류(Fallacy of Composition). 그는 거시경제학의 거의 모든 정책 오류가 같은 패턴을 따른다고 봤다. 개별적으로 합리적인 행동이, 합쳐졌을 때 비합리적인 결과를 낳는다.
케인즈의 유명한 예시가 절약의 역설이다. 경기 침체 때 모든 가계가 합리적으로 저축을 늘린다. 개인 차원에서는 미래에 대비하는 옳은 판단이다. 그러나 모두가 동시에 저축을 늘리면 소비가 줄고, 기업의 매출이 줄고, 고용이 줄어, 결국 저축할 돈조차 사라진다. 각자의 합리가 시스템의 비합리로 변환되는 순간이다.
Reference: Keynes, “The General Theory of Employment, Interest and Money” (1936)
데이터베이스의 개입
섹션 제목: “데이터베이스의 개입”‘합성의 오류’는 데이터베이스가 마주하는 가장 거대한 구조적 함정이다.
데이터베이스가 두려워하는 것은 단일 트랜잭션 실패가 아니다. 여러 트랜잭션이 동시에 얽히면서 만들어내는 시스템 차원의 붕괴다.
그래서 데이터베이스는 개별 작업의 합리성보다, 전체 실행 흐름의 안정성을 우선한다.
락으로 순서를 강제하고, 격리 수준으로 충돌 범위를 제한하며, MVCC로 충돌 자체를 우회한다. 끝내 데드락이 발생하면, 멀쩡한 트랜잭션 하나를 강제로 희생시켜서라도 전체 흐름을 복구한다.
데이터베이스의 모든 통제 장치는 결국 이 하나의 모순을 막기 위해 존재한다.
모든 처리가 각자 옳았음에도, 시스템 전체가 무너지는 그 순간을 예방하기 위해서다.
정리하며
섹션 제목: “정리하며”각 트랜잭션이 옳아도, 합쳐졌을 때 시스템은 무너질 수 있다. 정합성과 동시성의 충돌은 시스템이 동시에 작동하는 이상, 반드시 관리해야 하는 구조적 비용이다.
엔지니어가 이 구조를 이해하면, 동시성 장애 앞에서 “누가 잘못 짰는가”를 묻지 않는다. 질문이 달라진다. 이 조약을 어디까지 보장할 것인가, 그리고 그 보장의 비용을 어디에서 지불할 것인가. 트랜잭션 설계의 모든 결정이 이 두 질문 위에 서 있다.
다음 편: 한 데이터베이스가 모든 트래픽을 감당할 수 없을 때, 시스템은 노드를 나누기 시작한다. 그런데 데이터를 여러 노드로 나누는 순간, 정합성과 동시성의 조약이 분산된 환경에서 다시 협상되어야 한다.