들어가며
온라인 쇼핑몰에서 상품을 주문하면 내부적으로 여러 작업이 실행됩니다.
1. 주문 생성
2. 재고 차감
3. 결제 승인
4. 배송 요청
사용자에게는 하나의 ‘주문하기’ 작업이지만, MSA에서는 각 작업을 서로 다른 서비스가 처리할 수 있습니다.
주문 서비스 → 주문 DB
재고 서비스 → 재고 DB
결제 서비스 → 결제 DB
배송 서비스 → 배송 DB
그렇다면 주문과 재고 처리는 성공했지만 결제가 실패하면 어떻게 해야 할까요?
주문 생성 성공
재고 차감 성공
결제 승인 실패
결제는 실패했지만 주문 데이터는 생성됐고 재고도 이미 감소했습니다. 이렇게 하나의 업무가 여러 시스템에 걸쳐 처리될 때 발생하는 데이터 정합성 문제가 분산 트랜잭션 문제입니다.
트랜잭션이란?
트랜잭션은 여러 작업을 하나의 논리적인 작업 단위로 묶는 것입니다.
하나의 DB에 주문과 주문 상품을 저장한다면 다음과 같이 처리할 수 있습니다.
DB::transaction(function () use ($orderData, $items) {
$orderId = DB::table('orders')->insertGetId($orderData);
foreach ($items as $item) {
DB::table('order_items')->insert([
'order_id' => $orderId,
'product_id' => $item['product_id'],
'quantity' => $item['quantity'],
]);
}
});
모든 작업이 성공하면 커밋하고 하나라도 실패하면 전체 작업을 롤백합니다.
모두 성공 → COMMIT
하나라도 실패 → ROLLBACK
같은 데이터베이스 안에서는 DB가 트랜잭션을 관리해 주기 때문에 비교적 간단합니다.
분산 트랜잭션이란?
분산 트랜잭션은 하나의 비즈니스 작업이 여러 데이터베이스나 시스템에 걸쳐 실행되는 것을 의미합니다.
MSA에서는 서비스마다 자신의 데이터베이스를 독립적으로 관리하는 경우가 많습니다.
주문 서비스 → 주문 DB 트랜잭션
재고 서비스 → 재고 DB 트랜잭션
결제 서비스 → 결제 DB 트랜잭션
각 서비스의 작업은 서로 다른 트랜잭션입니다.
주문 DB → COMMIT
재고 DB → COMMIT
결제 DB → ROLLBACK
결제 작업이 실패해도 이미 커밋된 주문과 재고 데이터는 자동으로 롤백되지 않습니다.
분산 트랜잭션 문제란 하나의 업무가 여러 독립적인 시스템에 걸쳐 처리될 때 일부 작업만 성공하여 전체 데이터의 정합성이 깨질 수 있는 문제다.
분산 트랜잭션은 MSA에서만 발생할까?
분산 트랜잭션은 MSA에서 자주 발생하지만 MSA만의 문제는 아닙니다.
모놀리식 애플리케이션도 자체 DB를 변경하면서 외부 결제 API를 호출할 수 있습니다.
모놀리식 애플리케이션
├─ 자체 주문 DB 변경
└─ 외부 결제 API 호출
자체 DB와 외부 결제 시스템은 서로 독립적으로 결과를 확정합니다.
외부 결제 승인 성공
→ 자체 주문 DB 저장 실패
내부 DB를 롤백해도 외부 결제는 자동으로 취소되지 않습니다.
따라서 분산 트랜잭션 여부를 판단할 때 중요한 것은 MSA인지 모놀리식인지가 아닙니다.
하나의 업무가 서로 독립적인 여러 시스템에 걸쳐 처리되는지가 핵심이다.
외부 API도 DB::transaction()으로 묶을 수 있을까?
다음 코드를 살펴보겠습니다.
DB::transaction(function () use ($order) {
$order->update([
'status' => 'PAYMENT_PROCESSING',
]);
$this->paymentApi->approve(
orderId: $order->id,
amount: $order->total_amount,
);
$order->update([
'status' => 'PAID',
]);
});
외부 결제 API까지 DB::transaction() 안에 작성했지만 실제 트랜잭션 범위는 자체 DB뿐입니다.
자체 DB 변경 → 트랜잭션에 포함
외부 결제 API 호출 → 트랜잭션에 포함되지 않음
결제 승인 후 DB 저장이 실패하면 다음과 같은 상태가 됩니다.
외부 결제 → 승인 완료
자체 DB → ROLLBACK
DB::transaction()이 외부 결제 시스템까지 롤백해 주지는 않습니다.
분산 트랜잭션에서 발생하는 문제
1. 일부 작업만 성공한다
가장 대표적인 문제입니다.
주문 생성 성공
재고 차감 성공
결제 실패
시스템의 상태는 다음과 같이 서로 달라집니다.
주문 상태 → 생성 완료
재고 상태 → 차감 완료
결제 상태 → 결제 실패
고객은 결제하지 않았지만 주문과 재고는 이미 처리된 상태입니다.
2. 요청 결과를 알 수 없다
외부 결제 요청 중 네트워크 타임아웃이 발생할 수 있습니다.
결제 승인 요청
→ 결제사에서 승인 성공
→ 응답이 네트워크에서 유실
→ 주문 서버는 타임아웃 발생
타임아웃이 발생했다고 해서 결제가 실패한 것은 아닙니다.
요청이 결제사에 도착하지 않았을 수도 있음
결제 처리 중 실패했을 수도 있음
결제는 성공하고 응답만 유실됐을 수도 있음
결과를 확인하지 않고 결제를 다시 요청하면 이중 결제가 발생할 수 있습니다.
3. 한 번에 롤백할 수 없다
하나의 DB에서는 실패 시 ROLLBACK을 실행할 수 있습니다.
하지만 분산 환경에서는 각 서비스가 이미 자신의 DB에 커밋했을 수 있습니다.
주문 DB COMMIT
재고 DB COMMIT
결제 DB 실패
결제 서비스가 주문 DB와 재고 DB의 트랜잭션을 직접 롤백할 수는 없습니다.
이 문제를 처리하기 위해 2PC나 Saga 같은 방법을 사용합니다.
분산 트랜잭션 해결 방법
1. 2PC
2PC는 Two-Phase Commit의 약자로, 여러 참여자를 하나의 분산 트랜잭션처럼 처리하는 방식입니다.
2PC는 두 단계로 진행됩니다.
1단계: Prepare
트랜잭션 코디네이터가 모든 참여자에게 커밋할 준비가 됐는지 확인합니다.
Coordinator
├─ 주문 DB: 준비됐는가?
├─ 재고 DB: 준비됐는가?
└─ 결제 DB: 준비됐는가?
2단계: Commit
모든 참여자가 준비됐다고 응답하면 전체 커밋을 요청합니다.
주문 DB COMMIT
재고 DB COMMIT
결제 DB COMMIT
하나라도 준비되지 않았다면 전체 롤백을 요청합니다.
2PC의 장점
- 여러 참여자를 하나의 트랜잭션처럼 처리할 수 있다.
- 모두 성공하거나 모두 실패하는 원자성을 제공한다.
- 강한 일관성을 유지할 수 있다.
2PC의 단점
- 모든 참여 시스템이 2PC를 지원해야 한다.
- 트랜잭션이 끝날 때까지 자원을 오래 점유할 수 있다.
- 트랜잭션 코디네이터 장애를 고려해야 한다.
- 서비스 사이의 결합도가 높아질 수 있다.
- 일반적인 외부 HTTP API에는 적용하기 어렵다.
이러한 이유로 MSA에서는 2PC 대신 Saga 패턴을 사용하는 경우가 많습니다.
Saga 패턴
Saga 패턴은 하나의 큰 분산 트랜잭션을 여러 개의 로컬 트랜잭션으로 나누어 실행하는 방식입니다.
주문 생성
→ 재고 예약
→ 결제 승인
→ 배송 접수
→ 주문 확정
각 서비스는 자신의 DB에서만 트랜잭션을 실행합니다.
주문 서비스 → 주문 생성 COMMIT
재고 서비스 → 재고 예약 COMMIT
결제 서비스 → 결제 승인 COMMIT
배송 서비스 → 배송 접수 COMMIT
모든 단계가 성공하면 전체 Saga가 완료됩니다.
중간에 실패하면 어떻게 할까?
재고 예약까지 성공했지만 결제가 실패했다고 가정해 보겠습니다.
주문 생성 성공
재고 예약 성공
결제 승인 실패
이미 주문과 재고 트랜잭션은 커밋됐기 때문에 일반적인 DB 롤백을 실행할 수 없습니다.
대신 이미 완료된 작업을 취소하는 새로운 작업을 실행합니다.
결제 승인 실패
→ 재고 예약 해제
→ 주문 취소
이러한 취소 작업을 보상 트랜잭션이라고 합니다.
보상 트랜잭션이란?
보상 트랜잭션은 이미 완료된 작업의 결과를 비즈니스적으로 취소하는 새로운 트랜잭션입니다.
| 주문 생성 | 주문 취소 |
| 재고 예약 | 재고 예약 해제 |
| 결제 승인 | 결제 환불 |
| 배송 접수 | 배송 취소 |
배송 접수 단계에서 실패했다면 다음과 같이 역순으로 보상할 수 있습니다.
주문 생성 성공
재고 예약 성공
결제 승인 성공
배송 접수 실패
↓
결제 환불
재고 예약 해제
주문 취소
중요한 점은 보상 트랜잭션이 DB의 ROLLBACK은 아니라는 것입니다.
DB ROLLBACK
→ 아직 커밋되지 않은 변경을 원래대로 되돌림
보상 트랜잭션
→ 이미 커밋된 작업을 취소하는 새로운 업무 실행
예를 들어 결제 기록을 삭제하는 것이 아니라 환불 요청을 실행하고 환불 기록을 남겨야 합니다.
Saga 패턴의 구현 방식
Saga는 크게 Choreography와 Orchestration 방식으로 구현할 수 있습니다.
1. Choreography 방식
중앙에서 전체 흐름을 관리하는 서비스가 없습니다. 각 서비스가 이벤트를 발행하고 다른 서비스가 이벤트를 구독하여 다음 작업을 실행합니다.
주문 서비스
└─ OrderCreated 발행
↓
재고 서비스
└─ InventoryReserved 발행
↓
결제 서비스
└─ PaymentCompleted 발행
↓
배송 서비스
└─ 배송 접수
결제가 실패하면 실패 이벤트를 발행합니다.
PaymentFailed
↓
재고 서비스 → 재고 예약 해제
주문 서비스 → 주문 취소
장점
- 중앙 제어 서비스가 필요하지 않다.
- 각 서비스의 독립성을 유지하기 쉽다.
- 단순한 이벤트 흐름에 적합하다.
단점
- 전체 실행 흐름을 파악하기 어렵다.
- 참여 서비스가 많아지면 이벤트 관계가 복잡해진다.
- 장애가 발생한 위치와 현재 상태를 추적하기 어려울 수 있다.
2. Orchestration 방식
Saga Orchestrator가 전체 실행 순서를 관리합니다.
Saga Orchestrator
├─ 재고 서비스에 재고 예약 요청
├─ 결제 서비스에 결제 승인 요청
├─ 배송 서비스에 배송 접수 요청
└─ 실패 시 보상 작업 요청
실행 흐름은 다음과 같습니다.
Orchestrator → 재고 예약 요청
재고 서비스 → 예약 성공
Orchestrator → 결제 승인 요청
결제 서비스 → 승인 성공
Orchestrator → 배송 접수 요청
배송 서비스 → 접수 실패
Orchestrator → 결제 환불 요청
Orchestrator → 재고 예약 해제 요청
Orchestrator → 주문 취소 요청
장점
- 전체 비즈니스 흐름을 한곳에서 확인할 수 있다.
- 현재 진행 단계를 추적하기 쉽다.
- 실패, 재시도, 보상 흐름을 관리하기 쉽다.
- 복잡한 비즈니스 프로세스에 적합하다.
단점
- Orchestrator의 책임이 커질 수 있다.
- Orchestrator와 참여 서비스의 결합도가 높아질 수 있다.
- Orchestrator의 장애 복구도 고려해야 한다.
| 흐름 제어 | 각 서비스 | Orchestrator |
| 통신 방식 | 이벤트 중심 | 명령과 결과 중심 |
| 전체 흐름 확인 | 어려울 수 있음 | 비교적 쉬움 |
| 적합한 상황 | 단순한 흐름 | 복잡한 흐름과 분기 |
Saga 구현 시 필요한 처리
Saga를 사용한다고 해서 분산 트랜잭션 문제가 자동으로 해결되는 것은 아닙니다.
진행 상태 저장
현재 Saga가 어디까지 실행됐는지 저장해야 합니다.
STARTED
INVENTORY_RESERVED
PAYMENT_COMPLETED
DELIVERY_CREATED
COMPLETED
COMPENSATING
COMPENSATED
FAILED
애플리케이션이 중간에 종료되더라도 저장된 상태를 보고 다음 작업을 이어갈 수 있어야 합니다.
멱등성
동일한 요청이 여러 번 전달되어도 한 번 처리한 것과 같은 결과가 나와야 합니다.
동일한 결제 요청 2회
→ 실제 결제는 1회
동일한 환불 요청 2회
→ 실제 환불도 1회
재시도
일시적인 네트워크 오류라면 바로 보상하지 않고 재시도할 수 있습니다.
일시적인 오류 → 재시도
카드 한도 초과 → 즉시 실패 처리
결과를 알 수 없는 타임아웃 → 상태 조회
보상 실패 처리
보상 트랜잭션도 실패할 수 있습니다.
배송 접수 실패
→ 결제 환불 요청
→ 환불 API 장애
이 경우 환불 요청을 다시 실행할 수 있도록 상태를 저장하고 재시도해야 합니다.
2PC와 Saga 비교
| 처리 방식 | 하나의 분산 트랜잭션 | 여러 로컬 트랜잭션 |
| 실패 처리 | 전체 롤백 | 보상 트랜잭션 |
| 일관성 | 강한 일관성 | 최종적 일관성 |
| 자원 잠금 | 길어질 수 있음 | 상대적으로 짧음 |
| 외부 API 적용 | 일반적으로 어려움 | 적용 가능 |
| 구현 시 고려사항 | 코디네이터 | 상태, 재시도, 보상 |
| 적합한 환경 | 통제된 트랜잭션 환경 | MSA와 장시간 업무 흐름 |
정리
분산 트랜잭션은 특정한 코드나 상태값을 의미하지 않습니다.
하나의 비즈니스 작업이 여러 독립적인 데이터베이스나 시스템에 걸쳐 실행될 때 전체 데이터의 일관성을 어떻게 유지할 것인가에 대한 문제다.
하나의 DB에서는 COMMIT과 ROLLBACK으로 처리할 수 있지만, 분산 환경에서는 일부 시스템만 성공할 수 있습니다.
이를 해결하는 대표적인 방법은 다음과 같습니다.
2PC
→ 여러 참여자를 하나의 트랜잭션으로 묶는다.
Saga
→ 작업을 여러 로컬 트랜잭션으로 나눈다.
→ 중간에 실패하면 보상 트랜잭션을 실행한다.
MSA에서는 서비스의 독립성과 확장성을 유지하기 위해 Saga 패턴을 많이 사용합니다. 하지만 Saga를 적용하려면 진행 상태, 재시도, 멱등성 및 보상 실패까지 함께 설계해야 합니다.
결국 분산 트랜잭션의 핵심은 실패가 발생하지 않도록 만드는 것이 아닙니다.
일부 작업은 언제든 실패할 수 있다는 전제에서, 시스템이 일관된 상태로 복구될 수 있도록 설계하는 것이 핵심이다.
'CS 정리' 카테고리의 다른 글
| Cache-Aside를 실무에서 사용하면 어떤 문제가 생길까? (0) | 2026.08.29 |
|---|---|
| Cache-Aside란? (0) | 2026.08.28 |
| CQRS란? 읽기와 쓰기를 분리하는 이유 (0) | 2026.08.24 |
| 서킷 브레이커(Circuit Breaker)란? 외부 API가 죽었을 때 우리 서버까지 같이 죽지 않게 하기 (0) | 2026.08.23 |
| Transactional Outbox에서 여러 워커가 이벤트를 안전하게 선점하는 방법 (0) | 2026.08.22 |