MSA에서의 분산 트랜잭션
모놀리식 구조로 개발을 진행하는 경우에는
하나의 흐름으로 처리되어야 하는 작업은 메소드 상단에 @Transactional 를 붙여서 관리하였다.
모든 서비스가 하나의 프로젝트 내에서 관리되고 있기 때문에, 일부 서비스만 장애는 곧 전체 시스템 장애로 직결되었다.
따라서 하나의 작업에서 문제가 생겼을 때, 해당 작업을 모두 롤백되어도
같은 프로젝트 내부에 위치하며 데이터베이스를 공유하는 모놀리식 구조에서는 큰 문제점으로 인식되지 않았다.
그러나 프로젝트 규모가 증가함에 따라 독립적으로 개발 및 배포될 수 있는 서비스를 분리하여 운영하게 되었고,
이러한 분산 서비스 간의 통신을 위해서 API 호출과 이벤트 방식을 채택하였다.
하지만 MSA 환경에서 서비스 간의 통신을 수행하며 다음과 같은 문제가 발생하였다.
영화 티켓 판매 프로젝트에서 주문, 좌석(재고), 배송 서비스가 분리되어 있으며,
판매된 좌석이 실시간으로 비활성화 되어야 하고 주문이 완료되면 배송이 함께 생성된다고 가정한다.
문제점 ① 좌석 서비스 다운
상황 : 주문 생성 시, 좌석 서비스가 죽어 있으면 Feign 호출 실패 → 주문 전체 롤백 → 비교적 정상적인 동작
// OrderService
@Transactional
public void createOrder(OrderRequest request) {
Order order = orderRepository.save(
new Order(request.getUserId(), request.getSeatId())
);
// 좌석 차감 요청 (Feign)
seatClient.decreaseSeat(request.getSeatId());
// 배송 생성
deliveryClient.createDelivery(order.getId());
}
좌석 서비스 다운이 발생하였지만 트랜잭션 롤백으로 안전하게 처리됨.
Seat Service DOWN
→ FeignException 발생
→ @Transactional 롤백
→ Order 저장 취소됨
문제점 ② 네트워크 유실
상황: 좌석 감소는 성공했지만, 응답이 네트워크에서 유실됨 → 주문 실패로 판단하고 롤백 → 좌석만 감소됨 (데이터 불일치 발생)
@Transactional
public void createOrder(OrderRequest request) {
Order order = orderRepository.save(
new Order(request.getUserId(), request.getSeatId())
);
try {
seatClient.decreaseSeat(request.getSeatId());
} catch (Exception e) {
// 실제로 좌석이 줄고, 성공 응답을 보냈지만
// 네트워크 유실 발생 시
// 주문은 실패 처리됨
// 🚨 좌석만 줄어드는 문제 발생
throw new RuntimeException("Order Failed");
}
}
또한 주문 실패로 인식하여 동일 좌석으로 재주문을 시도할 경우, "이미 선택된 좌석입니다" 와 같은 오류가 발생함.
문제점 ③ 후속 서비스 실패
상황: 좌석 감소 성공 → 주문 저장 성공 → 배송 생성 실패 → 주문은 취소됐지만 좌석은 롤백 안됨 (데이터 불일치 발생)
@Transactional
public void createOrder(OrderRequest request) {
Order order = orderRepository.save(
new Order(request.getUserId(), request.getSeatId())
);
// 좌석 감소 (성공)
seatClient.decreaseSeat(request.getSeatId());
try {
// 배송 생성 (실패)
deliveryClient.createDelivery(order.getId());
} catch (Exception e) {
// 주문 취소됨
order.cancel();
// 🚨 좌석 롤백 없음
// 좌석은 여전히 줄어든 상태
throw e;
}
}
문제점 ④ 외부 API 지연
상황: 주문 생성 메소드에 알림 API 호출이 @Transactional 로 묶여 있다면,
외부 API 5초 지연 → DB Connection 5초 점유 → 동시 요청 증가 시, 커넥션 풀 고갈 가능
@Transactional
public void createOrder(OrderRequest request) {
Order order = orderRepository.save(
new Order(request.getUserId(), request.getSeatId())
);
seatClient.decreaseSeat(request.getSeatId());
// 🚨 외부 API 호출 (느림)
notificationClient.sendKakao(order.getId());
// 트랜잭션이 종료될 때까지 DB Connection이 반환되지 않기 때문에
// DB 커넥션을 계속 점유함
}
이처럼 MSA는 서비스를 작은 단위로 분리하여, 각 서비스가 독립적인 데이터베이스를 가지는 분산 시스템 환경에서 동작한다.
이 과정에서 네트워크 장애나 서비스 실패와 같은 예측하기 어려운 상황으로 인해
일부 서비스만 성공하거나 실패하는 문제가 발생한다.
또한 분산 환경에서는 여러 서비스와 데이터베이스가 하나의 요청 처리에 동시에 관여하기 때문에,
단일 시스템에서 보장되던 트랜잭션의 ACID 특성을 유지하기 어렵다.
이와 같은 문제는 결국 분산 시스템에서
일관성(Consistency), 가용성(Availability), 분할 내성(Partition Tolerance) 간의 선택 문제로 이어지며,
이를 설명하는 대표적인 개념이 CAP 이론이다.
MSA 환경과 CAP 이론
CAP 이론은 분산 시스템이 다음 세 가지 특성 중 두 가지만 동시에 보장할 수 있다는 원칙이다.

