MSA 트랜잭션 일관성, Saga부터 Outbox까지 완벽 가이드 (2PC 대체 패턴)
마이크로서비스 아키텍처(MSA)는 서비스의 독립성과 확장성을 극대화하는 혁신적인 방법론입니다. 하지만 이 독립성이 가져오는 가장 큰 기술적 난제 중 하나가 바로 '트랜잭션 일관성' 문제입니다. 단일 데이터베이스 내에서는 ACID 원칙을 통해 트랜잭션의 원자성(Atomicity)을 보장받지만, 여러 서비스와 데이터베이스가 얽힌 분산 환경에서는 이 보장이 깨지기 쉽습니다.
만약 주문 서비스가 결제 서비스와 통신하여 주문을 완료하는 과정에서, 결제 서비스만 실패하고 주문 서비스는 이미 성공했다면? 이처럼 '부분적으로 성공하고 부분적으로 실패하는' 상태를 어떻게 일관되게 복구할 것인가가 이 가이드의 핵심 목표입니다.
분산 트랜잭션의 함정: 2PC가 MSA에서 실패하는 이유
과거 분산 트랜잭션의 표준 해결책으로 논의되던 것이 바로 **2PC(Two-Phase Commit)**입니다. 2PC는 트랜잭션 관리자(Coordinator)가 모든 참여 서비스(Participant)에게 '준비(Prepare)' 단계를 거쳐 모두가 커밋할 준비가 되었는지 확인한 후, '커밋(Commit)'을 요청하는 방식입니다.
2PC의 작동 원리:
- Prepare Phase: 코디네이터가 모든 참여자에게 트랜잭션 실행 준비를 요청합니다. 참여자는 성공적으로 준비되면 '준비 완료' 응답을 보냅니다.
- Commit Phase: 모든 참여자가 준비되었다면, 코디네이터가 최종 커밋을 명령하고, 모두가 커밋을 수행합니다.
MSA 환경에서 2PC가 치명적인 한계를 갖는 이유:
- 가용성 저하 (Availability): 2PC는 모든 참여자가 동기적으로 응답해야만 트랜잭션이 완료됩니다. 단 하나의 서비스라도 응답이 느리거나 다운되면, 전체 트랜잭션이 멈추는 '블로킹(Blocking)' 현상이 발생하여 시스템 전체의 가용성이 심각하게 저하됩니다.
- 결합도 증가 (Coupling): 모든 서비스가 트랜잭션의 시작과 끝을 하나의 코디네이터에 의존하게 되므로, 서비스 간의 결합도가 높아져 MSA의 핵심 목표(독립성)에 역행합니다.
- 비동기화의 어려움: 현대의 MSA는 이벤트 기반 아키텍처(EDA)를 지향하며 비동기 통신이 필수적인데, 2PC는 본질적으로 동기적이고 강한 일관성(Strong Consistency)을 요구합니다.
결론적으로, 2PC는 MSA의 유연하고 고가용성(High Availability)을 추구하는 설계 철학과 근본적으로 충돌합니다.
핵심 해결책 1: Saga 패턴 완벽 이해
2PC의 대안으로 가장 널리 사용되는 것이 Saga 패턴입니다. Saga는 트랜잭션을 단일 원자적 단위가 아닌, 일련의 로컬 트랜잭션(Local Transaction)들의 순서로 간주하고, 만약 중간에 실패하면 **보상 트랜잭션(Compensating Transaction)**을 실행하여 시스템을 일관된 상태로 되돌리는 패턴입니다.
Saga의 핵심 개념은 다음과 같습니다.
- 로컬 트랜잭션 (Local Transaction): 각 마이크로서비스가 자신의 DB 내에서 독립적으로 수행하는 트랜잭션입니다. (ACID 보장)
- 보상 트랜잭션 (Compensating Transaction): 이전 단계의 로컬 트랜잭션이 성공적으로 수행되었으나, 후속 단계에서 실패했을 때, 그 성공했던 작업을 '취소'하는 역할을 합니다. (예: 주문 생성 성공 $\rightarrow$ 결제 실패 시, '주문 취소' 로직 실행)
Saga 구현 방식 비교: 오케스트레이션 vs. 코레오그래피
Saga를 구현하는 방식은 크게 두 가지로 나뉩니다.
| 구분 | 오케스트레이션 (Orchestration) | 코레오그래피 (Choreography) |
|---|---|---|
| 제어 방식 | 중앙의 오케스트레이터(Orchestrator)가 흐름 제어 | 이벤트 발행/구독(Event/Message)을 통한 분산 제어 |
| 구현 복잡도 | 비교적 단순 (흐름 제어 로직 집중) | 복잡 (모든 서비스가 이벤트를 알아야 함) |
| 장점 | 트랜잭션 흐름 파악이 용이, 중앙 관리 가능 | 서비스 간의 결합도가 가장 낮음, 확장성이 높음 |
| 단점 | 오케스트레이터가 병목/단일 장애점(SPOF)이 될 수 있음 | 복잡도가 높아지면 전체 흐름 추적이 어려움 (스파게티 코드 위험) |
| 적합한 상황 | 비즈니스 프로세스가 명확하고 순서가 중요한 경우 (예: 결제 플로우) | 서비스 간의 상호작용이 복잡하고 느슨하게 결합되어야 하는 경우 |
실무적 조언: 초기 단계에서는 흐름 제어가 명확한 오케스트레이션을 사용해 전체 흐름을 파악하는 것이 유리하며, 시스템이 성숙하고 서비스 간의 독립성이 극대화될 때 코레오그래피로 전환하는 것이 일반적인 로드맵입니다.
핵심 해결책 2: 트랜잭션 보장 심화 패턴 비교
Saga 패턴 외에도 특정 상황에 최적화된 강력한 패턴들이 존재합니다.
1. TCC (Try-Confirm-Cancel) 패턴
TCC는 Saga와 유사하게 보상 메커니즘을 사용하지만, '예약(Reservation)' 개념에 가깝습니다.
- Try: 각 서비스가 트랜잭션에 필요한 자원을 '예약'합니다. (실제 변경은 하지 않음)
- Confirm: 모든 서비스가 자원 확보에 성공하면, 최종적으로 '확정'합니다.
- Cancel: 하나라도 실패하면, 예약된 모든 자원을 '취소'합니다.
장점: 자원 확보 단계에서 실패를 조기에 감지하여 롤백이 빠릅니다. 단점: 자원 예약 로직을 각 서비스에 구현해야 하므로, 비즈니스 로직에 상당한 오버헤드를 추가합니다.
2. Outbox Pattern: 원자적 이벤트 발행의 마법
가장 흔하게 발생하는 문제는 "DB 트랜잭션은 성공했지만, 메시지 브로커에 이벤트 발행하는 과정에서 네트워크 오류로 실패하는 경우"입니다. 이 경우, DB와 이벤트 발행이 분리되어 데이터 불일치가 발생합니다.
Outbox Pattern은 이 문제를 **원자성(Atomicity)**을 통해 해결합니다.
작동 원리:
- 서비스는 비즈니스 로직을 수행하고, 발생해야 할 이벤트 메시지를 별도의 테이블인
Outbox테이블에 같은 DB 트랜잭션 내에 기록합니다. - 트랜잭션이 성공적으로 커밋되면, 해당 이벤트는 '발송 대기' 상태가 됩니다.
- 별도의 **메시지 리스너/트랜잭션 커밋 후 처리기(Outbox Relayer)**가 주기적으로 이 테이블을 감지하여, 메시지 브로커(Kafka, RabbitMQ 등)로 메시지를 전송하고, 성공적으로 전송된 기록은 삭제하거나 상태를 '전송 완료'로 변경합니다.
핵심: 이벤트 발행 자체를 데이터베이스 트랜잭션의 일부로 묶어, '데이터 변경'과 '이벤트 발행'이 항상 동기화되도록 보장합니다.
💡 요약 및 선택 가이드
| 패턴 | 핵심 원리 | 장점 | 사용 시점 |
|---|---|---|---|
| Saga Pattern | 여러 서비스 간의 분산 트랜잭션 관리 (보상 트랜잭션) | MSA 환경에서 분산 트랜잭션 구현 가능 | 여러 서비스가 순차적으로 비즈니스 로직을 수행할 때 |
| Outbox Pattern | 이벤트 발행을 DB 트랜잭션에 포함 | 데이터 일관성(Consistency)을 강력하게 보장 | 서비스의 상태 변경과 외부 시스템 알림이 동시에 발생해야 할 때 |
| Saga + Outbox | (가장 강력한 조합) | 분산 트랜잭션과 이벤트 일관성 모두 확보 | 복잡하고 중요한 비즈니스 흐름에서 가장 높은 안정성이 필요할 때 |
이 글은 AI 에이전트가 1차 초안을 작성한 뒤, 사람 편집자가 사실관계·출처·톤과 맥락을 검토하여 발행했습니다. 오류나 부정확한 내용이 확인되면 24시간 이내에 정정합니다.
댓글
불러오는 중...