대규모 트래픽 분산처리 시스템을 MSA 기반으로 개발하던 중 가장 복잡했던 부분은 주문(Order) 생성 시 여러 서비스(재고/배송) 와의 연동 과정에서 발생하는 트랜잭션 처리였다.
현재 개발 중인 물류 시스템은 다음과 같은 구조를 가지고 있다.
- Order-Service: 주문 생성
- Stock-Service: 재고 확인 및 차감
- Delivery-Service: 배송 요청 처리
중요한 점은 이 모든 과정을 하나의 트랜잭션처럼 처리하고 싶지만 마이크로서비스 구조(MSA)에서는 분산 트랜잭션을 하나의 DB 트랜잭션처럼 처리할 수 없다는 점이다.
Spring Cloud + OpenFeign을 이용해 Order-Service ↔ Stock-Service ↔ Delivery-Service 로직을 연동하고 실패 시 보상 트랜잭션(SAGA Orchestration) 으로 재고를 복구하는 구조를 구축했다.
1. 주문 생성 흐름 요약
1. 주문 요청이 들어오면 → Stock Service에 재고 차감 요청
2. 재고가 충분하면 차감 후, 차감된 가격과 허브 정보(hubId) 반환
3. OrderService에서 주문 생성 → DB 저장
4. DeliveryService에 배송 생성 요청
5. Delivery 생성 실패 시 → Stock 롤백 (보상 트랜잭션)
2. 처음 겪었던 문제: 재고가 부족해도 주문이 생성되고, 오히려 재고가 증가...?!
테스트를 실행해보니 재고가 부족한데 rollbackStock이 호출되어 오히려 재고가 증가했다.
처음엔 단순히 "재고 부족 예외"라고 생각했는데 주문 생성 실패 이후에도 rollbackStock()이 호출되면서 애초에 차감되지도 않은 재고를 다시 더하는 현상이 발생했다.
이는 MSA에서 트랜잭션을 다룰 때 자주 마주치는 문제다. 각 서비스는 독립적으로 실행되고 DB도 분리되어 있으므로 하나의 트랜잭션으로 묶을 수 없다. 이로 인해 실패했는지 여부를 판단하지 않고 rollback()을 호출하면 데이터 무결성이 깨지게 된다.
이 문제를 해결하기 위해 다음과 같은 보상 트랜잭션 조건 분기를 도입했다.
3. 보상 트랜잭션이란? (SAGA Orchestration 방식)
Kafka 등 이벤트 기반 아키텍처 없이 Order-Service 내에서만 로직적으로 분산 트랜잭션을 관리하고 싶었다. 그래서 내가 도입한 건
오케스트레이션 방식의 보상 트랜잭션 (Compensating Transaction)
즉 하나의 트랜잭션 흐름이 실패하면 이미 처리된 이전 단계를 역으로 취소하는 방식이다.
어떤 상황에서 rollback(보상)이 필요한가?
1. 재고 부족으로 실패한 경우 rollback X
- 재고 검증 → 실패 시 예외 발생
- 주문도 생성되지 않았으므로 rollback 대상 없음
2. 배송 생성 실패한 경우 rollback O
- 재고 차감 → 주문 생성 → 배송 요청 시 실패
- 이미 차감된 재고를 rollback 해야 함
코드에서 이를 명확히 나눴다.
StockCheckResponse stockResponse = null;
try {
stockResponse = stockClient.checkAndDecreaseStock(...);
// 주문 생성 + 저장
// 배송 생성 요청
// 배송 ID로 주문 업데이트
} catch (Exception e) {
log.warn("주문 생성 도중 예외 발생 → 보상 트랜잭션 조건 확인 중: {}", e.getMessage());
if (stockResponse != null) {
//stockResponse가 null이 아니라는 건 재고 차감이 이미 수행된 것!
//이미 재고 차감이 된 경우에만 rollback
log.warn("배송 생성 실패로 보상 트랜잭션 수행 시작 (rollbackStock 호출)");
try {
rollbackStock(request.getProductId(), request.getQuantity());
log.info("보상 트랜잭션 성공: 재고 복구 완료");
} catch (Exception rollbackEx) {
log.error("보상 트랜잭션 실패: 재고 복구 실패", rollbackEx);
}
}
throw new RuntimeException("주문 생성 실패 -> 보상 트랜잭션 수행!: " + e.getMessage());
}
이 분기를 통해 재고 차감이 성공했을 경우에만 rollback이 실행되도록 설계했다. 이렇게 흐름을 명확히 나눔으로써 각 단계에서 실패 발생 시 어느 시점까지 처리됐는지를 기반으로 보상 트랜잭션을 판단할 수 있게 되었다.
4. 실제 로그로 보는 SAGA 보상 적용 결과
배송 요청이 실패했을 때 콘솔에는 다음과 같은 로그가 출력된다.
[WARN] 주문 생성 도중 예외 발생 → 보상 트랜잭션 조건 확인 중
[WARN] 배송 생성 실패로 보상 트랜잭션 수행 시작 (rollbackStock 호출)
[INFO] 보상 트랜잭션 성공: 재고 복구 완료
이걸 통해 실제로 보상 트랜잭션이 잘 동작하고 있는지 확인할 수 있다.
5. 테스트 시나리오 요약
테스트 유형 | 설명 | 기대 결과 |
정상 주문 생성 | 재고 충분 + 배송 성공 | 주문 생성, 재고 차감, 배송 ID 저장 |
배송 실패 | 재고 충분하지만 배송 생성 실패 | 주문은 저장되지만, 배송 생성 실패로 재고 복구 실행됨 |
재고 부족 | 재고 자체가 부족함 | 주문 생성 자체 실패, rollback 불필요 |
6. 마무리
이번 작업을 통해 다음과 같은 교훈을 얻었다. MSA에서 "트랜잭션"은 기능이 아니라 설계다.
상태 값 없이도, Kafka 없이도, 오케스트레이션 기반으로 충분히 관리 가능한 구조가 가능하다.
중요한 건 실패를 가정하고 미리 흐름을 잘 나누는 것!
'Spring > MSA' 카테고리의 다른 글
MSA 환경에서 FeignClient 응답 데이터가 누락된 이유는? – CommonResponse<DTO>와 변수 범위 이슈 (0) | 2025.03.24 |
---|---|
MSA에서 JPA 트랜잭션 문제 & SAGA 패턴 필요성 (0) | 2025.03.17 |
[MSA] MSA 기반 상품 주문 시스템 (1) (0) | 2025.03.07 |
서킷 브레이커 (Resilience4j) (1) | 2025.02.12 |
Spring cloud와 MSA (0) | 2025.02.11 |
대규모 트래픽 분산처리 시스템을 MSA 기반으로 개발하던 중 가장 복잡했던 부분은 주문(Order) 생성 시 여러 서비스(재고/배송) 와의 연동 과정에서 발생하는 트랜잭션 처리였다.
현재 개발 중인 물류 시스템은 다음과 같은 구조를 가지고 있다.
- Order-Service: 주문 생성
- Stock-Service: 재고 확인 및 차감
- Delivery-Service: 배송 요청 처리
중요한 점은 이 모든 과정을 하나의 트랜잭션처럼 처리하고 싶지만 마이크로서비스 구조(MSA)에서는 분산 트랜잭션을 하나의 DB 트랜잭션처럼 처리할 수 없다는 점이다.
Spring Cloud + OpenFeign을 이용해 Order-Service ↔ Stock-Service ↔ Delivery-Service 로직을 연동하고 실패 시 보상 트랜잭션(SAGA Orchestration) 으로 재고를 복구하는 구조를 구축했다.
1. 주문 생성 흐름 요약
1. 주문 요청이 들어오면 → Stock Service에 재고 차감 요청
2. 재고가 충분하면 차감 후, 차감된 가격과 허브 정보(hubId) 반환
3. OrderService에서 주문 생성 → DB 저장
4. DeliveryService에 배송 생성 요청
5. Delivery 생성 실패 시 → Stock 롤백 (보상 트랜잭션)
2. 처음 겪었던 문제: 재고가 부족해도 주문이 생성되고, 오히려 재고가 증가...?!
테스트를 실행해보니 재고가 부족한데 rollbackStock이 호출되어 오히려 재고가 증가했다.
처음엔 단순히 "재고 부족 예외"라고 생각했는데 주문 생성 실패 이후에도 rollbackStock()이 호출되면서 애초에 차감되지도 않은 재고를 다시 더하는 현상이 발생했다.
이는 MSA에서 트랜잭션을 다룰 때 자주 마주치는 문제다. 각 서비스는 독립적으로 실행되고 DB도 분리되어 있으므로 하나의 트랜잭션으로 묶을 수 없다. 이로 인해 실패했는지 여부를 판단하지 않고 rollback()을 호출하면 데이터 무결성이 깨지게 된다.
이 문제를 해결하기 위해 다음과 같은 보상 트랜잭션 조건 분기를 도입했다.
3. 보상 트랜잭션이란? (SAGA Orchestration 방식)
Kafka 등 이벤트 기반 아키텍처 없이 Order-Service 내에서만 로직적으로 분산 트랜잭션을 관리하고 싶었다. 그래서 내가 도입한 건
오케스트레이션 방식의 보상 트랜잭션 (Compensating Transaction)
즉 하나의 트랜잭션 흐름이 실패하면 이미 처리된 이전 단계를 역으로 취소하는 방식이다.
어떤 상황에서 rollback(보상)이 필요한가?
1. 재고 부족으로 실패한 경우 rollback X
- 재고 검증 → 실패 시 예외 발생
- 주문도 생성되지 않았으므로 rollback 대상 없음
2. 배송 생성 실패한 경우 rollback O
- 재고 차감 → 주문 생성 → 배송 요청 시 실패
- 이미 차감된 재고를 rollback 해야 함
코드에서 이를 명확히 나눴다.
StockCheckResponse stockResponse = null;
try {
stockResponse = stockClient.checkAndDecreaseStock(...);
// 주문 생성 + 저장
// 배송 생성 요청
// 배송 ID로 주문 업데이트
} catch (Exception e) {
log.warn("주문 생성 도중 예외 발생 → 보상 트랜잭션 조건 확인 중: {}", e.getMessage());
if (stockResponse != null) {
//stockResponse가 null이 아니라는 건 재고 차감이 이미 수행된 것!
//이미 재고 차감이 된 경우에만 rollback
log.warn("배송 생성 실패로 보상 트랜잭션 수행 시작 (rollbackStock 호출)");
try {
rollbackStock(request.getProductId(), request.getQuantity());
log.info("보상 트랜잭션 성공: 재고 복구 완료");
} catch (Exception rollbackEx) {
log.error("보상 트랜잭션 실패: 재고 복구 실패", rollbackEx);
}
}
throw new RuntimeException("주문 생성 실패 -> 보상 트랜잭션 수행!: " + e.getMessage());
}
이 분기를 통해 재고 차감이 성공했을 경우에만 rollback이 실행되도록 설계했다. 이렇게 흐름을 명확히 나눔으로써 각 단계에서 실패 발생 시 어느 시점까지 처리됐는지를 기반으로 보상 트랜잭션을 판단할 수 있게 되었다.
4. 실제 로그로 보는 SAGA 보상 적용 결과
배송 요청이 실패했을 때 콘솔에는 다음과 같은 로그가 출력된다.
[WARN] 주문 생성 도중 예외 발생 → 보상 트랜잭션 조건 확인 중
[WARN] 배송 생성 실패로 보상 트랜잭션 수행 시작 (rollbackStock 호출)
[INFO] 보상 트랜잭션 성공: 재고 복구 완료
이걸 통해 실제로 보상 트랜잭션이 잘 동작하고 있는지 확인할 수 있다.
5. 테스트 시나리오 요약
테스트 유형 | 설명 | 기대 결과 |
정상 주문 생성 | 재고 충분 + 배송 성공 | 주문 생성, 재고 차감, 배송 ID 저장 |
배송 실패 | 재고 충분하지만 배송 생성 실패 | 주문은 저장되지만, 배송 생성 실패로 재고 복구 실행됨 |
재고 부족 | 재고 자체가 부족함 | 주문 생성 자체 실패, rollback 불필요 |
6. 마무리
이번 작업을 통해 다음과 같은 교훈을 얻었다. MSA에서 "트랜잭션"은 기능이 아니라 설계다.
상태 값 없이도, Kafka 없이도, 오케스트레이션 기반으로 충분히 관리 가능한 구조가 가능하다.
중요한 건 실패를 가정하고 미리 흐름을 잘 나누는 것!
'Spring > MSA' 카테고리의 다른 글
MSA 환경에서 FeignClient 응답 데이터가 누락된 이유는? – CommonResponse<DTO>와 변수 범위 이슈 (0) | 2025.03.24 |
---|---|
MSA에서 JPA 트랜잭션 문제 & SAGA 패턴 필요성 (0) | 2025.03.17 |
[MSA] MSA 기반 상품 주문 시스템 (1) (0) | 2025.03.07 |
서킷 브레이커 (Resilience4j) (1) | 2025.02.12 |
Spring cloud와 MSA (0) | 2025.02.11 |