- 일관성(Consistency)
- 모든 노드가 동일한 데이터를 동시에 볼 수 있다.
- 한 노드에 쓰기 작업이 수행되면, 이후 모든 노드에서 읽기 작업이 수행될 때마다 업데이트된 값이 반환된다.
- 가용성(Availabillity)
- (장에 시에도) 시스템은 항상 응답을 반환한다.
- 오류가 발생하지 않은 노드에 대한 모든 요청은 응답을 받지만, 해당 응답에 최신 버전의 데이터가 포함되어 있다는 보장은 없다.
- 분할 내성(Partition Tolerance)
- 네트워크 유실(메시지 손실, 시스템 오류) 이 발생해도 시스템은 작동한다.
장애가 없는 네트워크는 존재하지 않으며,
MSA는 여러 서비스가 네트워크로 연결되어 있기 때문에 P(분할 내성)는 필수로 선택된다.
이후 서비스의 특성과 요구 사항에 따라 C(일관성)과 A(가용성) 중 어느 것을 우선시할 것인지 판단해야 한다.
판단 기준은 다음과 같다.
- 금융 거래/상품 재고 → 데이터 정합성이 생명이기 때문에 일관성(C)를 우선
- 상품 추천/알림 → 데이터가 최신이 아니더라도 사용자에게 끊김 없는 서비스를 제공하기 위해 가용성(A) 우선
2PC(Two-Phase Commit) - CP 성향
분산 트랜잭션에서 “전부 성공 아니면 전부 취소”라는 의미를 가지는 트랜잭션 원자성을 보장하기 위한 방법이다.
[수행 방법]
CP 방식은 두 단계를 통해 관련 모든 시스템이 트랜잭션을 일관되게 처리하도록 한다.

- 준비 단계 (Prepare Phase)
- 코디네이터(중앙 조정자)는 모든 참여자들에게 트랜잭션 준비 요청을 브로드캐스팅한다.
- 각 참여자들은 준비 완료 또는 준비 실패 응답을 코디네이터한태 응답한다.
- 커밋 단계 (Commit Phase)
- 코디네이터는 모든 서비스의 응답을 받을 때까지 대기한다.
- 모든 참여자가 준비 완료 상태라면 커밋을, 하나라도 실패하면 롤백을 결정한다.
- 결정된 작업을 모든 참여자들에게 브로드캐스팅하며 참여자들은 코디네이터의 요청을 보고 커밋 또는 롤백 후 통보한다.
[✅장점]
2PC 방식을 사용하면 다음과 같은 장점을 가진다.
- 분산 시스템에서 트랜잭션의 원자성을 보장한다.
- 모든 참여자가 트랜잭션을 성공적으로 완료하거나, 실패 시 일관되게 롤백하도록 한다.
따라서 DB와 메시지 큐를 동시에 커밋해야 하거나,
서로 다른 DB를 하나의 트랜잭션으로 묶어서 동시 쓰기 작업이 필요한 경우처럼
여러 시스템에 걸친 리소스를 하나의 트랜잭션으로 처리해야 할 때 2PC 방식이 사용되고 있다.
[⚠️한계점]
하지만 이렇게 모든 참여자의 응답을 받아 수행되는 2PC 방식의 한계점도 분명히 존재한다.

