
MSA의 악몽, 분산 트랜잭션 (Saga 패턴으로 해결하기)
서비스를 MSA로 쪼갰더니 트랜잭션 관리가 지옥이 되었습니다. 주문은 성공했는데 결제는 실패하고, 재고는 이미 차감되었다면? 모놀리식의 ACID가 그리워지는 순간, 분산 환경에서 데이터 일관성을 지키는 Two-Phase Commit(2PC), Saga 패턴(Choreography, Orchestration)을 구체적인 예제와 함께 다뤄봤습니다.

서비스를 MSA로 쪼갰더니 트랜잭션 관리가 지옥이 되었습니다. 주문은 성공했는데 결제는 실패하고, 재고는 이미 차감되었다면? 모놀리식의 ACID가 그리워지는 순간, 분산 환경에서 데이터 일관성을 지키는 Two-Phase Commit(2PC), Saga 패턴(Choreography, Orchestration)을 구체적인 예제와 함께 다뤄봤습니다.
DB 설계의 기초. 데이터를 쪼개고 쪼개서 이상 현상(Anomaly)을 방지하는 과정. 제1, 2, 3 정규형을 쉽게 설명합니다.

왜 CPU는 빠른데 컴퓨터는 느릴까? 80년 전 고안된 폰 노이만 구조의 혁명적인 아이디어와, 그것이 남긴 치명적인 병목현상에 대해 정리했습니다.

ChatGPT는 질문에 답하지만, AI Agent는 스스로 계획하고 도구를 사용해 작업을 완료한다. 이 차이가 왜 중요한지 정리했다.

결제 API 연동이 끝이 아니었다. 중복 결제 방지, 환불 처리, 멱등성까지 고려하니 결제 시스템이 왜 어려운지 뼈저리게 느꼈다.

모놀리식(Monolithic) 아키텍처 시절은 개발자에게 천국이었습니다. 적어도 데이터 일관성(Consistency) 문제에 있어서는요. 데이터베이스가 하나였으니까요.
BEGIN TRANSACTION;
UPDATE inventory ...;
INSERT INTO payments ...;
COMMIT; -- 혹은 ROLLBACK;
이 모든 게 한 방(Atomic)에 처리되었습니다. 성공하면 다 같이 성공, 실패하면 깨끗하게 없던 일이 됩니다 (ACID).
하지만 마이크로서비스 아키텍처(MSA)에서는 상황이 다릅니다. '주문 서비스', '재고 서비스', '결제 서비스'가 각각 다른 DB를 씁니다.
이러면 어떻게 되나요? 이미 결제된 돈은 어떡하죠? 주문은 취소해야 하나요? 자동으로 롤백해주는 DB 기능은 이제 없습니다. 우리가 직접 이 난장판을 수습해야 합니다. 이것이 분산 트랜잭션(Distributed Transaction) 문제입니다.
가장 고전적인 방법은 "모두가 준비될 때까지 기다리는 것"입니다. 코디네이터(Coordinator)라는 관리자가 등장해서 두 단계로 진행합니다.
Saga 패턴은 "긴 트랜잭션을 여러 개의 짧은 로컬 트랜잭션으로 쪼개는 것"입니다. 각 서비스는 자기 할 일을 하고, 다음 서비스에게 "내 차례 끝났어"라고 이벤트를 던집니다. 만약 중간에 실패하면? 보상 트랜잭션(Compensating Transaction)을 실행해서 거꾸로 되돌립니다.
보상 트랜잭션이란? (Undo)
결제 승인의 보상 트랜잭션 ->결제 취소재고 차감의 보상 트랜잭션 ->재고 복구주문 생성의 보상 트랜잭션 ->주문 상태를 '취소'로 변경
Saga 패턴에는 두 가지 구현 방식이 있습니다.
중앙 관리자 없이 서비스끼리 이벤트를 주고받습니다.
OrderCreated 이벤트 발행.OrderCreated 수신 -> 결제 시도 -> 성공 시 PaymentProcessed 발행.PaymentProcessed 수신 -> 재고 차감 -> 실패! -> InventoryFailed 발행.InventoryFailed 수신 -> 결제 취소(보상 트랜잭션).InventoryFailed 수신 -> 주문 상태 취소 변경.중앙에 오케스트레이터(Saga Manager)라는 놈이 있어서 지시를 내립니다. "결제 서비스야, 결제해." -> (성공) -> "재고 서비스야, 재고 줄여." -> (실패) -> "결제 서비스야, 아까 그거 취소해."
분산 시스템에서는 네트워크 오류가 일상입니다. 이벤트가 오다가 사라질 수도 있고, 두 번 올 수도 있습니다. 만약 결제 요청이 타임아웃 되어서 재시도를 했는데, 사실 첫 번째 요청이 성공했었다면? 두 번 결제되는 대참사가 일어납니다.
이걸 막으려면 모든 트랜잭션은 멱등성(Idempotent)을 가져야 합니다. "같은 요청을 여러 번 수행해도 결과는 처음 한 번 실행한 것과 같아야 한다." 주문 ID나 결제 ID 같은 고유 키(Unique Key)를 사용해서, "어? 이미 처리된 ID네?" 하고 무시할 수 있어야 합니다. Saga 패턴을 구현할 때 멱등성은 선택이 아니라 필수입니다.
"로컬 DB 업데이트"와 "이벤트 발행"이 원자적(Atomic)이어야 하는 문제가 있습니다. 주문은 생성했는데, 메시지 큐(Kafka)가 죽어서 이벤트를 못 보내면? 주문만 생기고 결제는 시작도 안 하는 불일치가 생깁니다.
이럴 때 쓰는 것이 아웃박스 패턴입니다.
Outbox 테이블에 저장합니다. (주문 생성 + 이벤트 저장을 하나의 로컬 트랜잭션으로 묶음).Outbox 테이블을 읽어서 메시지 큐로 쏴줍니다.Outbox에서 삭제합니다.이렇게 하면 "적어도 한 번(At-least-once)" 전송을 보장할 수 있습니다.
Saga와 비슷하지만 조금 더 엄격한 패턴입니다. REST API를 설계할 때 유용합니다.
호텔 예약 같은 경우에 적합합니다. "방 일단 잡아놔(Try)" 해놓고 결제 안 하면 "풀어(Cancel)", 결제하면 "확정해(Confirm)".
분산 시스템에서 "실시간으로 완벽하게 똑같은 데이터"를 보장하는 것은 불가능에 가깝거나, 비용이 너무 듭니다 (CAP 정리). 그래서 우리는 결과적 일관성을 받아들여야 합니다. "지금 당장은 데이터가 안 맞을 수도 있지만, 잠시 기다리면(이벤트가 다 돌면) 결국에는 맞게 된다"는 개념입니다.
주문 버튼 눌렀는데 "주문 완료" 떴다가, 1분 뒤에 "재고 부족으로 취소되었습니다" 카톡이 오는 것. 이게 바로 결과적 일관성이 적용된 현실 예시입니다. 사용자 경험(UX)으로 기술적 한계를 극복하는 것이죠. MSA를 한다면 ACID에 대한 집착을 버려야 합니다.