2PC에서 커밋 로그로 옮겨간 정합성
분산 정합성의 무게중심은 동기 조정 프로토콜에서 커밋 로그로 옮겨갔습니다. 소스 로그만 읽는 복구에는 정보 한계가 있어, 무엇을 로그에 남길지가 설계의 핵심이 됩니다.
정합성을 지키는 자리가 2PC의 커밋 시점에서 커밋 로그로 옮겨갔습니다. 그래서 설계 질문도 어떻게 조정할지가 아니라 무엇을 로그에 남겨야 복구가 되는지로 바뀝니다.
두 저장소의 상태를 맞추는 일은 오랫동안 조정 프로토콜의 몫이었습니다. 2단계 커밋(Two-Phase Commit, 2PC)은 모든 참여자가 준비를 마친 뒤에야 커밋을 확정해 원자성을 지킵니다. 지금 실무에서 쓰는 기법들은 그 자리를 대체로 비워 두고, 각자의 커밋 로그에 흔적을 남긴 뒤 나중에 맞춥니다.
이 이동은 서로 다른 갈래에서 같은 모양으로 나타납니다. 트랜잭셔널 아웃박스와 변경 데이터 캡처(Change Data Capture, CDC)는 이벤트 발행을 로그로 옮겼습니다. 에포크 기반 낙관적 동시성 제어는 조정을 배치 경계로 미뤘습니다. 스냅샷 테이블은 과거 시점의 사실을 조회로 재구성하지 않고 적재된 행으로 고정합니다.
대가가 하나 붙습니다. 조정을 걷어낸 만큼 복구가 로그에 의존하는데, 소스 쪽 로그만 읽는 복구에는 수학적으로 증명된 한계가 있습니다. 그래서 이 글의 결론은 로그로 옮기라는 권유가 아니라, 옮긴 뒤에 무엇을 로그에 적어야 복구가 닫히는지에 대한 기준입니다.
조정이 빠져나간 자리
2PC가 실무에서 밀려난 이유는 성능보다 장애 모드입니다. 코디네이터가 준비 단계와 커밋 단계 사이에서 죽으면 참여자들은 락을 쥔 채 결정을 기다립니다. 이 구간에는 자체 타임아웃이 없어서, 코디네이터가 돌아올 때까지 해당 자원이 묶입니다.
그래서 서비스 경계를 넘는 트랜잭션은 Saga와 아웃박스 조합으로 대체됐습니다. 각 단계를 독립 트랜잭션으로 쪼개고 실패는 보상으로 되돌리는 방식입니다. 이 선택지의 비교는 결제 시스템의 멱등성과 트랜잭션 무결성에 정리해 두었습니다. 여기서 볼 것은 그 다음입니다. 조정이 빠져나간 자리를 무엇이 채웠는가.
| 기법 | 정합성이 확정되는 시점 | 로그가 떠맡은 일 | 남아 있는 조정 |
|---|---|---|---|
| 2PC | 커밋 경로 안 | 없음(로그는 참여자 복구용) | 준비와 커밋 왕복 |
| 트랜잭셔널 아웃박스 | 로컬 커밋 시점 | 발행 대상의 확정 | 릴레이와 싱크 사이 |
| CDC | 로컬 커밋 직후 | 변경 스트림 자체 | 커서와 싱크 사이 |
| 에포크 기반 OCC | 에포크 경계 | 배치 단위 write-set | 에포크당 한 번 |
| 스냅샷 테이블 | 이벤트 또는 마감 시각 | 그 시점 값의 보존 | 없음(로컬 트랜잭션) |
표의 네 행이 공유하는 성질은 하나입니다. 정합성을 확정하는 지점이 요청 경로 밖으로 나갔고, 그 결과를 로그가 증언합니다. 다만 마지막 열을 보면 조정이 사라진 것이 아니라 위치를 옮겼을 뿐이라는 점도 드러납니다.
로그가 대신 떠맡은 네 가지 일
아웃박스는 발행을 로그로 옮깁니다. 이벤트를 브로커에 직접 보내는 대신 같은 데이터베이스의 이벤트 테이블에 먼저 적고, 별도 릴레이가 그 테이블을 읽어 발행하는 패턴입니다. 비즈니스 행 변경과 이벤트 행 삽입을 한 로컬 트랜잭션으로 커밋하면, 데이터베이스 커밋이 곧 이벤트 확정이 됩니다. 아웃박스의 기본 동작은 앞의 글에서 다뤘으므로 반복하지 않습니다.
CDC는 한 걸음 더 나아가 애플리케이션이 이벤트 행을 쓰는 일조차 없앱니다. Debezium 같은 도구가 MySQL binlog나 PostgreSQL WAL을 직접 읽어 변경 스트림을 만듭니다. 우아한형제들은 주문 도메인 데이터와 이벤트를 같은 트랜잭션으로 커밋한 뒤 CDC로 카프카에 흘립니다. 커넥터 태스크가 하나만 도는 제약 때문에 아웃박스 테이블을 토픽별로 쪼개 처리량을 확보했습니다(우아한형제들 기술블로그, 2026-09 확인).
에포크 기반 OCC는 조정 자체를 배치 경계로 미룹니다. 리전마다 로컬에서 먼저 커밋하고 write-set을 에포크 단위로 교환한 뒤, 뒤늦게 드러난 충돌만 결정론적으로 재실행합니다. 커밋 경로에서 광역 왕복을 걷어낸 설계입니다. 재실행 비용이 충돌률에 따라 어떻게 이동하는지는 에포크 기반 낙관적 동시성 제어에 정리했습니다.
네 번째는 백필입니다. 2020년에 공개된 DBLog는 라이브 데이터베이스를 멈추지 않고 통째로 복사하기 위해, 기본키 순서로 청크를 읽고 각 청크를 소스 로그의 워터마크로 감쌉니다. 2026년 5월의 형식 검증 연구 arXiv:2605.31475는 이 재생이 만들어 내는 상태를 가상 컷(virtual cut)으로 정형화했습니다. 물리 스냅샷을 한 번도 뜨지 않고도 선택한 프론티어에서 키별 상태가 소스와 같아진다는 결과이며, 논문은 이것이 정확히 한 번 전달이나 목적지 수렴을 보장하지는 않는다고 명시합니다.
소스 로그만으로 닫히지 않는 복구
여기서 한계가 드러납니다. 아웃박스와 CDC는 애플리케이션이 하던 이중 쓰기를 릴레이 프로세스로 옮길 뿐, 싱크에 전달하는 행위와 소스에 체크포인트를 적는 행위는 여전히 별개의 지속성 연산입니다. 원격 싱크가 요청을 수락한 직후, 체크포인트를 적기 전에 프로세스가 죽으면 재시작된 복구는 로컬 상태만으로 이전 전송의 성공 여부를 알 수 없습니다.
2026년 8월 공개된 Isabelle/HOL 형식 검증 연구 arXiv:2608.00501이 이 직관에 증명을 붙였습니다. 논문의 주 결과는 정보 한계(information bound)입니다. 소스 쪽 지속 상태는 완전히 같은데 싱크의 승인 기록만 다른 두 개의 도달 가능한 크래시 후 상태를 구성할 수 있습니다. 소스 쪽만 관측하는 복구 정책은 두 상태를 구별하지 못하므로 같은 배치를 고를 수밖에 없고, 그 배치는 한쪽에서 중복을 만들거나 다른 쪽에서 유실을 남깁니다.
논문은 전달 후 체크포인트 순서를 지키고 크래시 시점만 비결정적인 프로토콜에서도 같은 결론이 성립한다고 밝힙니다. 로컬 스키마를 정교하게 짜거나 격리 수준을 올려도 이 벽은 넘지 못한다는 뜻입니다. 감사 도구가 소스 데이터베이스의 정합성을 아무리 잘 검증해도, 원격 수신자가 무엇을 받았는지는 소스에 적혀 있지 않기 때문입니다.
복구에 필요한 로그 내용
같은 논문이 한계를 넘는 조건도 함께 제시합니다. 싱크의 승인 기록이 권위 있고 완전하며 최신이면, 그리고 소스 좌표가 각 연산을 구별할 수 있으면, 복구는 빠진 연산을 계산할 수 있습니다. 소스 로그가 요구하는 의무 집합에서 싱크가 이미 수락한 것을 빼는 차집합 연산입니다.
이 조건은 로그 설계에 대한 세 가지 요구로 번역됩니다. 첫째, 이벤트마다 엄격히 단조 증가하는 좌표를 붙여야 합니다. LSN이나 UUIDv7처럼 정렬 가능한 유일 식별자여야 복구 배치 안에서 같은 것이 두 번 들어가지 않습니다. 둘째, 싱크가 자신의 승인 기록을 조회 가능한 형태로 노출해야 합니다. 카프카의 오프셋이나 컨슈머 쪽 인박스 테이블이 그 역할을 합니다.
셋째는 인플라이트 메시지 문제입니다. 프로세스가 죽어도 소켓 버퍼나 재시도 큐에 남은 과거 메시지는 사라지지 않습니다. 복구가 싱크를 조회해 차집합을 보낸 직후 그 지연 메시지가 도착하면 중복 수락이 일어납니다. 논문은 이를 세대 번호 기반 펜스로 막습니다. 복구 프로세스가 배치를 반영하면서 펜스를 다음 세대로 올리고, 싱크는 자기 펜스보다 낮은 세대의 메시지를 거부합니다.
같은 발상이 동시 복구자에도 적용됩니다. 오케스트레이터가 살아 있는 워커를 죽었다고 오판해 두 번째 복구자를 띄우면, 둘 다 정상적인 차집합 정책을 따라도 이중 발송이 납니다. 복구자가 작업 전에 자기 세대를 싱크에 원자적으로 등록하고, 발송 시점에 그 권한이 유지될 때만 싱크가 쓰기를 받는 규율이 필요합니다. 펜싱 토큰이 없는 락은 지연된 쓰기를 막지 못한다는 분산 락의 오랜 교훈과 같은 구조입니다.
보장을 끊는 증거의 수명
여기까지가 성립해도 보장에는 만료가 있습니다. 논문이 마지막으로 다루는 것이 유한한 중복 제거 상태와 잘린 소스 이력이 보장의 수명을 어떻게 제한하는가입니다. 실제 시스템의 멱등키 저장소와 로그 보관 기간이 무한하지 않기 때문입니다.
Stripe API 레퍼런스는 멱등키를 최소 24시간이 지나면 자동으로 정리할 수 있다고 밝힙니다(2026-09 확인). 정리된 뒤 같은 키가 재사용되면 새 요청이 생성됩니다. 장애 복구가 이 창을 넘기면 재시도된 요청이 신규 요청으로 처리됩니다. 중복 제거는 사라지지 않고 조용히 만료됩니다.
소스 쪽에도 같은 문제가 있습니다. 커밋 로그를 자르면 잘려나간 구간이 싱크에 이미 완전히 반영됐다는 보증이 없는 한 복구의 근거가 사라집니다. 그래서 보관 정책은 저장 비용 문제가 아니라 정합성 문제입니다. 중복 제거 테이블의 보관 기간이 목표 복구 시간보다 짧으면, 그 차이만큼이 보장되지 않는 구간입니다.
로그로 옮겨지지 않는 것
이 축으로 모든 정합성 문제가 재배치되지는 않습니다. 세 가지 반례가 있습니다.
첫째, 단일 데이터베이스 안의 이상 현상은 로그의 문제가 아닙니다. 쓰기 왜곡(write skew)은 두 트랜잭션이 각자 다른 행을 읽고 다른 행을 쓰면서 조합이 불변식을 깨는 현상이라, 스냅샷 격리가 통과시킵니다. 해법은 로그 설계가 아니라 직렬화 가능 격리 수준이나 명시적 락입니다. 격리 수준과 MVCC의 지형은 데이터베이스 트랜잭션과 MVCC에 있습니다. 애플리케이션 경계 안의 트랜잭션 전파는 Spring 트랜잭션과 전파가 다룹니다.
둘째, 싱크가 승인 기록을 내주지 않으면 정보 한계는 그대로 남습니다. 외부 결제 게이트웨이는 멱등키를 받아 중복 승인은 막아 주지만, 우리 쪽 복구가 조회할 수 있는 완전한 수락 장부를 노출하지는 않습니다. 이때 남는 수단은 사후 수렴입니다. 내부 장부와 외부 명세서를 주기적으로 대조하는 대사 배치가 그 역할을 하며, 이것은 복구를 닫는 것이 아니라 불일치를 늦게라도 발견하는 장치입니다.
셋째, 수렴을 로그 없이 데이터 타입으로 푸는 갈래가 따로 있습니다. 충돌 없는 복제 데이터 타입(CRDT)은 복제본이 조정 없이 각자 수정해도 같은 변경 집합을 받으면 같은 상태에 도달하도록 대수 구조를 설계합니다. 여기서는 복구 배치를 계산할 필요 자체가 없어지며, 대가는 표현할 수 있는 연산이 제한된다는 것입니다. 자세한 구조는 CRDT에 정리했습니다.
한 가지 더 덧붙이면, 2PC가 전부 틀린 것도 아닙니다. 참여자가 모두 한 조직의 통제 아래 있고 트랜잭션이 짧으며 코디네이터를 고가용으로 둘 수 있으면 조건이 달라집니다. 그때는 2PC의 단순함이 아웃박스와 펜스와 대사를 모두 운영하는 비용보다 쌉니다. 이 판단은 논문이 아니라 제 판단입니다.
설계 결정 기준
로그로 옮길지 결정하기 전에 물어야 할 것은 하나입니다. 이 싱크에서 복구가 닫히는가.
| 싱크의 성질 | 로그에 남길 것 | 복구가 닫히는가 |
|---|---|---|
| 승인 기록을 조회 가능(카프카 오프셋, 내부 인박스 테이블) | 단조 좌표와 세대 번호 | 닫힘. 차집합으로 배치를 산출 |
| 멱등키는 받지만 장부는 비공개(외부 결제 게이트웨이) | 좌표에서 결정론적으로 파생한 멱등키 | 부분. 중복은 막고 유실은 대사로 |
| 아무 신원도 받지 않음(메일·SMS 발송) | 발송 시도 기록 | 안 닫힘. 중복과 유실 중 하나를 고름 |
| 조정을 배치로 미룸(에포크 기반 OCC) | write-set과 에포크 id | 닫힘. 재실행 비용은 충돌률을 따라감 |
| 시점 사실만 필요(주문·정산 스냅샷) | 그 시점 값 자체 | 닫힘. grain 유니크 제약 위의 멱등 upsert |
세 번째 행이 가장 중요합니다. 승인 기록이 없는 싱크에서는 어떤 정교한 설계도 중복과 유실 중 하나를 고르는 문제로 귀결되므로, 어느 쪽 비용이 큰지를 먼저 정하고 그 선택을 문서에 남겨야 합니다. 나머지 행에서는 남길 것을 남기면 복구가 계산 가능해집니다.
운영 지표도 이 구분을 따라갑니다. 중복과 유실은 원인도 대응도 다르므로 하나의 정합성 지표로 뭉뚱그리면 안 됩니다. 소스 로그의 프론티어와 싱크 오프셋 사이의 지연을 계속 추적하는 것이 유실 감지의 기본선이고, 중복은 싱크 쪽 중복 제거 테이블의 충돌 횟수로 드러납니다.
정리
분산 정합성을 지키는 자리는 커밋 경로 안의 동기 조정에서 커밋 로그로 옮겨갔습니다. 아웃박스와 CDC는 발행을, 에포크 기반 OCC는 조정을, 스냅샷 테이블은 시점 보존을 각각 로그와 적재된 행에 맡깁니다. 다만 소스 로그만 읽는 복구는 중복이나 유실 중 하나를 피할 수 없다는 것이 형식 검증으로 증명되어 있고, 이 벽은 싱크의 승인 기록을 조회할 때만 무너집니다. 그래서 설계에서 먼저 확인할 것은 로그의 스키마가 아니라 싱크가 자기 수락 기록을 내주는지, 그리고 그 기록과 멱등키의 보관 기간이 목표 복구 시간보다 긴지입니다. 이 둘이 확보되지 않는 싱크에서는 중복과 유실 중 어느 쪽을 감수할지 먼저 정하는 것이 순서입니다.