3. MSA에서 트랜잭션이 어려운 이유

박은서's avatar
May 01, 2026
3. MSA에서 트랜잭션이 어려운 이유

MSA에서 트랜잭션이 어려운 이유

1️⃣ 트랜잭션

1) 트랜잭션이란?

  • 여러 작업을 하나로 묶어서 처리하는 것
  • 전부 성공해야 하고, 중간에 하나라도 실패하면 전부 원래대로 되돌려야 함

2) 예시

  • 주문 생성
  • 결제 승인
  • 재고 차감
➡️ 이 3개가 하나의 작업처럼 움직여야 함
  • 3개 다 성공 → 최종 성공
  • 중간에 하나 실패 → 전부 취소

2️⃣ 모놀리식에서 트랜잭션이 쉬운 이유

1) 하나의 애플리케이션 + 하나의 DB인 경우가 많음

  • 같은 DB 안에서 처리 가능
  • DB 트랜잭션으로 묶기 쉬움
  • 예시
    • @Transactional public void order() { orderRepository.save(order); paymentRepository.save(payment); stockRepository.decrease(itemId, qty); }
      ➡️ 중간에 예외 발생하면 DB가 rollback 해줌
      즉, 하나의 DB 트랜잭션으로 끝남

3️⃣ MSA에서 어려운 이유

1) 서비스가 나뉘어 있음

  • 따로 존재하는 서비스
    • 주문 서비스
    • 결제 서비스
    • 재고 서비스
  • DB도 따로 가짐
    • 주문 DB
    • 결제 DB
    • 재고 DB
즉, 더 이상 하나의 @Transactional로 전체를 묶을 수 없음

2) 네트워크를 거쳐야 함

  • 모놀리식은 메서드 호출이지만, MSA는 서비스끼리 HTTP, gRPC, 메시지 큐 등으로 통신함
    • ➡️ 문제 발생
    • 주문 서비스는 성공
    • 결제 서비스 호출 중 네트워크 오류
    • 재고 서비스는 아직 실행 안 됨
➡️ 이렇게 되면 중간 상태가 생김

3) 각 서비스의 로컬 트랜잭션은 성공했는데 전체는 실패할 수 있음

  1. 주문 서비스 → 주문 생성 성공
  1. 결제 서비스 → 결제 실패
  1. 재고 서비스 → 실행 안 됨
➡️ 현재 상태
  • 주문은 생김
  • 결제는 안 됨
  • 재고는 그대로
즉, 사용자 입장에서는 이상한 데이터가 됨

4️⃣ 핵심 문제: 분산 트랜잭션(Distributed Transaction)

1) 분산 트랜잭션이란?

  • 여러 서비스, 여러 DB에 걸친 트랜잭션(주문 DB, 결제 DB, 재고 DB 등)을 하나의 트랜잭션처럼 다루고 싶은 것

2) 어려운 이유

  • 각 서비스가 서로 다른 서버, DB, 트랜잭션 경계를 가지고 있기 때문
  • DB 하나 안에서는 rollback이 쉽지만,
    • 서비스가 나뉘면 이미 다른 서비스에서 commit된 작업을 한 번에 되돌리기 어려움

5️⃣ 대표적인 문제 상황

1) 부분 성공 문제

  • 상황
    • 주문은 저장됐는데 결제는 실패
  • 결과
    • 주문만 남음
    • 사용자는 주문이 된 건지 안 된 건지 헷갈림

2) 데이터 불일치 문제

  • 상황
    • 결제는 완료됐는데 재고 차감은 실패
  • 결과
    • 돈은 받았음
    • 재고는 그대로
    • 나중에 재고 수량이 꼬일 수 있음

3) 중복 처리 문제

  • 상황
    • 결제 요청을 보냈는데 응답이 안 옴
    • 그래서 재시도했더니 결제가 두 번 됨
  • 결과
    • 중복 결제
    • 중복 주문
    • 중복 이벤트 처리

4) 롤백 불가 문제

  • 상황
    • 이미 다른 서비스가 commit한 작업은 내 서비스에서 rollback()처럼 쉽게 되돌릴 수 없음
  • 예시
    • 결제 서비스가 카드 승인 완료
    • 그 뒤 주문 서비스 실패
    • ➡️ 이때 단순 rollback이 아니라 결제 취소라는 별도 작업이 필요함
      즉, MSA에서는 rollback 대신 보상 작업이 자주 필요함

