Saga Choreography와 Orchestration은 무엇을 맞바꾸는가

seonwoo_jung·2026년 6월 10일

1. 도입

마이크로서비스로 주문, 결제, 재고, 배송을 나누면 데이터베이스도 함께 나뉘는 경우가 많다. 이때 가장 먼저 막히는 질문은 단순하다. "주문 생성과 결제 승인과 재고 차감을 한 트랜잭션으로 묶을 수 없다면, 실패했을 때 어디까지 되돌려야 할까?"

Microservices Patterns에서 Chris Richardson은 이런 상황을 다룰 때 Saga 패턴을 제시한다. Saga는 여러 서비스에 걸친 작업을 로컬 트랜잭션의 연속으로 나누고, 중간 실패가 나면 이미 끝난 단계에 대해 보상 트랜잭션을 실행하는 방식으로 이해할 수 있다.

여기서 다시 선택지가 갈린다. 각 서비스가 이벤트를 보고 다음 일을 알아서 시작하는 Choreography로 갈지, 중앙의 조정자가 명령을 내리는 Orchestration으로 갈지다. 둘 다 "분산 트랜잭션을 피한다"는 같은 목표를 갖지만, 복잡도를 배치하는 위치가 다르다.

Saga의 핵심은 모든 것을 한 번에 커밋하는 것이 아니라, 실패를 전제로 흐름과 보상을 설계하는 데 있다.

2. Saga가 해결하려는 문제

전통적인 단일 애플리케이션에서는 하나의 데이터베이스 트랜잭션 안에서 여러 테이블을 바꾸는 일이 자연스럽다. 주문 서비스가 orders, payments, inventory 테이블을 같은 DB에서 다룬다면 실패 시 롤백도 비교적 명확하다.

마이크로서비스에서는 이 전제가 깨진다. 주문 서비스는 주문 DB를, 결제 서비스는 결제 DB를, 재고 서비스는 재고 DB를 가진다. 각 서비스가 자기 데이터만 직접 소유한다면, 한 서비스가 다른 서비스의 테이블을 같은 트랜잭션으로 수정할 수 없다.

물론 2-phase commit 같은 분산 트랜잭션 기술이 있지만, Richardson은 마이크로서비스 환경에서 서비스와 데이터베이스의 결합도가 커지고 가용성에도 부담을 줄 수 있다고 설명한다. 그래서 Saga는 "하나의 큰 ACID 트랜잭션" 대신 다음 구조를 택한다.

  1. 각 단계는 자기 서비스 안에서 로컬 트랜잭션으로 커밋한다.
  2. 커밋 후 이벤트를 발행하거나 다음 명령을 보낸다.
  3. 뒤 단계가 실패하면 앞 단계의 효과를 취소하는 보상 작업을 실행한다.

예를 들어 주문 생성 Saga는 아래처럼 흘러갈 수 있다.

주문 생성
  -> 결제 승인
  -> 재고 예약
  -> 배송 요청
  -> 주문 확정

재고 예약에서 실패하면 결제 승인을 취소하고 주문을 실패 상태로 바꾸는 식이다. 중요한 점은 보상 작업이 단순한 DB 롤백이 아니라는 것이다. 이미 외부 결제 승인 요청이 나갔다면 "승인 취소"라는 도메인 작업을 별도로 만들어야 한다.

3. Choreography: 이벤트를 따라 흐르는 Saga

Choreography 방식에서는 중앙 조정자가 없다. 각 서비스는 자신이 관심 있는 이벤트를 구독하고, 이벤트를 받으면 로컬 트랜잭션을 실행한 뒤 다시 이벤트를 발행한다.

예를 들어 주문 서비스가 OrderCreated 이벤트를 발행하면 결제 서비스가 이를 보고 결제를 승인한다. 결제 서비스는 PaymentApproved 또는 PaymentRejected 이벤트를 발행한다. 재고 서비스는 PaymentApproved를 보고 재고를 예약한다.

Order Service
  -- OrderCreated -->
Payment Service
  -- PaymentApproved -->
