TCC(Try-Confirm-Cancel) 분산 트랜잭션 패턴
1. 개요
가. 정의
TCC(Try-Confirm-Cancel)는 하나의 분산 트랜잭션을 참여 서비스마다 Try(자원 예약)·Confirm(예약 확정)·Cancel(예약 취소)의 세 연산으로 분해하여, 전 서비스의 Try가 성공하면 Confirm을, 하나라도 실패하면 Cancel을 호출함으로써 애플리케이션 수준에서 2단계 커밋 의미를 구현하는 보상 기반 분산 트랜잭션 패턴이다.
TCC는 데이터베이스가 제공하는 2PC의 "준비(prepare)-커밋(commit)" 구조를 업무 로직으로 끌어올린 패턴으로 이해하면 명확하다. 2PC가 자원관리자(DB)의 락과 undo/redo 로그에 의존해 자원을 묶어두는 반면, TCC는 각 서비스가 자신의 자원을 "예약·확정·취소"하는 세 개의 멱등 API를 스스로 노출하고, 조정자가 이 세 연산을 두 단계(1단계 Try, 2단계 Confirm/Cancel)로 호출한다. 그래서 TCC를 흔히 "비즈니스 수준의 2PC(application-level 2PC)" 또는 보상형(compensation-based) 트랜잭션이라 부른다. Pat Helland가 CIDR 2007 논문 Life beyond Distributed Transactions에서 제시한 잠정 연산(tentative operation)·예약(reservation) 개념이 사상적 뿌리이며, Alibaba의 Seata TCC 모드 등을 통해 실무 패턴으로 널리 정착했다.
나. 등장 배경과 필요성
마이크로서비스 아키텍처(MSA)는 DB per Service 원칙을 따르기 때문에, 하나의 업무(예: 전자상거래 주문 = 계좌 차감 + 재고 감소 + 포인트 적립)가 서로 다른 서비스와 물리 DB에 걸쳐 수행된다. 모놀리식 시절처럼 단일 COMMIT으로 원자성을 보장할 수 없어, 분산 트랜잭션 전략이 필요해진다. 전통적 해법인 2단계 커밋(2PC/XA)은 강한 일관성을 주지만, 1단계 prepare 이후 커밋이 끝날 때까지 DB 락을 장시간 점유하여 처리량과 가용성을 떨어뜨리고, 조정자 장애 시 참여자가 자원을 쥔 채 멈추는 블로킹 문제가 있다. 이는 대규모 인터넷·금융 트래픽과 맞지 않는다.
사가(Saga)는 각 단계를 즉시 커밋되는 로컬 트랜잭션으로 처리하고 실패를 사후 보상으로 정정하여 가용성을 얻지만, 중간 단계의 변경이 즉시 외부에 노출(dirty read)되고 격리성이 없다는 약점이 있다. 예컨대 결제 서비스가 돈을 실제로 빼간 뒤 재고 단계가 실패하면, 환불(보상) 전까지 고객 계좌에는 이미 출금이 보인다.
TCC는 이 두 극단 사이의 절충이다. 핵심 아이디어는 "즉시 확정하지 말고 먼저 예약하라"이다. 1단계 Try에서는 실제 자원을 소비하는 대신 일부를 격리·동결(예: 가용 재고 100 → 가용 90·동결 10)만 해두고, 모든 참여자의 예약이 성공한 뒤에야 2단계 Confirm에서 동결분을 실제로 확정한다. 덕분에 DB 락을 길게 잡지 않으면서도(2PC 대비), 중간 상태의 노출을 자원 동결로 억제(사가 대비)하는 준(準)격리성을 확보한다. 즉 TCC는 "자원을 물리적으로 잠그는 대신, 업무 데이터에 예약 상태를 두어 논리적으로 격리"하는 전략이다.
2. 전체 구조와 동작 원리
TCC 시스템은 전역 트랜잭션을 지휘하는 트랜잭션 조정자(TM/TC)와, 각자 try/confirm/cancel 세 인터페이스를 구현한 TCC 참여자(Resource)로 구성된다. 조정자는 1단계에서 모든 참여자의 Try를 호출하고, 그 결과에 따라 2단계에서 전원 Confirm 또는 전원 Cancel을 호출한다.
flowchart TB
subgraph APP["애플리케이션(전역 트랜잭션 시작)"]
TM["트랜잭션 조정자(TM/TC)"]
end
subgraph P1["계좌 서비스"]
A1["try: 출금액 동결"]
A2["confirm: 동결→실제 출금"]
A3["cancel: 동결 해제"]
end
subgraph P2["재고 서비스"]
B1["try: 재고 예약(가용↓·동결↑)"]
B2["confirm: 예약→실제 차감"]
B3["cancel: 예약 해제"]
end
subgraph P3["포인트 서비스"]
C1["try: 적립 예약"]
C2["confirm: 적립 확정"]
C3["cancel: 예약 취소"]
end
TM -- "1단계 Try" --> A1 & B1 & C1
TM -- "2단계 Confirm(모두 성공 시)" --> A2 & B2 & C2
TM -- "2단계 Cancel(하나라도 실패 시)" --> A3 & B3 & C3
위 구조의 요체는 각 자원이 세 가지 상태 전이를 스스로 책임진다는 점이다. 조정자는 "무엇을 예약·확정·취소할지"의 업무 의미를 모른 채 두 단계의 호출 순서만 보장하고, 실제 데이터 조작은 각 서비스가 담당한다. 이 관심사 분리 덕분에 조정자는 범용(프레임워크)으로, 자원 로직은 도메인별로 독립 진화할 수 있다.
정상 흐름과 실패 흐름을 시간 축으로 보면 다음과 같다. 1단계에서 하나라도 Try가 실패하면 조정자는 즉시 2단계를 Cancel로 전환하며, 전원 성공 시에만 Confirm을 진행한다.
sequenceDiagram
participant TM as 조정자(TC)
participant Acc as 계좌
participant Inv as 재고
participant Pt as 포인트
Note over TM,Pt: 1단계 — Try(예약)
TM->>Acc: try(출금 1만원 동결)
Acc-->>TM: OK
TM->>Inv: try(상품 1개 예약)
Inv-->>TM: OK
TM->>Pt: try(100p 적립 예약)
Pt-->>TM: FAIL(한도 초과)
Note over TM,Pt: 2단계 — 하나라도 실패 → 전원 Cancel
TM->>Acc: cancel(동결 해제)
TM->>Inv: cancel(예약 해제)
TM->>Pt: cancel(공회전 롤백)
Note right of TM: 전원 Try 성공이었다면<br/>대신 confirm 일괄 호출
가. 1단계 — Try(자원 예약·검증)
Try는 업무 가능성 검사 + 자원 격리를 수행하는 단계다. 단순히 "할 수 있는가"를 확인하는 데 그치지 않고, 뒤이을 Confirm이 반드시 성공할 수 있도록 필요한 자원을 미리 동결(freeze)해 둔다. 예를 들어 계좌 서비스의 Try는 잔액에서 1만 원을 즉시 빼는 대신, 가용잔액 -= 1만, 동결잔액 += 1만으로 상태만 옮긴다. 이렇게 하면 동시에 들어온 다른 트랜잭션이 같은 1만 원을 중복 사용하지 못하므로 격리성이 확보되고, 동시에 실제 출금은 아직 일어나지 않았으므로 되돌리기(Cancel)가 값싸다. 따라서 Try 설계의 품질이 TCC 전체의 정합성을 좌우하며, "Confirm이 실패할 수 없을 만큼 Try에서 충분히 검증·예약"하는 것이 핵심 원칙이다.
나. 2단계 — Confirm(예약 확정)
Confirm은 Try에서 동결해 둔 자원을 실제로 소비하는 단계다. 계좌 서비스라면 동결잔액 -= 1만으로 출금을 확정하고, 재고 서비스라면 예약분을 실재고에서 차감한다. Confirm의 중요한 설계 전제는 "절대 실패하지 않아야 한다"는 것이다. 이미 Try에서 모든 검증과 예약이 끝났으므로, Confirm은 추가 검사를 하지 않고 예약된 자원을 확정만 한다. 만약 네트워크 오류 등으로 Confirm이 일시 실패하면 조정자는 성공할 때까지 재시도(at-least-once)하므로, Confirm은 멱등(idempotent)이어야 한다. 이 "Confirm은 재시도로 반드시 성공시킨다"는 전제가 사가의 보상과 다른 TCC만의 성격이다.
다. 2단계 — Cancel(예약 취소)
Cancel은 1단계에서 하나라도 Try가 실패했을 때, 이미 성공한 참여자들의 동결 자원을 원상 복구하는 단계다. 계좌의 Cancel은 동결잔액 -= 1만, 가용잔액 += 1만으로 되돌린다. Try에서 실제 소비가 아니라 동결만 했으므로 Cancel은 사가의 의미적 보상(환불처럼 새 정방향 거래)보다 단순하고 안전하다. Cancel 역시 재시도될 수 있으므로 멱등해야 한다.
3. TCC의 3대 정합성 과제(Seata 기준)
TCC를 실제로 구현할 때 반드시 해결해야 하는 세 가지 비정상 상황이 있으며, Apache Seata의 TCC 설계 원칙 문서가 이를 표준적으로 정리한다. 이 세 과제를 다루지 못하면 동결 자원이 영구히 묶이거나 데이터가 깨진다.
| 과제 | 발생 상황 | 표준 처리 방법 |
|---|---|---|
| 멱등성(Idempotence) | 네트워크 재시도로 Try/Confirm/Cancel이 2회 이상 호출됨 | 트랜잭션 상태를 로그 테이블에 기록, 이미 처리된 요청은 건너뜀(한 번과 여러 번 실행 결과가 동일) |
| 빈 롤백(Empty Rollback) | Try가 도달하지 못했는데 Cancel이 먼저 호출됨 | Try 실행 여부를 기록, Try 기록이 없으면 Cancel은 아무 일도 하지 않고 성공 반환 |
| 공중 매달림(Hanging) | 지연된 Try가 Cancel보다 늦게 도착해 동결이 영구 잔존 | Cancel이 로그를 남기고, Try는 해당 Cancel 기록이 있으면 실행을 거부 |
멱등성이 필요한 이유는 분산 환경의 통신이 본질적으로 at-least-once이기 때문이다. 조정자는 응답을 못 받으면 성공할 때까지 재호출하는데, 실제로는 이전 호출이 성공했는데 응답만 유실된 경우가 흔하다. 이때 Confirm이 멱등하지 않으면 출금이 두 번 확정되는 이중 차감이 발생한다. 실무에서는 트랜잭션 ID별 상태 필드를 두고 "상태가 이미 CONFIRMED면 재확정하지 않는" 식으로, 또는 UPDATE 영향 행 수를 검사해 0건이면 건너뛰는 식으로 구현한다.
빈 롤백은 조정자가 Try 호출의 결과를 받지 못해(Try가 네트워크 문제로 참여자에 아예 도달하지 못함) 트랜잭션을 롤백 결정하고 Cancel을 호출할 때 생긴다. 참여자 입장에서는 예약한 적 없는 자원을 취소하라는 요청이므로, Try 기록이 없으면 Cancel을 공(空)처리하고 성공을 반환해야 한다. 그렇지 않고 무조건 자원을 복구하면 존재하지 않는 동결을 풀어 잔액이 늘어나는 오류가 난다.
공중 매달림(현수, suspension)은 빈 롤백의 후속 위험이다. Try가 네트워크 지연으로 Cancel 처리 이후에 뒤늦게 도착하면, 이 Try가 자원을 동결하지만 이미 전역 트랜잭션은 끝났으므로 그 동결을 풀어 줄 Confirm·Cancel이 다시는 오지 않는다. 결과적으로 동결 자원이 영구히 잠긴다. 해결책은 Cancel이 "이 트랜잭션은 취소됨"을 로그에 남기고, 뒤늦은 Try는 그 기록을 확인해 스스로 실행을 거부하도록 하는 것이다. 이 세 과제는 서로 얽혀 있으므로, 보통 하나의 트랜잭션 상태 로그 테이블로 일괄 관리한다.
4. 2PC·Saga와의 비교
세 패턴은 모두 분산 트랜잭션을 다루지만, 일관성·격리성·자원 점유 방식에서 근본적으로 다르다. 단순 항목 나열이 아니라 "왜 그 차이가 생기는가"를 보면 선택 기준이 분명해진다.
| 구분 | 2PC(XA) | TCC | 사가(Saga) |
|---|---|---|---|
| 일관성 | 강한 일관성 | 준강한(사실상 짧은 창의 결과적) | 결과적 일관성 |
| 격리성 | DB 락으로 보장 | 자원 동결로 준격리 | 없음(중간 노출) |
| 롤백 방식 | DB undo 로그 | Cancel(예약 해제) | 의미적 보상 거래 |
| 자원 점유 | 길다(커밋까지 락) | 짧다(동결만) | 거의 없음 |
| 침습성 | 낮음(DB가 처리) | 높음(3 API 구현) | 중간(보상 로직) |
| 적합 영역 | 소수·동질 DB | 금융·결제 등 강일관 요구 | 장기·느슨 결합 흐름 |
2PC는 DB 자원관리자가 락과 로그로 격리·복구를 대신 처리하므로 애플리케이션 침습성이 낮지만, 그 대가로 락을 길게 점유해 확장성이 나쁘다. TCC는 격리·복구 책임을 애플리케이션으로 이전해 락 점유를 동결 수준으로 줄였으나, 모든 참여 서비스가 세 개의 API를 직접 구현해야 하므로 개발 비용과 침습성이 높다. 사가는 격리성을 아예 포기하는 대신 구현이 가장 느슨하고 장기 실행 흐름에 적합하지만, 중간 상태가 노출되어 격리성이 중요한 금융 거래에는 부적합하다. 따라서 "강한 일관성과 짧은 자원 점유를 동시에" 요구하는 결제·계좌 이체·재고 차감 같은 영역이 TCC의 전형적 적용지이며, 느슨한 워크플로(여행 예약 전체 흐름 등)에는 사가가 더 어울린다.
5. 심화 — 실무 적용과 프레임워크 동향
TCC는 중국 대형 인터넷 기업의 고(高)트래픽 금융 트랜잭션에서 특히 널리 쓰인다. 대표적으로 Alibaba/Ant 계열의 결제·계좌 시스템은 초당 수만 건의 이체를 처리하면서 2PC의 락 점유를 감당할 수 없어, 자원 동결 기반 TCC를 핵심 패턴으로 채택했다. 이 경험이 오픈소스화된 것이 Apache Seata의 TCC 모드로, 개발자는 @TwoPhaseBusinessAction으로 Try 메서드를 선언하고 commit/rollback 메서드명을 지정하면, Seata의 트랜잭션 코디네이터(TC)가 전역 트랜잭션 로그를 관리하며 2단계 호출·재시도·멱등/빈롤백/현수 방어를 지원한다. 그 밖에 Hmily, ByteTCC, TCC-Transaction 등 Java 진영의 TCC 프레임워크가 유사한 모델을 제공한다.
구체적 사례로 은행 계좌 이체를 보면, 출금 계좌의 Try는 이체액을 동결하고 입금 계좌의 Try는 "입금 예정" 레코드를 생성한다. 두 Try가 모두 성공하면 Confirm에서 출금 계좌는 동결분을 실제 차감하고 입금 계좌는 예정액을 실잔액에 반영한다. 한쪽이라도 실패하면 Cancel로 동결·예정 레코드를 되돌린다. 이 과정에서 고객이 보는 가용잔액은 Try 즉시 줄어들지만, 실제 회계상 출금은 Confirm에서 확정되므로 이중 사용이 원천 차단된다. 또 다른 사례인 항공권 예매에서는 좌석 Try가 특정 좌석을 "HOLD" 상태로 수 분간 예약하고, 결제 Try가 성공한 뒤 Confirm에서 "CONFIRMED"로 확정하는 식으로, 흔히 보는 "N분 내 결제" 좌석 홀드 UX가 바로 TCC의 자원 동결 모델과 맞닿아 있다.
최신 동향으로는, 순수 TCC의 높은 구현 비용(모든 서비스가 3배의 API를 작성)을 줄이기 위해 Seata가 TCC와 함께 AT(Automatic Transaction) 모드(DB 프록시가 undo 로그를 자동 생성해 개발자 작업을 최소화)를 제공하고, 상황에 따라 TCC/AT/Saga/XA 모드를 혼용하는 설계가 확산되고 있다. 또한 메시지 기반의 신뢰성 있는 이벤트(Transactional Outbox)와 TCC를 조합해 강일관이 꼭 필요한 핵심 단계만 TCC로, 나머지는 이벤트로 느슨하게 처리하는 하이브리드 트랜잭션 설계가 실무의 주류가 되고 있다.
6. 고려사항 및 시사점(기술사 관점)
TCC 도입은 단순한 라이브러리 선택이 아니라 데이터 모델·운영·조직 전반의 트레이드오프를 수반하므로, 다음을 전략적으로 판단해야 한다.
적용 범위의 선별(적용 전략): 모든 트랜잭션을 TCC로 감싸면 개발·운영 비용이 폭증한다. 강한 격리와 짧은 점유가 동시에 필요한 핵심 업무(결제·이체·재고)에만 선택적으로 적용하고, 느슨한 흐름은 사가·이벤트로 분리하는 혼합 전략이 현실적이다. "일관성 요구 수준에 따라 패턴을 차등 적용"하는 설계 역량이 관건이다.
구현 침습성과 설계 난도(트레이드오프): TCC는 참여 서비스마다 Try·Confirm·Cancel 세 연산과 멱등성·빈 롤백·현수 방어를 모두 구현해야 하므로, 서비스당 코드량과 테스트 부담이 크게 는다. 특히 Confirm을 "절대 실패하지 않도록" 만들려면 Try에서의 충분한 예약이 전제되어, 도메인 데이터 모델에 동결·예약 상태 필드를 추가하는 침습적 설계가 불가피하다. 레거시·외부 서비스처럼 3개 API를 강제할 수 없는 경우 적용이 제한된다.
격리성 부작용과 관측성(운영): TCC의 준격리는 "잠깐의 자원 동결" 창을 전제하므로, Try 후 Confirm까지의 시간이 길어지면 동결 자원이 쌓여 가용량 고갈·교착이 올 수 있다. 조정자 장애나 참여자 무응답으로 미완결 트랜잭션이 남으면 동결이 해소되지 않으므로, 전역 트랜잭션 로그 모니터링, 타임아웃 기반 자동 Cancel, 미결 트랜잭션 수동 개입 콘솔 등 운영 관측성이 필수다.
데이터 정합성 검증과 보완(전망·연계): at-least-once 통신 위에서 멱등 키·상태 로그로 정확성을 지켜야 하므로, 정기적 대사(reconciliation) 배치로 동결-확정 상태를 교차 검증하는 안전망을 둬야 한다. 연계 기술로는 Saga·Transactional Outbox·CDC가 자연스럽게 결합되며, 향후 트랜잭션 메시 프레임워크(Seata 등)의 멀티모드 발전과 서버리스/이벤트 아키텍처 확산 속에서 TCC는 "강일관이 필요한 지점의 정밀 도구"로 자리매김할 전망이다.
참고자료
- Apache Seata, "TCC Mode Design Principle", https://seata.apache.org/blog/tcc-mode-design-principle
- Apache Seata, "TCC Mode" User Docs, https://seata.apache.org/docs/user/mode/tcc/
- Pat Helland, "Life beyond Distributed Transactions: an Apostate's Opinion", CIDR 2007, https://ics.uci.edu/~cs223/papers/cidr07p15.pdf
한 줄 요약: TCC는 분산 트랜잭션을 참여 서비스별 Try(예약)·Confirm(확정)·Cancel(취소) 세 연산으로 분해해, 2PC의 긴 락 점유 없이 사가보다 강한 준격리성을 얻는 애플리케이션 수준 2단계 커밋 패턴이며, 멱등성·빈 롤백·현수 방어가 구현의 성패를 좌우한다.