6️⃣ 왜 2PC를 잘 안 쓰는가?

1) 2PC(2-Phase Commit)란?

  • 여러 시스템에 “준비됐니?”, “그럼 다 같이 commit 하자”하고 맞추는 방식

2) 이론적으로는 가능하지만 실무에서는 잘 안 씀

  • 성능 부담 큼
  • coordinator 장애 시 복잡
  • 락이 오래 걸릴 수 있음
  • MSA의 느슨한 결합과 잘 안 맞음
  • 클라우드/NoSQL/메시지 브로커 환경과도 잘 안 맞는 경우 많음
➡️ “전통적인 DB식 강한 트랜잭션”을 MSA 전체에 그대로 적용하기 어렵다

7️⃣ MSA에서 해결 방법

🎯 해결 방식의 핵심 방향

① 강한 일관성보다 최종 일관성(Eventual Consistency)을 많이 사용
  • 지금 당장 모든 서비스 데이터가 완벽히 똑같을 필요는 없고 조금 시간이 지나면 맞춰지도록 설계
  • 순간적으로는 주문 완료 / 결제 대기 상태일 수 있지만 잠시 후 결제 성공 이벤트가 오면 최종 완료
② 하나의 큰 트랜잭션 대신 각 서비스는 자기 DB에서만 로컬 트랜잭션 처리
  • 서비스 간에는 이벤트/보상 처리로 연결

1) 대표 해결 방법 1: Saga 패턴

① Saga란?
  • 하나의 큰 트랜잭션 대신 여러 개의 작은 로컬 트랜잭션으로 나누고,
    • 실패하면 이전 작업들을 보상 트랜잭션으로 취소하는 방식
② 예시 흐름
  1. 주문 서비스 → 주문 생성
  1. 결제 서비스 → 결제 승인
  1. 재고 서비스 → 재고 차감
➡️ 그런데 3번에서 실패하면 2번, 1번을 되돌림
  1. 결제 취소 (2번)
  1. 주문 취소 (1번)
③ 중요한 점
  • 여기서 “되돌림”은 DB rollback이 아니라 비즈니스적으로 반대 작업을 수행하는 것
    • 주문 생성 ↔ 주문 취소
    • 결제 승인 ↔ 결제 취소
    • 재고 차감 ↔ 재고 복구
④ 장단점
  • 장점
    • MSA 구조에 잘 맞음
    • 서비스 독립성 유지 가능
  • 단점
    • 설계가 복잡함
    • 보상 로직을 직접 만들어야 함
    • 모든 작업이 완벽히 되돌릴 수 있는 건 아님
⑤ Saga의 두 가지 방식
  1. Choreography 방식
      • 중앙 관리자 없이 서비스들이 이벤트를 보고 각자 다음 행동 수행
        • 주문 생성됨 이벤트 발행
        • 결제 서비스가 듣고 결제 수행
        • 결제 완료 이벤트 발행
        • 재고 서비스가 듣고 재고 차감
      • 장점
        • 중앙 제어가 없어 느슨한 결합
      • 단점
        • 흐름 추적 어려움
        • 서비스가 많아지면 복잡해짐
  1. Orchestration 방식
      • 중앙 오케스트레이터가 흐름 제어
        • 오케스트레이터가 주문 서비스 호출
        • 성공하면 결제 서비스 호출
        • 성공하면 재고 서비스 호출
        • 실패하면 보상 작업 호출
      • 장점
        • 흐름이 명확함
        • 관리 쉬움
      • 단점
        • 중앙 제어 컴포넌트 필요
        • 오케스트레이터 의존성 생김

2) 대표 해결 방법 2: Outbox 패턴

① 필요한 이유
  • 자주 생기는 문제
    • DB 저장은 성공
    • 그런데 이벤트 발행은 실패
    • 주문은 DB에 저장됨
    • 그런데 OrderCreated 이벤트를 메시지 브로커에 못 보냄
    • ➡️ 그러면 다른 서비스는 주문이 생긴 걸 모름
② 해결 아이디어
  • 주문 저장과 이벤트 저장을 같은 로컬 DB 트랜잭션으로 처리
    • orders 테이블 저장
    • outbox 테이블에 이벤트 저장
    • ➡️ 둘 다 한 트랜잭션으로 commit
      ➡️ 그 다음 별도 프로세스가 outbox를 읽어서 메시지 브로커로 발행
