본문 바로가기
won2dev-log
HomeArchiveTagsCategoriesAboutProjects
HomeArchiveTagsCategoriesAboutProjects
won2dev-logwon2dev-logwon2dev-log

비전공 개발자의 로그 | won2dev-log

Navigation
  • Home
  • Archive
  • About
  • Projects
Categories
  • Docs
  • TIL
  • Project
  • Automation
  • Git · GitHub
더보기
Tags
  • TIL
  • Java
  • Spring
  • Backend
  • n8n
더보기
About

기록을 거름 삼아 공유는 성장을 만든다.

LicensePrivacy
© won2dev 2026. All rights reserved.
Home›TIL›MSA에서 트랜잭션은 왜 어려운가 — 분산 트랜잭션과 Saga Pattern
TIL

MSA에서 트랜잭션은 왜 어려운가 — 분산 트랜잭션과 Saga Pattern

won2dev·2026년 05월 17일
#TIL#MSA#Saga

처음엔 당연하다고 생각했다. 실패하면 rollback 하면 되는 거 아닌가.

근데 서비스가 나뉘기 시작하면 그게 안 된다. 이전 글에서 주문 하나에 결제, 배송, 재고가 다 엮인다고 했는데, Kafka로 비동기 처리는 해결했다 치자. 근데 이런 상황은 어떻게 되는 걸까?

결제는 성공했는데, 재고 차감에서 실패하면?

모놀리스였으면 그냥 @Transactional 하나로 전체 rollback. 근데 MSA에선 결제 서비스 DB, 재고 서비스 DB가 따로 있다. 한쪽만 성공한 채로 끝나버린다.

이게 MSA의 핵심 난제다.


모놀리스에선 왜 됐나

모놀리스 구조 다이어그램 — 단일 DB에 여러 테이블
모놀리스 구조 다이어그램 — 단일 DB에 여러 테이블

모놀리스 구조에서 트랜잭션은 단순하다.

plain text
주문 처리 시작
  ├── 결제 처리     ← 같은 DB
  ├── 재고 차감     ← 같은 DB
  └── 배송 등록     ← 같은 DB
전부 성공 → COMMIT
하나라도 실패 → 전체 ROLLBACK
주문 처리 시작
  ├── 결제 처리     ← 같은 DB
  ├── 재고 차감     ← 같은 DB
  └── 배송 등록     ← 같은 DB
전부 성공 → COMMIT
하나라도 실패 → 전체 ROLLBACK

같은 DB 안에서 일어나는 일이라 ACID 트랜잭션이 다 책임진다.

  • Atomicity: 전부 성공하거나, 전부 실패하거나
  • Consistency: 데이터 정합성 보장
  • Isolation: 다른 트랜잭션이 중간 상태를 못 봄
  • Durability: 커밋되면 영구 저장

스프링에서 @Transactional 하나 붙이면 끝나는 이유가 이것이다.


MSA에서 왜 안 되는가

MSA 구조 다이어그램 — 서비스마다 독립된 DB
MSA 구조 다이어그램 — 서비스마다 독립된 DB

MSA의 핵심 원칙 중 하나가 "서비스마다 DB를 독립적으로 가진다" 는 거다.

왜냐면 DB를 공유하면 결국 서비스가 DB에서 커플링되기 때문이다. 배포도 같이, 장애도 같이, 스케일도 같이 가야 한다. MSA를 쓰는 이유가 없어진다.

그래서 이렇게 나뉜다:

서비스DB
주문 서비스주문 DB
결제 서비스결제 DB
재고 서비스재고 DB
배송 서비스배송 DB

이 상태에서 "결제 성공 + 재고 차감 실패" 가 나면?

  • 결제 DB엔 결제 완료 기록이 남아 있음
  • 재고 DB엔 차감이 안 됨
  • 두 DB가 서로 다른 상태 → 정합성 깨짐

그리고 이건 단순히 서버 내부 문제가 아니다.

사용자는 카드 결제 완료 알림까지 받았는데, 실제로는 주문이 실패 상태가 되어버린다.