- 참여자들이 수가 계속 증가한다면? (확장 불가)
참여자가 30-40명으로 늘어난다면, 모든 참여자가 커밋 또는 롤백을 완료할 때까지 대기해야 한다.
이 과정에서 네트워크 지연이나 특정 참여자의 처리 지연이 발생하면 성능이 급속도로 저하될 수 있다. - 만약 코디네이터가 다운이 된다면? (SPOF)
코디네이터가 장애를 일으키면 전체 트랜잭션이 불확실한 상태에 빠질 수 있다.
코디네이터가 복구될 때까지 참여자들은 대기 상태가 된다. - 너무 느린 참여자가 있다면? (성능 저하)
트랜잭션이 커밋되거나 롤백될 때까지 관련 자원들이 잠긴 상태로 유지되어야 하므로, 이로 인해 다른 작업이 지연될 수 있다.
따라서 각 서비스가 독립된 서버, 독립된 DB를 가지며 네트워크를 통해 통신하는 MSA 환경에서는,
하나의 코디네이터가 모든 참여자를 제어하는 방식은 확장성과 장애 대응 측면에서 명확한 한계를 가진다.
그럼 우리는 이러한 분산 환경에서 각 서비스의 독립성을 유지하면서 데이터 일관성을 보장하기 위해서는 어떤 방식이 필요할까? 이러한 문제를 해결하기 위한 대표적인 방식이 Saga 패턴이다.
SAGA 패턴 - AP 성향
“ 일단 하나씩 진행하고 실패하면 되돌리자 ”
- 트랜잭션을 여러 단계로 나누어 처리하고, 각 단계가 독립적으로 커밋되는 방식
- 서비스 간의 메시지 또는 이벤트를 주고 받으며, 실패 시 보상 트랜잭션을 역순으로 실행하여 상태를 롤백한다.

보상 트랜잭션
분산 트랜잭션 환경에서 데이터 일관성을 유지하기 위한 방법 중 하나이다. 특정 작업이 실패했을 때, 해당 작업의 결과를 되돌리기 위한 별도의 보상 작업을 실행한다.

[보상 트랜잭션 특징]
보상 트랜잭션 특징은 다음과 같다.
- 상태 복구: 작업 실패 시 데이터를 이전 상태로 복구하여 일관성을 유지한다.
- 비동기적 보상: 작업이 실패했음을 탐지한 후 보상 트랜잭션이 수행된다.
- 서비스 내 책임 분담: 각 서비스가 자신의 보상 트랜잭션을 정의하고 실행한다.
[보상 트랜잭션 처리 흐름]
영화 예약 시스템에서 고객이 영화를 예매하고 결제를 완료했지만, 좌석 확보 과정에서 실패했다고 가정한다.
이때 예약 기록 삭제 → 결제 취소 순으로 진행된다면,
사용자 입장에서는 예약이 사라졌음에도 환불이 아직 처리되지 않은 상태가 되어 일시적인 데이터 불일치가 발생할 수 있다.
따라서 사용자에게 노출되는 불일치 구간을 최소화하기 위해
가장 최근에 수행된 작업부터 역순으로 되돌리는 방식으로 보상 트랜잭션을 수행하며,
결제 취소(환불 진행 안내) → 예약 상태 취소 처리 순으로 실행된다.
여기서 다음과 같은 의문이 들었다.
좌석 확보 실패로 예약이 취소되었으면 예약 데이터를 (soft) 삭제해도 되지 않을까?
결과적으로 보상 트랜잭션에서는 데이터를 삭제하기보다 상태를 변경하는 방식이 더 적절하다고 판단하였다.
그 이유는 다음과 같다.
- 왜, 언제, 누가 취소했는지에 대한 이력 추적이 필요한다.
- 예약 상태를 통해 결제 취소 실패, 재고 롤백 실패와 같은 후속 장애 복구 처리가 가능하다.
- 사용자 관점에서도 예약 내역이 아예 사라지는 것보다, 예약 취소 상태로 표시되는 것이 더 자연스럽다(UX 향상).
- Saga 패턴에서 각 서비스가 수행한 작업에 대응하는 보상을 정의해야 하므로, 예약 상태를 기반으로 보상 흐름을 제어하는 것이 전체 트랜잭션 관리에 더 적합하다.
SAGA 패턴의 두 가지 실행 방식
❶ 코레오그래피(Choreography) 사가 패턴
중앙 제어 없이, 각 서비스가 이벤트를 발행하고 해당 이벤트에 관심 있는 다른 서비스들이 이를 구독하여 동작을 이어가는 방식의 패턴이다.