③ 장점
  • DB 저장과 이벤트 유실 문제를 줄일 수 있음
  • Saga와 같이 자주 사용됨

3) 대표 해결 방법 3: Idempotency(멱등성)

① 중요한 이유
  • MSA에서는 네트워크 문제 때문에 재시도가 자주 발생함
    • 응답이 안 와서 다시 요청
    • 메시지가 중복 전달
    • 소비자가 같은 이벤트를 두 번 처리
② 멱등성이란?
  • 같은 요청을 여러 번 처리해도 결과가 한 번 처리한 것과 같아야 함
    • 같은 결제 요청 ID로 두 번 요청해도 결제는 한 번만 처리
    • 같은 주문 취소 이벤트를 두 번 받아도 한 번만 취소
③ 방법
  • 요청 ID 저장
  • 이벤트 ID 저장
  • 중복 처리 여부 체크

8️⃣ 상태 설계

1) 상태를 설계해야 하는 이유

① 성공/실패만 두면 부족함
  • MSA에서는 중간 상태가 필요함
    • ORDER_CREATED
    • PAYMENT_PENDING
    • PAYMENT_COMPLETED
    • STOCK_RESERVED
    • COMPLETED
    • FAILED
    • CANCELED
    • ➡️ 이렇게 상태를 세분화해야 함
  • 이유
    • 모든 작업이 즉시 끝나지 않기 때문
    • 지금은 결제 대기 중일 수 있고 잠시 뒤 최종 완료될 수 있음
    • ➡️ 그래서 사용자에게도 “주문 완료”가 아니라 “주문 접수됨 / 결제 확인 중” 같은 상태를 보여줘야 할 때가 많음

2) Spring 기준으로 이해하기

@Transactional의 범위
  • @Transactional내 서비스의 로컬 DB 작업까지만 안전하게 묶어줌
    • @Transactional public void createOrder() { orderRepository.save(order); // 여기까지는 로컬 트랜잭션 paymentClient.requestPayment(...); // 다른 서비스 호출 }
    • 여기서 paymentClient.requestPayment(...)는 다른 서비스 호출이므로 같은 DB 트랜잭션처럼 묶이지 않음
    • ➡️ 내 DB 저장, 다른 서비스 처리를 하나의 ACID 트랜잭션으로 보장할 수 없음
② 그래서 보통 이렇게 감
  • 내 DB 저장
  • 이벤트 발행 or 다음 서비스 호출
  • 실패 시 보상 처리
  • 상태값 관리

3) 주문 예시로 전체 흐름 보기

⭕ 정상 흐름
  1. 주문 서비스: 주문 생성 (PENDING)
  1. 주문 서비스: OrderCreated 이벤트 발행
  1. 결제 서비스: 결제 성공
  1. 결제 서비스: PaymentCompleted 이벤트 발행
  1. 재고 서비스: 재고 차감 성공
  1. 재고 서비스: StockReserved 이벤트 발행
  1. 주문 서비스: 주문 상태 COMPLETED
❌ 실패 흐름
  1. 주문 서비스: 주문 생성 (PENDING)
  1. 결제 서비스: 결제 성공
  1. 재고 서비스: 재고 차감 실패
  1. 결제 서비스: 결제 취소
  1. 주문 서비스: 주문 취소 상태 변경

9️⃣ 한 줄로 이해하기

모놀리식은 하나의 DB 안에서 rollback으로 되돌리기 쉽지만,
MSA는 서비스별로 DB와 트랜잭션이 분리되어 있어서 중간 실패가 생기면 단순 rollback이 안 되고,
대신 보상 트랜잭션, 상태 관리, 이벤트 기반 처리, 멱등성 보장으로 문제를 해결해야 한다.

🔟 시험/면접용 핵심 정리

1) 왜 문제인가?

  • 서비스마다 DB와 트랜잭션 경계가 다름
  • 하나의 ACID 트랜잭션으로 묶기 어려움

2) 무슨 문제가 생기나?

  • 부분 성공
  • 데이터 불일치
  • 중복 처리
  • 롤백 어려움

3) 어떻게 해결하나?

  • Saga 패턴
  • 보상 트랜잭션
  • Outbox 패턴
  • 멱등성 보장
  • 최종 일관성 수용
  • 상태값 세분화
Share article