사용자 입장에선 돈은 빠져나갔고, 주문은 없다. 이게 MSA에서 트랜잭션 처리를 제대로 안 했을 때 나타나는 비즈니스 문제다. 기술 문제가 아니라 고객 신뢰 문제가 된다.

@Transactional은 같은 DB 연결 안에서만 유효하다. 네트워크 너머의 다른 서비스 DB까지 rollback 해주는 마법은 없다.


2PC — 분산 트랜잭션의 첫 번째 시도

이 문제를 해결하려는 첫 번째 접근이 2PC (Two-Phase Commit) 다.

2PC 흐름 다이어그램 — Coordinator가 Prepare → Commit 두 단계
2PC 흐름 다이어그램 — Coordinator가 Prepare → Commit 두 단계

이름 그대로 두 단계로 진행된다.

Phase 1: Prepare

  • 중앙 Coordinator가 모든 참여 서비스에 "커밋할 준비 됐어?" 라고 물음
  • 각 서비스는 "OK" 또는 "NO" 응답

Phase 2: Commit or Rollback

  • 전부 OK면 → 전체 COMMIT
  • 하나라도 NO면 → 전체 ROLLBACK

이론상 완벽해 보인다. 근데 실제 MSA 환경에서 쓰기가 굉장히 어렵다.

문제 1: 블로킹 Prepare 단계에서 모든 서비스가 응답할 때까지 락을 잡고 기다린다. 서비스가 수십 개면 그만큼 대기 시간이 늘어난다.
문제 2: 단일 장애점 Coordinator가 죽으면? Prepare까지만 된 상태로 모든 서비스가 락을 잡고 기다린다. 전체 시스템이 멈춰버린다.
문제 3: 네트워크 분단 Commit 신호를 보냈는데 일부 서비스에만 도달하면? 절반은 커밋, 절반은 미커밋. 오히려 더 복잡한 상태가 된다.
문제 4: 확장성 서비스가 5개일 때는 그럭저럭 버틸 수 있다. 근데 50개가 되면? Coordinator 하나가 50개 서비스의 응답을 기다리고, 50개 서비스가 동시에 락을 잡는다. 서비스가 늘수록 병목이 심해지는 구조다. MSA를 쓰는 이유 자체가 독립적 확장인데, 2PC는 그걸 정면으로 막는다.

MSA는 수십~수백 개의 서비스가 네트워크로 연결된 구조다. 네트워크 장애는 언제든 생긴다. 2PC는 이 환경에서 너무 취약하다.


Saga Pattern — 다른 접근

2PC가 "전부 동시에 커밋하자" 라는 접근이었다면, Saga는 방향을 바꾼다.

각 서비스가 자기 트랜잭션을 독립적으로 처리하고, 실패하면 이미 성공한 것들을 직접 되돌린다.

전체를 하나의 트랜잭션으로 묶는 대신, 로컬 트랜잭션의 연속으로 나눈다.

Saga 흐름 다이어그램 — 각 서비스가 순서대로 로컬 트랜잭션 처리, 실패 시 보상 트랜잭션 역방향 실행
Saga 흐름 다이어그램 — 각 서비스가 순서대로 로컬 트랜잭션 처리, 실패 시 보상 트랜잭션 역방향 실행

주문 흐름 예시:

plain text
1. 주문 서비스    → 주문 생성 (로컬 트랜잭션)
2. 결제 서비스    → 결제 처리 (로컬 트랜잭션)
3. 재고 서비스    → 재고 차감 (로컬 트랜잭션) ← 실패
1. 주문 서비스    → 주문 생성 (로컬 트랜잭션)
2. 결제 서비스    → 결제 처리 (로컬 트랜잭션)
3. 재고 서비스    → 재고 차감 (로컬 트랜잭션) ← 실패

재고 차감에서 실패하면?