[사용자 신용 확인 흐름 - 코레오그래피]
- Order 서비스가 POST /orders 요청을 받아 PENDING 상태의 주문을 생성한다.
- 그 후, Order Created 이벤트를 발생시킨다.
- Customer 서비스의 이벤트 핸들러는 신용 한도를 예약하려고 시도한다.
- 그런 다음, 그 결과를 나타내는 이벤트를 발생시킨다.
- Order 서비스의 이벤트 핸들러는 그 이벤트를 받아, 주문을 승인하거나 거절한다.
코레오그래피 방식은 서비스 간 직접 호출이 아닌 이벤트 기반 간접 통신을 사용하기 때문에 서비스 간 결합도가 낮다.
// OrderService
eventPublisher.publish(
new OrderCreatedEvent(orderId)
);
// SeatService
@EventListener
public void handle(OrderCreatedEvent event) {
reserveSeat(event.getSeatId());
}
// DeliveryService
@EventListener
public void handle(OrderCreatedEvent event) {
createDelivery(event.getOrderId());
}
이와 같이 OrderService는 주문 생성 이벤트를 발행하고 있지만,
좌석과 배달에 대한 존재 여부를 모르고 누가 이 이벤트를 받을지도 모른다.
따라서 새로운 서비스가 추가되거나 제거되더라도 기존 서비스에 대한 수정이 최소화된다.
또한 중앙 제어자가 존재하지 않기 때문에 단일 실패 지점(SPOF)이 없고 확장성 측면에서도 우수하다.
하지만 서비스 수가 증가할수록 이벤트 흐름이 여러 서비스에 분산되기 때문에, 전체 비즈니스 흐름을 파악하기 어려워진다.
예를 들어 주문을 처리하기 위해 어떤 순서로 어떤 서비스가 동작하는지 확인하기 위해
OrderService 코드
SeatService 코드
PaymentService 코드
DeliveryService 코드
주문과 연관된 코드 전체를 다 찾아봐야 한다.
또한 Saga 참가자 간의 순환 종속성이 발생할 가능성이 있으며,
여러 서비스를 동시에 실행해야 하는 통합 테스트의 복잡성이 증가하는 단점이 있다.
따라서 비즈니스 로직이 복잡한 대규모 프로젝트보다 서비스 수가 적고 비즈니스 흐름이 비교적 단순한 프로젝트에서는
코레오그래픽 방식을 선택하는 것이 적절할 수 있다.
❷ 오케스트레이션(Orchestration) 사가 패턴
분산 트랜잭션을 책임지는 별도의 Orchestration(중계자)가 존재하는 패턴이다.
이 중계자는 각 서비스의 수행 상태를 관리하며, 다음에 수행해야 할 작업을 참가 서비스들에게 명령(Command) 형태로 전달한다.
또한 Saga 수행 과정에서 각 단계의 상태를 저장 및 해석하여, 모든 작업이 정상적으로 완료되면 트랜잭션을 종료하고
중간에 오류가 발생하면 보상 트랙잭션을 실행하여 오류 복구를 처리한다.

