Day 6. 분산 트랜잭션과 실패 처리
1. 오늘의 학습 목표
- 로컬 트랜잭션과 분산 트랜잭션의 차이를 설명할 수 있다.
- 하나의
@Transactional로 여러 서비스의 변경을 롤백할 수 없는 이유를 설명할 수 있다. - 보상 트랜잭션과 Saga의 관계를 주문 흐름에 적용할 수 있다.
- Choreography와 Orchestration의 기초 차이를 비교할 수 있다.
- DB 저장과 Kafka 발행 사이의 이중 쓰기 문제 및 Outbox의 필요성을 설명할 수 있다.
- Outbox와 Consumer 멱등성이 함께 필요한 이유를 설명할 수 있다.
2. 학습 전 생각해보기
질문 1
Order DB 저장은 성공했지만 Kafka 발행은 실패하면 다른 서비스는 주문 생성을 어떻게 알 수 있을까?
내 답변
질문 2
주문 생성과 재고 차감까지 성공한 뒤 결제가 실패하면 앞선 성공을 단순 DB 롤백할 수 있을까?
내 답변
질문 3
각 서비스가 이벤트만 보고 다음 행동을 결정하는 방식과 중앙 진행자가 단계를 지시하는 방식은 어떤 차이가 있을까?
내 답변
3. 핵심 개념 설명
3.1 로컬 트랜잭션과 분산 트랜잭션
로컬 트랜잭션은 한 서비스가 관리하는 하나의 데이터베이스 범위에서 변경을 원자적으로 묶는다. Order 서비스의 주문과 주문 항목을 같은 DB 트랜잭션으로 저장하면 둘 다 성공하거나 둘 다 롤백할 수 있다.
분산 트랜잭션 문제는 하나의 업무가 여러 서비스와 각자의 DB에 걸쳐 있을 때 생긴다. 주문 생성, 재고 차감, 결제 승인은 각각 다른 프로세스와 데이터베이스에서 별도 로컬 트랜잭션으로 완료된다.
네트워크를 사이에 둔 각 로컬 성공을 하나의 메모리 호출처럼 되돌릴 수 없다. 어느 단계에서든 지연, 장애, 응답 유실이 생길 수 있으므로 부분 성공을 업무적으로 다뤄야 한다.
3.2 하나의 @Transactional로 롤백할 수 없는 이유
Spring의 일반적인 @Transactional은 현재 애플리케이션이 연결한 트랜잭션 자원 범위를 관리한다. Order 메서드에 붙인 애너테이션이 이미 Stock 서비스 DB에 커밋된 변경이나 Payment 서비스의 외부 승인을 자동 롤백하지 않는다.
각 서비스는 자기 트랜잭션 관리와 커밋 시점을 가진다. Order 프로세스가 실패하더라도 다른 서비스 프로세스의 트랜잭션 관리자에게 같은 호출 스택으로 롤백 명령을 전달할 수 없다.
따라서 MSA에서는 “전체를 한 번에 롤백”하는 사고 대신 각 단계의 상태를 기록하고, 뒤 단계 실패 시 이미 성공한 업무를 어떻게 되돌리거나 상쇄할지 설계한다.
3.3 보상 트랜잭션과 Saga
보상 트랜잭션은 이미 커밋된 업무의 효과를 반대로 상쇄하는 새로운 업무 동작이다. 재고 차감이 커밋된 뒤 결제가 실패했다면 과거 트랜잭션을 시간 역행해 롤백하는 것이 아니라 재고 복원이라는 새 트랜잭션을 수행한다.
보상은 기술적인 반대 SQL과 항상 같지 않다. 결제 취소 수수료나 이미 발송된 상품처럼 완전히 원상복구할 수 없는 업무도 있다. 그러므로 도메인 의미를 가진 보상 규칙이 필요하다.
Saga는 여러 서비스의 로컬 트랜잭션을 일련의 단계로 구성하고, 실패하면 앞서 완료된 단계에 대응하는 보상을 실행해 전체 업무 결과를 맞추는 패턴이다.
3.4 Choreography와 Orchestration
Choreography 방식에서는 중앙 지휘자 없이 각 서비스가 이벤트를 보고 자기 작업과 다음 이벤트를 결정한다. Order 생성 → Stock 차감 → Payment 처리처럼 서비스들이 사건에 반응하며 흐름이 진행된다.
Orchestration 방식에서는 Orchestrator가 전체 진행 상태를 알고 각 서비스에 다음 작업을 지시한다. 흐름을 한곳에서 보기 쉬운 대신 Orchestrator에 책임과 결합이 모일 수 있다.
둘 중 하나가 항상 정답은 아니다. 단계 수, 흐름 가시성, 변경 빈도와 팀 책임을 고려한다. 오늘은 두 방식의 역할 차이까지만 이해한다.
3.5 DB 저장 성공, Kafka 발행 실패: 이중 쓰기 문제
Order가 주문을 DB에 저장한 뒤 Kafka에 OrderCreated를 발행하면 두 개의 서로 다른 자원에 쓰게 된다. DB 트랜잭션이 커밋된 직후 Kafka 발행이 실패하면 주문은 존재하지만 Stock은 주문 사실을 영원히 모를 수 있다.
반대로 Kafka를 먼저 발행하고 DB 저장이 실패하면 Consumer는 실제로 존재하지 않는 주문의 이벤트를 받을 수 있다. 실행 순서만 바꿔서는 두 쓰기의 원자성을 만들 수 없다.
이처럼 DB 변경과 메시지 발행 중 하나만 성공할 수 있는 상황을 이중 쓰기 문제라고 볼 수 있다.
3.6 Transactional Outbox가 필요한 이유
Transactional Outbox는 업무 데이터와 “발행할 이벤트”를 같은 로컬 DB 트랜잭션에서 함께 저장한다. Order와 Outbox 레코드가 같이 커밋되므로 주문만 저장되고 발행 의도가 사라지는 틈을 줄인다.
별도의 발행 처리가 Outbox의 미발행 레코드를 Kafka에 보낸다. Kafka가 잠시 실패해도 Outbox 레코드가 남아 있으므로 나중에 다시 시도할 수 있다.
Outbox가 DB와 Kafka를 하나의 트랜잭션으로 합치는 것은 아니다. DB 안에서 업무 변경과 발행 의도를 원자적으로 보존하고, 실제 발행은 재시도 가능한 별도 단계로 만드는 패턴이다.
3.7 Outbox와 멱등성이 함께 필요한 이유
Outbox 발행기가 Kafka 전송에는 성공했지만 Outbox를 “발행 완료”로 표시하기 전에 종료될 수 있다. 재시작 후 같은 레코드를 다시 보내면 Consumer는 같은 이벤트를 여러 번 받을 수 있다.
Outbox는 이벤트 유실 가능성을 줄이지만 중복 가능성을 없애지 않는다. 따라서 Consumer는 event ID 등을 기준으로 이미 적용한 이벤트의 업무 효과를 다시 만들지 않도록 멱등하게 처리해야 한다.
두 패턴은 서로 다른 실패 지점을 다룬다. Outbox는 Producer 쪽의 DB 변경과 발행 사이를, 멱등성은 Consumer 쪽의 중복 업무 효과를 보호한다.
4. 흐름으로 이해하기
[Order DB 로컬 트랜잭션]
├── Order 저장
└── Outbox(OrderCreated) 저장
│
↓ 별도 발행기
Kafka
│ (중복 전달 가능)
↓
Stock 멱등 Consumer
│
├── 차감 성공 → StockDeducted
└── 차감 실패 → 실패 이벤트
│
↓
후속 중단/보상
결제 실패 → 재고 복원이라는 새 로컬 트랜잭션
Order와 발행 의도는 같은 로컬 트랜잭션에 저장된다. 발행기는 Kafka 전송을 재시도할 수 있으며 이 과정에서 중복이 생길 수 있어 Consumer 멱등성이 필요하다. 결제 실패 뒤 재고 복원은 기존 커밋의 기술적 롤백이 아니라 새로운 보상이다.
5. 직접 채우는 핵심 질문
질문 1. 로컬 트랜잭션과 분산 트랜잭션 문제를 자원과 프로세스 범위로 비교하라.
내 답변
답변에 포함해야 할 키워드
- 하나의 DB
- 여러 서비스
- 부분 성공
질문 2. Order의 @Transactional이 Stock DB 변경을 롤백하지 못하는 이유는 무엇인가?
내 답변
답변에 포함해야 할 키워드
- 프로세스
- 트랜잭션 관리자
- 별도 커밋
질문 3. 결제 실패 후 재고 복원이 “롤백”보다 “보상”인 이유를 설명하라.
내 답변
답변에 포함해야 할 키워드
- 이미 커밋
- 새로운 트랜잭션
- 업무 의미
질문 4. Saga를 로컬 트랜잭션과 보상의 조합으로 설명하라.
내 답변
답변에 포함해야 할 키워드
- 단계
- 부분 성공
- 보상
질문 5. Choreography와 Orchestration에서 “다음 행동을 결정하는 주체”를 비교하라.
내 답변
답변에 포함해야 할 키워드
- 각 서비스
- 이벤트
- 중앙 Orchestrator
질문 6. DB 저장 후 Kafka 발행 방식의 실패 구간을 설명하라.
내 답변
답변에 포함해야 할 키워드
- DB 커밋
- 발행 실패
- 이벤트 유실
질문 7. Kafka를 먼저 발행하면 이중 쓰기 문제가 해결되지 않는 이유는 무엇인가?
내 답변
답변에 포함해야 할 키워드
- 발행 성공
- DB 실패
- 존재하지 않는 업무 데이터
질문 8. Outbox가 보장하려는 것과 보장하지 않는 것을 각각 적어라.
내 답변
답변에 포함해야 할 키워드
- 발행 의도
- 같은 DB 트랜잭션
- 중복 가능
질문 9. Outbox 발행 성공 직후 프로세스가 종료되면 왜 Consumer 멱등성이 필요한가?
내 답변
답변에 포함해야 할 키워드
- 완료 표시 전 종료
- 재발행
- 중복 효과 방지
6. 상황 판단 문제
상황 1
주문 생성, 재고 차감은 성공했고 결제 승인만 실패했다.
- 이미 커밋된 두 변경을 Order의 예외로 모두 롤백할 수 있는가?
- 재고에는 어떤 보상 동작이 필요한가?
- 주문 상태에는 어떤 실패 결과를 표현해야 하는가?
내 답변
상황 2
Order 저장 메서드가 커밋된 뒤 KafkaTemplate.send()가 실패했다. Outbox는 없다.
- 서비스별 상태는 어떻게 달라지는가?
- 메서드 호출 순서만 바꾸는 해결책이 충분하지 않은 이유는 무엇인가?
- Outbox는 무엇을 같은 트랜잭션에 넣는가?
내 답변
상황 3
Outbox 발행기가 같은 OrderCreated(eventId=10)을 두 번 Kafka에 보냈다. Stock은 두 번 차감했다.
- Outbox가 실패한 것인가, 예상 가능한 중복인가?
- Producer와 Consumer 쪽에서 각각 필요한 보호는 무엇인가?
내 답변
7. 오늘의 용어 정리
| 용어 | 내가 작성하는 정의 |
|---|---|
| 로컬 트랜잭션 | |
| 분산 트랜잭션 | |
| 부분 성공 | |
| 보상 트랜잭션 | |
| Saga | |
| Choreography | |
| Orchestration | |
| 이중 쓰기 문제 | |
| Transactional Outbox | |
| 멱등 Consumer |
8. 프로젝트에 한 줄 연결
- 주문·재고·결제는 각 서비스의 로컬 트랜잭션으로 처리되므로 결제 실패 시 재고 복원 보상이 필요하다.
- 업무 데이터 저장과 이벤트 발행 의도를 함께 보존하기 위해 각 Producer 서비스에 Outbox가 필요할 수 있다.
- Outbox가 재발행한 이벤트에도 Stock과 Payment의 업무 효과는 멱등해야 한다.
9. 오늘의 자기 점검
- 핵심 개념을 내 말로 설명할 수 있다.
- 오늘 등장한 주요 용어를 구분할 수 있다.
- 주문 시스템에 개념을 적용할 수 있다.
- 이해하지 못한 부분을 표시했다.
- 다음 주제와 현재 주제를 혼동하지 않는다.
10. 이해하지 못한 부분
11. 3문장 요약
12. 복습 퀴즈
- Order의
@Transactional이 기본적으로 관리하는 범위는 무엇인가? - 이미 커밋된 재고 차감을 상쇄하는 새 업무 동작을 무엇이라 하는가?
- 중앙 진행자 없이 서비스가 이벤트에 반응하는 Saga 방식은 무엇인가?
- 업무 데이터와 발행할 이벤트를 같은 DB 트랜잭션으로 저장하는 패턴은 무엇인가?
- Outbox를 사용해도 Consumer 멱등성이 필요한 이유는 무엇인가?
복습 퀴즈 정답과 해설 보기
- 현재 서비스가 관리하는 로컬 트랜잭션 자원, 보통 해당 서비스의 DB 범위다.
- 보상 트랜잭션이다.
- Choreography다.
- Transactional Outbox다.
- 발행 성공과 완료 표시 사이의 장애 등으로 같은 이벤트가 재발행될 수 있기 때문이다.