plain text
3. 재고 서비스    → 차감 실패
2. 결제 서비스    → 결제 취소 (보상 트랜잭션)
1. 주문 서비스    → 주문 취소 (보상 트랜잭션)
3. 재고 서비스    → 차감 실패
2. 결제 서비스    → 결제 취소 (보상 트랜잭션)
1. 주문 서비스    → 주문 취소 (보상 트랜잭션)

이미 성공한 것들을 거꾸로 되돌리는 보상 트랜잭션(Compensating Transaction) 이 핵심이다.

실제로 이런 상황이다:

쇼핑몰에서 10만원짜리 상품을 주문했다. 카드 결제까지 완료됐는데, 알고 보니 재고가 없었다. 이때 Saga는 결제 서비스에 "결제 취소" 보상 트랜잭션을 날린다. 사용자 카드로 10만원이 환불된다.

DB rollback이 아니다. 이미 커밋된 결제를 새로운 취소 트랜잭션으로 되돌리는 것이다. 그게 보상 트랜잭션의 의미다.


Choreography vs Orchestration

Saga를 구현하는 방식은 두 가지다.

Choreography (안무형)

Choreography 다이어그램 — 서비스들이 이벤트로 서로 소통, 중앙 조율자 없음
Choreography 다이어그램 — 서비스들이 이벤트로 서로 소통, 중앙 조율자 없음

서비스들이 이벤트로 서로 소통한다. 중앙에서 지휘하는 주체가 없다.

plain text
주문 서비스 → "주문생성됨" 이벤트 발행
결제 서비스 → 이벤트 수신 → 결제 처리 → "결제완료됨" 이벤트 발행
재고 서비스 → 이벤트 수신 → 재고 차감 → "재고차감됨" 이벤트 발행
주문 서비스 → "주문생성됨" 이벤트 발행
결제 서비스 → 이벤트 수신 → 결제 처리 → "결제완료됨" 이벤트 발행
재고 서비스 → 이벤트 수신 → 재고 차감 → "재고차감됨" 이벤트 발행

장점: 서비스 간 결합도가 낮다. 새 서비스 추가해도 기존 서비스 건드릴 필요 없음.

단점: 전체 흐름이 코드 한 곳에 없다. 어디서 문제가 생겼는지 추적하기 어렵다.

Kafka 같은 메시지 브로커랑 자연스럽게 잘 맞는다.

Orchestration (지휘형)

Orchestration 다이어그램 — 중앙 Orchestrator가 각 서비스에 순서대로 명령
Orchestration 다이어그램 — 중앙 Orchestrator가 각 서비스에 순서대로 명령

중앙 Orchestrator가 전체 흐름을 지휘한다.

plain text
Orchestrator → 결제 서비스에 "결제해" 명령
결제 서비스  → 결제 완료 후 결과 반환
Orchestrator → 재고 서비스에 "차감해" 명령
재고 서비스  → 차감 완료 후 결과 반환
Orchestrator → 결제 서비스에 "결제해" 명령
결제 서비스  → 결제 완료 후 결과 반환
Orchestrator → 재고 서비스에 "차감해" 명령
재고 서비스  → 차감 완료 후 결과 반환

장점: 전체 흐름이 한 곳에 모여 있어서 파악하기 쉽다. 디버깅도 상대적으로 쉬움.

단점: Orchestrator 자체가 점점 무거워진다. 잘못하면 모든 비즈니스 로직이 여기 몰린다.

ChoreographyOrchestration
지휘자없음 (이벤트 기반)있음 (중앙 조율)
결합도낮음중간
흐름 파악어려움쉬움
디버깅어려움쉬움
적합한 상황서비스 수가 많고 독립성 중요흐름이 복잡하고 추적이 중요

Eventual Consistency — 완벽한 일관성은 포기한다

Saga의 핵심 전제가 있다. 일시적으로 불일치한 상태가 존재할 수 있다.

결제 서비스는 "완료"인데, 아직 재고 서비스가 처리 중인 순간이 있다.

이걸 Eventual Consistency (최종 일관성) 라고 한다.