[사용자 신용 확인 흐름 - 오케스트레이션]
- Order 서비스가 POST /orders 요청을 받아 Create Order Saga 오케스트레이터를 생성한다.
- 사가 오케스트레이터는 PENDING 상태의 주문을 생성한다.
- 그런 다음, Customer 서비스로 Reserve Credit(신용 예약) 명령을 보낸다.
- Customer 서비스는 신용 예약을 시도한다.
- 그 후, 결과를 나타내는 응답 메시지를 사가 오케스트레이터에게 보낸다.
- 사가 오케스트레이터는 해당 결과에 따라 주문을 승인하거나 거절한다.
오케스트레이터는 모든 단계의 실행 순서를 제어하므로 서비스 간 순환 종속성이 발생할 가능성이 낮고,
각 참가 서비스는 다른 서비스의 존재를 알 필요 없이 오케스트레이터의 명령만 수행하면 된다.
결과적으로 작업 흐름이 한 곳에 모이게 되고 이 덕분에 디버깅과 장애 분석이 비교적 용이하다는 장점을 가진다.
따라서 서비스 수가 많거나 비즈니스 로직이 복잡한 대규모 워크플로우에 환경에 적합하다.
하지만 모든 흐름을 중앙에서 제어하는 구조이기 때문에 오케스트레이터 내부 로직이 점점 복잡해질 수 있으며,
전체 흐름을 담당하는 오케스트레이터에서 장애가 발생하는 경우 단일 실패 지점(SPOF)이 될 가능성도 존재한다.
Saga 패턴 도입 시, 문제 및 고려 사항
Saga 패턴은 MSA 환경에서 데이터 일관성을 유지하기 위한 효과적인 방법이지만, 단순히 패턴을 도입하는 것만으로 모든 문제가 해결되는 것은 아니다. 실제 시스템에 Saga를 적용하기 위해서는 다음과 같은 사항을 우선적으로 고려한다면, 안정적인 분산 시스템을 설계하기 위한 중요한 첫 걸음이 될 것이다.
- 디자인 사고의 변화와 러닝커브
Saga 패턴을 채택하기 위해서는 기존의 단일 트랜잭션 중심 설계에서 벗어나,
여러 마이크로서비스 간의 트랜잭션 흐름과 데이터 일관성 유지 방식에 대한 새로운 설계 접근이 필요하다.
또한 보상 트랜잭션 설계, 이벤트 흐름 관리, 비동식 처리 방식에 대한 이해가 요구되기 때문에
Saga 패턴을 도입하기 위한 러닝커브가 존재한다. - 되돌릴 수 없는 로컬 데이터베이스 변경
Saga 방식에서는 각 서비스가 자신의 로컬 데이터베이스에 변경 내용을 커밋한 이후 다음 단계를 진행한다.
따라서 모놀리식 구조의 트랜잭션처럼 단순한 롤백으로 이전 상태로 되돌릴 수 없으며,
이를 대신하여 보상 트랜잭션을 설계해야 한다. - 일시적인 오류 처리 및 멱등성
네트워크 지연이나 일시적인 오류는 분산 시스템에서 빈번하게 발생할 수 있다.
따라서 동일한 요청이 여러 번 전달되더라도 결과가 동일하게 유지될 수 있도록 멱등성을 반드시 보장해야 한다. - Saga 모니터링 및 추적 필요성
Saga는 여러 단계를 거쳐 수행되기 때문에 각 단계의 진행 상태를 추적할 수 있는 모니터링 및 로깅 체계가 필수적으로 필요하다. 이를 통해 장애 발생 시 어떤 단계에서 문제가 발생했는지 신속하게 파악할 수 있다. - 보상 트랜잭션의 한계
보상 트랜잭션이 항상 성공하지 못할 수 있으므로 시스템이 일관되지 않은 상태로 남을 수 있다.
이를 고려하여 적절한 재시도 전략과 장애 처리 전략을 설계해야 한다.
[참고자료]
https://www.hellointerview.com/learn/system-design/core-concepts/cap-theorem
https://learn.microsoft.com/ko-kr/azure/architecture/patterns/saga