시작하기
많은 회사에서 모놀리식 개발환경보다 MSA 개발환경이 점차 기본으로 자리잡아가고 있습니다.
기존 모놀리식 서버에서 작성되어진 레거시에서 기능별, 도메인별로 MSA 환경으로 마이그레이션 하게 되면 그 서버 수는 기하급수적으로 많아지게 되죠.
MSA로 분리되는 환경이라 하더라도 초반에는 독립적인 도메인이 많아서 문제없이 운영할 수 있지만, 민첩하게 데이터가 연결되어야 하고 원자성 보장이 필요한 도메인들이 분리되기 시작하면서 점차 분산 트랜잭션 관리에 대한 필요성이 제기됩니다.
그래서 분산 트랜잭션을 처리하는 대표 방법(2PC, SAGA)을 비교하며 상황별로 어떤 방식을 선택하면 좋을지 정리했습니다.
2PC (Two Phase Commit)
분산 환경에서 여러 노드(데이터베이스, 서비스 등)에 걸친 트랜잭션을 원자성(모두 성공하거나 모두 실패하도록) 을 보장하기 위해 나온 고전적인 분산 트랜잭션 프로토콜입니다.
1단계: 준비 단계 (Prepare Phase)
- 코디네이터(Coordinator) 가 참여자(Participants, 예: 여러 DB)들에게 트랜잭션을 준비하라고 요청합니다.
- 단, 이 시점에서는 아직 커밋하지 않고, 트랜잭션 로그에 기록한 뒤 lock 을 잡은 상태로 대기합니다.
2단계: 커밋/롤백 단계 (Commit / Abort Phase)
- 코디네이터가 모든 참여자가
"YES"라고 응답했는지 확인합니다.- 모두 YES → 커밋 명령을 내려서 실제 커밋을 수행합니다.
- 하나라도 NO → 롤백 명령을 내려서 취소합니다.
🔹 예시 (주문·결제)
- 고객이 상품을 구매할 때 결제 DB와 재고 DB를 함께 갱신
- 코디네이터가 “결제 DB에 결제 승인 준비 가능?” → YES
- 코디네이터가 “재고 DB에 재고 차감 준비 가능?” → YES
- 모두 YES → 코디네이터가 “둘 다 커밋!” → 결제 레코드 생성, 재고 수량 1 감소
- 재고 DB가 NO 응답 → 코디네이터가 결제 DB에도 “롤백” → 결제 승인 취소
Sample Code
import { DataSource } from 'typeorm';
import { Payment } from './entities/payment.entity';
import { Stock } from './entities/stock.entity';
async function twoPhaseCommitExample(
paymentDataSource: DataSource,
stockDataSource: DataSource,
userId: number,
productId: number,
amount: number
) {
// 1. 각 DB의 QueryRunner 생성
const paymentRunner = paymentDataSource.createQueryRunner();
const stockRunner = stockDataSource.createQueryRunner();
await paymentRunner.connect();
await stockRunner.connect();
// 2. 트랜잭션 시작
await paymentRunner.startTransaction();
await stockRunner.startTransaction();
try {
// === Phase 1: Prepare ===
// Payment 처리
const payment = new Payment();
payment.userId = userId;
payment.amount = amount;
await paymentRunner.manager.save(payment);
// Stock 차감
const stock = await stockRunner.manager.findOneByOrFail(Stock, { productId });
if (stock.quantity < 1) {
throw new Error('재고 부족');
}
stock.quantity -= 1;
await stockRunner.manager.save(stock);
// === Phase 2: Commit ===
await paymentRunner.commitTransaction();
await stockRunner.commitTransaction();
console.log('✅ 결제 및 재고 차감 성공');
} catch (err) {
console.error('❌ 트랜잭션 실패:', err.message);
// Phase 2: Rollback
try {
await paymentRunner.rollbackTransaction();
} catch (e) {
console.error('Payment rollback 실패:', e);
}
try {
await stockRunner.rollbackTransaction();
} catch (e) {
console.error('Stock rollback 실패:', e);
}
} finally {
await paymentRunner.release();
await stockRunner.release();
}
}Two Phase Commit 패턴은 간단하게 구현할 수 있지만 단점도 존재합니다. 이러한 한계 때문에 확장성과 느슨한 결합을 중시하는 MSA에서는 SAGA가 자주 선택됩니다.
확장성 문제
- 참여자가 늘어나면 준비/커밋 메시지가 기하급수적으로 증가합니다.
- 네트워크 트래픽과 로그 기록 비용도 같이 증가하기 때문에 대규모 분산 환경에서는 확장성이 떨어집니다.
장기 Lock으로 인한 자원 고갈
- Prepare 단계에서 참여자는 해당 리소스를 Lock 해야 합니다.
- Commit/Abort 결정을 기다리는 동안 다른 트랜잭션은 진행할 수 없어 → 병렬 처리 성능이 크게 떨어집니다.
성능 저하 (Latency & Throughput 문제)
- 최소 두 번의 네트워크 Round Trip이 필요합니다.
- Prepare 요청/응답
- Commit 요청/응답
- 각 참여자가 디스크에 로그(Write-Ahead Log)를 남겨야 해서 I/O 오버헤드가 발생합니다.
- 따라서 지연(latency)이 크고 TPS(초당 처리량)가 떨어집니다.
SAGA 패턴
여러 서비스(각각 독립 DB 보유)가 참여하는 긴 트랜잭션을 작은 로컬 트랜잭션들의 연속으로 나눠서 처리합니다.
중간에 실패가 나면, 이미 완료된 작업들을 되돌리기 위해 보상 트랜잭션(Compensating Transaction) 을 실행합니다.
- 2PC처럼 강한 일관성을 강제하지 않고
- 각 서비스가 자기 DB에 커밋하고
- 실패 시 보상 작업으로 최종적 일관성(Eventual Consistency) 을 보장하는 것
SAGA 패턴을 구현하는 방법에는 2가지가 있습니다.
1. 오케스트레이션(Orchestration) 방식
- 중앙 오케스트레이터 서비스가 전체 Saga의 흐름을 제어합니다.
- 오케스트레이터가 각 서비스에 순서대로 트랜잭션을 요청하고, 실패 시 보상 트랜잭션도 호출합니다.
- 장점: 흐름이 한 곳에 모여 가시성이 좋고 디버깅이 쉽습니다.
- 단점: 오케스트레이터가 단일 장애지점(SPOF)이 될 수 있어 이중화·모니터링이 필수입니다.
참고, 대규모 오케스트레이션 코디네이터
2. 코레오그래피(Choreography) 방식
- 별도의 중앙 조정자 없이, 각 서비스가 이벤트를 발행하고 다른 서비스가 이를 구독해 처리합니다.
- 실패 시에도 이벤트를 발행해서 보상 트랜잭션을 실행합니다.
- 장점: 중앙 병목이 없고 확장성이 좋습니다.
- 단점: 서비스 간 이벤트 흐름이 얽히면 전체 플로우를 파악하기 어렵고, 중복 이벤트 처리·순서 보장이 복잡해집니다.
재고 차감 중 오류가 발생했을 때 보상트랜잭션을 발생시켜 주문과 pg 결제를 되돌려줍니다.
운영 복잡도
도메인별 오케스트레이션 서버 → 중앙 오케스트레이션 서버 → 코레오그래피 → 2PC
(왼쪽으로 갈수록 분산·확장성, 오른쪽으로 갈수록 중앙 집중·강한 일관성)
SAGA 설계 시 체크포인트
- 보상 트랜잭션 정의: 각 단계에 대한 반대 작업을 명확히 설계해야 합니다. 취소가 불가능한 작업(알림 발송 등)은 늦은 단계로 배치하거나 별도 처리합니다.
- 멱등성(Idempotency): 중복 이벤트나 재시도에 대비해 동일 요청을 여러 번 처리해도 결과가 한 번만 반영되도록 키 설계와 상태 체크를 준비합니다.
- 메시지 신뢰성: Outbox 패턴 + 메시지 브로커(예: Kafka, RabbitMQ)를 사용해 로컬 커밋과 이벤트 발행을 느슨하게 묶어 데이터 유실을 막습니다.
- 타임아웃과 재시도: 서비스 간 네트워크 장애를 고려해 재시도 정책, 최대 지연 시간, 데드레터 큐를 정의합니다.
- 모니터링: 각 스텝의 상태(성공/실패/보상 여부)와 이벤트 지연을 추적할 수 있는 대시보드와 알람을 준비합니다.
2PC vs SAGA 선택 가이드
- 강한 일관성이 반드시 필요하고 참여 노드가 적다면 2PC가 단순합니다.
- 확장성·고가용성이 중요하고 약한 일관성을 수용할 수 있다면 SAGA가 적합합니다.
- 결제·주문처럼 외부 시스템 연동이 많은 경우, 보상 트랜잭션과 멱등 처리까지 포함한 SAGA 설계가 실무에서 더 안전하게 동작합니다.