Inventory Service
  -- InventoryReserved -->
Order Service

장점은 각 서비스가 느슨하게 연결된다는 점이다. 주문 서비스가 결제 서비스의 API를 직접 호출하지 않아도 되고, 새로운 후속 작업이 필요하면 이벤트 구독자를 추가할 수 있다. 작은 흐름에서는 구조가 단순하고 자연스럽다.

하지만 흐름이 길어질수록 전체 Saga를 한눈에 보기 어려워진다. "주문 생성 후 어떤 이벤트들이 어떤 순서로 이어지는가"를 이해하려면 여러 서비스의 이벤트 핸들러를 따라가야 한다. 또한 여러 서비스가 같은 이벤트에 반응하면서 순서, 중복 처리, 재시도 정책이 흩어질 수 있다.

Choreography에서 특히 조심할 지점은 순환 의존이다. 주문 서비스가 결제 이벤트에 반응하고, 결제 서비스가 주문 이벤트에 반응하는 구조는 자연스럽지만, 정책이 늘어나면 서비스들이 암묵적으로 서로의 상태 전이를 기대하게 된다. 코드상 직접 호출이 없더라도 이벤트 계약을 통해 결합될 수 있다.

그래서 Choreography는 다음 조건에서 잘 맞는다고 이해했다.

조건이유
Saga 단계가 적다전체 흐름을 이벤트 몇 개로 추적할 수 있다
참여 서비스가 적다이벤트 계약이 과도하게 퍼지지 않는다
후속 작업이 독립적이다구독자 추가가 자연스럽다
중앙 흐름 제어가 덜 중요하다각 서비스의 자율성이 장점이 된다

4. Orchestration: 조정자가 명령하는 Saga

Orchestration 방식에서는 Saga orchestrator가 전체 흐름을 알고 있다. orchestrator는 각 서비스에 명령을 보내고, 응답을 받아 다음 단계를 결정한다. 서비스들은 자기 로컬 트랜잭션을 수행하고 결과를 돌려준다.

CreateOrderSaga
  -> Payment Service: authorizePayment
  <- Payment authorized
  -> Inventory Service: reserveInventory
  <- Inventory reservation failed
  -> Payment Service: cancelAuthorization
  -> Order Service: rejectOrder

이 방식의 장점은 흐름이 한 곳에 모인다는 점이다. 어떤 단계 다음에 어떤 단계가 실행되는지, 실패 시 어떤 보상이 필요한지를 orchestrator 코드에서 확인할 수 있다. Saga가 길거나 분기 조건이 많다면 이 가시성이 꽤 중요해진다.

반대로 orchestrator가 많은 도메인 지식을 갖게 될 위험이 있다. "결제가 승인되면 재고를 예약한다" 정도는 프로세스 지식이지만, "어떤 상품군은 어떤 재고 정책을 따른다" 같은 세부 규칙까지 orchestrator가 알기 시작하면 각 서비스의 책임이 흐려진다.

Orchestration은 중앙 컴포넌트가 생기는 만큼 테스트와 운영 포인트도 달라진다. orchestrator가 다운되면 진행 중인 Saga를 어떻게 재개할지, 각 명령이 중복 전달되어도 안전한지, 보상 명령이 실패하면 어떤 상태로 남길지 설계해야 한다. 이 문제들은 Choreography에도 있지만, Orchestration에서는 orchestrator의 상태 저장과 재시작 전략으로 더 명시적으로 드러난다.

작은 의사 코드로 보면 책임의 위치가 더 분명하다.

class CreateOrderSaga {
    void start(OrderId orderId) {
        try {
            payment.authorize(orderId);
            inventory.reserve(orderId);
            shipping.request(orderId);
            order.approve(orderId);
        } catch (InventoryFailed e) {
            payment.cancelAuthorization(orderId); // 이미 끝난 단계를 보상한다
            order.reject(orderId);
        }
    }
}