모놀리스의 @Transactional은 트랜잭션 도중엔 중간 상태를 아무도 못 본다 (Isolation). 근데 Saga는 각 서비스가 독립적으로 커밋하니까 중간 상태가 외부에 노출될 수 있다.

대신 "결국엔 일관성이 맞춰진다"는 걸 보장한다.

그럼 사용자는 중간 상태를 보게 되는 걸까? 맞다. 의도적으로 허용하는 거다.

주문 직후 잠깐 동안은 "결제 완료 / 배송 준비 중" 같은 중간 상태가 사용자 화면에 보일 수 있다. 재고 서비스가 아직 처리 중인 순간이기 때문이다.

쿠팡에서 주문하면 "주문 접수됨 → 결제 확인 중 → 배송 준비 중" 이런 상태가 순서대로 바뀌는 걸 본 적 있을 거다. 그게 Eventual Consistency를 UI에서 자연스럽게 표현하는 방식이다. 한 번에 확정된 상태를 보여주는 게 아니라, 처리가 완료되는 순서대로 상태를 업데이트한다.

이게 성능이랑 트레이드오프다. 강한 일관성을 원하면 모든 서비스가 동기적으로 기다려야 한다. Eventual Consistency를 받아들이면 각 서비스가 독립적으로, 빠르게 처리할 수 있다.

MSA에서 Saga를 쓴다는 건 이 트레이드오프를 의식적으로 선택하는 거다.


정리

모놀리스MSA + 2PCMSA + Saga
트랜잭션 방식단일 DB 트랜잭션분산 트랜잭션 (동기)로컬 트랜잭션 + 보상 (비동기)
실패 처리DB rollback전체 rollback보상 트랜잭션
일관성강한 일관성강한 일관성Eventual Consistency
성능보통낮음 (블로킹)높음

처음엔 rollback 하면 되는 거 아닌가 싶었다. 근데 서비스가 나뉘면 rollback 자체가 "어떻게?" 라는 질문이 된다.

Saga는 그 질문에 대한 현실적인 답이다. 완벽한 일관성 대신, 보상이라는 개념으로 정합성을 맞춰간다.

Kafka 글에서 메시지 브로커가 왜 필요한지 봤다면, 이번엔 그 위에서 데이터 정합성을 어떻게 지키는지를 봤다.

근데 여기서 자연스럽게 다음 질문이 생긴다.

Saga에서 각 서비스는 로컬 트랜잭션 완료 후 이벤트를 발행한다.

근데 DB commit은 성공했는데 Kafka publish가 실패하면?

이벤트가 안 나갔으니 다음 서비스가 반응을 못 한다. Saga 흐름 자체가 끊겨버린다. 이게 Saga를 실제로 구현할 때 맞닥뜨리는 가장 현실적인 문제다.


참고 자료

  • microservices.io — Saga Pattern
  • Martin Fowler — Microservice Trade-Offs

공유하기
이전 글Keycloak을 처음 쓰면서 배운 것들 — 그게 뭔지도 모르고 시작했다다음 글 FK는 포인터다 — 주문 시스템에서 스냅샷이 필요한 이유

목차

  • 모놀리스에선 왜 됐나
  • MSA에서 왜 안 되는가
  • 2PC — 분산 트랜잭션의 첫 번째 시도
  • Saga Pattern — 다른 접근
  • Choreography vs Orchestration
  • Choreography (안무형)
  • Orchestration (지휘형)
  • Eventual Consistency — 완벽한 일관성은 포기한다
  • 정리

카테고리

TIL

태그

#TIL#MSA#Saga

최근 글

AI 에이전트에게 부하테스트를 맡겼더니, 테스트 환경을 직접 만들고 버그까지 찾아냈다JWT는 Stateless가 장점인데, 왜 Redis까지 붙였나Keycloak을 처음 쓰면서 배운 것들 — 그게 뭔지도 모르고 시작했다FK는 포인터다 — 주문 시스템에서 스냅샷이 필요한 이유Kafka 입문 — 메시징 큐가 왜 필요한가