본문으로 건너뛰기

11.5 실전 고수 팁 — MSA 분산 환경과 보상 트랜잭션, Outbox 패턴 맛보기

하나의 웅장한 서버 덩어리(Monolithic 모놀리식)에서 탈피해, 결제 서버, 배송 서버, 재고 서버를 각각 쪼개어 배포하는 MSA (Micro Service Architecture) 환경은 트랜잭션 롤백 관점에서 극악의 난이도를 자랑합니다.

초보들은 "왜 서버 쪼개면 트랜잭션 장애가 잡기 힘든가요?" 묻습니다.

🚫 1. 2PC (Two-Phase Commit) 분산 트랜잭션의 함정

한 지붕(단일 DB)에 모여 살 때는 @Transactional 하나 띡 붙여주면 재고 차감하다 뻑 났을 때 결제 이력INSERT 기록까지 0.1초만에 다 같이 깔끔하게 롤백(Rollback) 되었습니다.

그러나 서버 3개가 찢어지고 각각 A데이터베이스(결제DB), B데이터베이스(재고DB)를 별도로 쓰게 된다면? "결제 완료 성공!(Commit)" ➡️ "Kafka 메세지 전송" ➡️ 그 사이 배달 앱 강제 종료 ➡️ "배송 준비(Update) 중에 에러!"

이미 다른 서버 DB에서 Commit을 찍고 끝나버린 "완료된 거래(결제 데이터)"를 어떻게 원격으로 취소(롤백)시킬 것인가요?


↩️ 2. 보상 트랜잭션 (Saga Pattern) 코어 로직

이미 다른 DB에 커밋된 흔적(결제완료 row)을 물리적으로 지우는 건 불가능합니다. 그래서 "결제 금액을 다시 환불해주는(상계하는, 마이너스(-) 치는)" 역방향 로직(보상 트랜잭션, Compensating Transaction) 을 실행하라고 알려주는 방식이 바로 사가 패턴(Saga Pattern) 입니다.

@Service
@RequiredArgsConstructor
public class OrderSagaOrchestrator {

private final PaymentClient paymentClient; // 원격 REST (결제 서버 모듈)
private final InventoryClient inventoryClient; // 원격 REST (재고 서버 모듈)

public void createOrderSaga() {
try {
// [정방향 스텝 1] 결제 승인 Call (A서버) -> 트랜잭션 Commit 완료
paymentClient.approve();

// [정방향 스텝 2] 재고 차감 Call (B서버) -> 여기서 Lock 걸려 시간 초과로 에러(Exception) 터짐!
inventoryClient.decrease();

} catch (InventoryException e) {
// [🚨 보상 트랜잭션 발동]
// 이미 커밋되어버린 A서버(결제)에게 "야! 취소(환불)시켜!" 라고 마이너스 명령을 때림
paymentClient.cancelApprove();

throw new RuntimeException("SAGA 분산 처리 실패: 전체 주먹구구식 롤백 완료!");
}
}
}

📬 3. Outbox(아웃박스) 패턴 매커니즘 구현

MSA에서 서버 간 소통을 위해 Kafka 같은 메시지 브로커 이벤트(Event)를 쏩니다. "주문 완료 DB에 도장 찍고(Commit) ➡️ 상대방 서버 쳐다보라고 Kafka에 완료 메시지 쏴!"

문제는 이겁니다. "내 DB 트랜잭션은 커밋됐는데, 그 후 0.1초 순간 Kafka 서버가 다운돼서 메시지가 안 날아가버리면?" (반쪽짜리 좀비 생성)

이 원자성을 보장하기 위해 아예 "내 DB에 발송용 편지함 테이블(Outbox Table)" 을 함께 만들어 "하나의 트랜잭션 구간(@Transactional)" 안에서 동시에 묶어버립니다.

① 아웃박스 엔티티 설계

@Entity
public class OutboxEvent {
@Id @GeneratedValue
private Long id;
private String aggregateType; // "ORDER"
private Long aggregateId; // 100
private String payloadJson; // "{'status': 'PAID'}"
private boolean processed; // 외부(카프카) 전송 성공 완료 플래그
}

② 비즈니스 트랜잭션과 아웃박스 발행의 원자적(Atomic) 묶음

@Service
@RequiredArgsConstructor
public class OrderService {
@Transactional // 하나의 지붕으로 묶음
public void createOrder(Order order) {
// 1. 진짜 비즈니스 테이블에 INSERT
orderRepository.save(order);

// 2. 카프카로 쏘지 말고, 동일한 내 DB Outbox 임시 편지함 테이블에 기록을 같이 INSERT
// 둘 중 하나만 터져도 둘 다 함께 Rollback 방어 가능!
outboxRepository.save(new OutboxEvent("ORDER", order.getId()));
}
}

③ 이벤트 배포 백그라운드 스레드 (Relay)

별도의 Spring Batch@Scheduled(fixedDelay = 5000) 무한 루프 봇(스케줄러)이, 내 DB의 Outbox 테이블에서 processed == false 인 안 날아간 찌꺼기 편지들만 주기적으로 긁어모아 카프카로 다시 안전하게 폴링(Polling) 발사하고 true로 체크해줍니다.

이것이 현대 실리콘밸리 우버 오픈 프레임워크와 토스(Toss), 배달의민족 등에서 쓰는 가장 신뢰성 있는 분산 메세징 보장 기법 (Transactional Outbox Pattern) 의 정수입니다.