실제 구현은 이렇게 단순하지 않다. 네트워크 타임아웃, 메시지 중복, 프로세스 재시작 때문에 각 단계의 상태를 저장하고 재시도 가능한 형태로 만들어야 한다. 그래도 예시는 Orchestration이 "다음 단계 선택"을 한 곳에 둔다는 점을 보여준다.

5. 둘은 무엇을 맞바꾸는가

Choreography와 Orchestration의 차이를 "이벤트냐 명령이냐"로만 보면 절반만 본 셈이다. 더 중요한 차이는 전체 프로세스의 지식이 어디에 놓이는가다.

Choreography는 흐름의 지식을 각 서비스에 분산한다. 서비스 간 직접 호출은 줄어들지만, 이벤트 구독 관계를 통해 프로세스가 암묵적으로 형성된다. Orchestration은 흐름의 지식을 orchestrator에 모은다. 프로세스는 읽기 쉬워지지만, 조정자가 커지면 중앙집중적인 결합이 생긴다.

아래처럼 비교해볼 수 있다.

관점ChoreographyOrchestration
흐름 위치여러 이벤트 핸들러에 분산orchestrator에 집중
결합 형태이벤트 계약에 결합명령 API와 프로세스에 결합
장점서비스 자율성, 구독자 확장흐름 가시성, 실패 처리 명시성
위험전체 흐름 추적 어려움, 순환 의존조정자 비대화, 중앙 로직 집중
잘 맞는 경우짧고 단순한 Saga길고 분기가 많은 Saga

내가 기준을 잡는다면, 처음부터 한쪽을 원칙으로 고정하지는 않을 것 같다. 단계가 적고 이벤트 자체가 도메인 사실로 자연스럽다면 Choreography가 간결하다. 반대로 "이 단계가 실패하면 저 단계는 보상하고, 특정 조건에서는 다른 경로로 간다"처럼 프로세스 규칙이 핵심이면 Orchestration이 읽기 쉽다.

두 방식 모두 메시지 중복과 재시도에 대비해야 한다. OrderCreated 이벤트가 두 번 전달될 수 있고, authorizePayment 명령이 타임아웃 뒤 실제로는 성공했을 수도 있다. 그래서 각 서비스의 작업은 가능한 한 idempotent하게 설계해야 한다. 같은 주문 ID로 결제 승인을 두 번 요청했을 때 두 번 승인되지 않도록 막는 식이다.

또 하나의 공통점은 관찰 가능성이다. Saga는 여러 서비스에 걸친 시간차 작업이므로, 로그와 추적 ID가 없으면 실패 지점을 찾기 어렵다. Choreography에서는 이벤트 체인을 따라갈 수 있어야 하고, Orchestration에서는 Saga 인스턴스별 현재 단계와 보상 이력을 볼 수 있어야 한다.

6. 정리

Saga는 마이크로서비스에서 서비스별 로컬 트랜잭션을 이어 붙여 비즈니스 작업을 완성하는 패턴이다. 실패하면 이미 커밋된 작업을 보상 트랜잭션으로 되돌린다. Microservices Patterns가 강조하는 방향도 결국 분산 트랜잭션에 의존하기보다, 실패와 보상을 애플리케이션 흐름으로 모델링하는 쪽에 가깝다.

Choreography는 이벤트 기반으로 흐름을 분산시키고, Orchestration은 조정자를 두어 흐름을 한 곳에 모은다. Choreography는 작고 자연스러운 이벤트 흐름에 잘 맞고, Orchestration은 복잡한 프로세스와 보상 규칙을 명시적으로 다루기 좋다.

다음에 더 파고들 만한 주제는 두 가지다. 첫째, Saga에서 보상 트랜잭션이 항상 원래 작업의 완전한 역연산이 될 수 있는지다. 둘째, Saga 메시지를 안전하게 발행하기 위한 Outbox 패턴과 idempotency key 설계다. Saga 자체보다도 이 주변 장치들이 운영 안정성을 크게 좌우한다고 느꼈다.

참고 자료

  • Chris Richardson, Microservices Patterns, Saga pattern / Managing transactions with sagas
  • Microservices.io, Pattern: Saga

0개의 댓글