이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.
- MSA에서 단일 ACID transaction을 포기하면 무엇을 새로 설계해야 하는가?
- 데이터 소유권은 table 이름이 아니라 어떤 write 권한과 API 계약으로 드러나는가?
- Saga, outbox, idempotency는 성능, 정합성, 장애 격리, 운영 복잡도를 어떻게 바꾸는가?
개요
Monolith에서는 주문 생성, 결제 예약, 재고 차감을 하나의 DB transaction으로 묶을 수 있다. MSA에서는 각 service가 자기 DB를 소유하므로 같은 방식이 어렵다. 한 service가 다른 service의 table을 직접 수정하면 독립성이 깨지고, 분산 transaction을 억지로 묶으면 가용성과 운영 난이도가 나빠진다. 그래서 MSA에서는 데이터 owner, saga, outbox, idempotency, 보상 상태, 사용자 pending 계약이 필요하다. 핵심은 모든 것을 즉시 일관되게 만드는 것이 아니라 어떤 불변식은 즉시 보장하고 어떤 상태는 지연을 허용할지 정하는 것이다.
이 문서가 바꾸는 설계 축
| 축 | 바뀌는 내용 |
|---|---|
| 성능 | local transaction은 짧아지지만 service 간 event와 재처리 비용이 생긴다. |
| 정합성 | 단일 ACID에서 eventual consistency, saga, compensation으로 바뀐다. |
| 장애 격리 | 한 service DB 장애를 격리할 수 있지만 pending/unknown 상태가 늘어난다. |
| 운영 복잡도 | outbox, inbox, DLQ, replay, reconciliation, 상태 모니터링이 필요하다. |
데이터 소유권
Data owner는 “누가 table을 가지고 있는가”보다 “누가 write 권한과 불변식 책임을 가지는가”로 정한다.
| 데이터 | owner | 외부 service가 할 수 있는 일 |
|---|---|---|
| 주문 상태 | order service | 주문 API로 command 요청 |
| 결제 승인 | payment service | 결제 예약/취소 API 호출 |
| 재고 수량 | inventory service | 재고 예약 API 호출 |
| 배송 상태 | shipping service | 배송 요청 API 호출 |
| 외부 service가 owner DB를 직접 update하면 owner가 사라진다. | ||
| 조회가 필요하면 API, event projection, read model로 가져간다. | ||
| Write 권한은 하나여야 충돌 해결 기준이 명확해진다. |
대표 시나리오
주문 생성 flow를 service로 나눈다고 하자. 사용자 기대는 “주문 버튼을 눌렀을 때 결제 가능 여부를 알 수 있다”이다. 하지만 결제 provider timeout, 재고 service 지연, message broker 장애가 생길 수 있다. 단일 transaction처럼 즉시 성공/실패만 반환하면 거짓말이 된다. API 계약은 pending을 포함해야 한다.
{
"orderId": "order-123",
"status": "PAYMENT_PENDING",
"nextAction": "WAIT_OR_REFRESH"
}사용자는 완료가 아니라 접수와 진행 상태를 받는다. 이 계약이 있어야 eventual consistency를 사용자 경험으로 감당할 수 있다.
Saga 상태 모델
Saga는 여러 local transaction을 상태 전이로 연결한다.
CREATED
-> STOCK_RESERVED
-> PAYMENT_AUTHORIZED
-> CONFIRMED
CREATED
-> STOCK_RESERVE_FAILED
-> CANCELED
PAYMENT_AUTHORIZED
-> SHIPPING_REQUEST_FAILED
-> COMPENSATION_REQUIRED각 상태는 재시도 가능한지, 사용자에게 보여도 되는지, 보상이 필요한지 가져야 한다. 상태 없이 event만 던지면 장애 중 현재 주문이 어디에 멈췄는지 알 수 없다.
Orchestration과 Choreography
| 방식 | 장점 | 비용 | 적합한 경우 |
|---|---|---|---|
| orchestration | 중앙 saga가 순서와 상태를 관리한다. | orchestrator가 복잡해진다. | 주문처럼 단계와 보상이 명확한 flow |
| choreography | service들이 event에 반응한다. | 전체 흐름 파악이 어렵다. | 느슨한 후속 작업, 알림, 집계 |
| 주문 결제처럼 사용자 영향이 큰 flow는 orchestration이 더 읽기 쉽다. | |||
| 알림, 통계, 추천처럼 후속 반응은 choreography가 자연스럽다. | |||
| 둘 중 하나가 정답이 아니라 운영자가 장애 중 흐름을 추적할 수 있어야 한다. |
Outbox
Local DB 변경과 event 발행은 같은 transaction으로 묶기 어렵다. Outbox는 DB 변경과 발행할 event 기록을 같은 local transaction에 저장한다.
create table order_outbox (
event_id varchar(80) primary key,
aggregate_id varchar(80) not null,
event_type varchar(80) not null,
payload json not null,
status varchar(30) not null,
created_at timestamp not null
);@Transactional
public OrderId createOrder(CreateOrderCommand command) {
Order order = orderRepository.save(Order.create(command));
outboxRepository.save(OrderCreatedEvent.from(order));
return order.id();
}별도 publisher가 outbox를 읽어 broker로 발행한다. 발행 실패는 주문 transaction을 깨뜨리지 않고 재시도할 수 있다.
Idempotency와 Inbox
Event는 중복될 수 있다. Consumer는 event id나 business key로 중복 처리를 막아야 한다.
create table processed_events (
event_id varchar(80) primary key,
consumer_name varchar(80) not null,
processed_at timestamp not null
);Payment service가 같은 OrderCreated event를 두 번 받아도 결제 예약은 한 번만 만들어야 한다.
외부 결제 API에도 idempotency key를 전달한다.
MSA에서 중복 처리는 예외 상황이 아니라 기본 동작이다.
보상 트랜잭션
분산 환경에서는 rollback이 아니라 보상이 필요하다. 결제가 승인된 뒤 재고 예약이 실패하면 결제 취소 command를 보내야 한다. 보상도 실패할 수 있으므로 상태를 남긴다.
| 실패 위치 | 보상 |
|---|---|
| 재고 예약 실패 | 주문 취소 |
| 결제 승인 후 배송 요청 실패 | 배송 재시도 또는 결제 취소 |
| 결제 취소 실패 | 수동 확인 queue |
| event 발행 실패 | outbox 재시도 |
| 보상은 원래 상태로 완벽히 되돌리는 마법이 아니다. | |
| 돈, 재고, 포인트처럼 외부 영향이 있는 작업은 수동 조정 경로까지 필요하다. |
정합성 계약
데이터마다 필요한 정합성이 다르다.
| 데이터 | 즉시 보장 | 지연 허용 |
|---|---|---|
| 결제 금액 | 중복 승인 금지 | 결제 완료 표시 지연 |
| 재고 차감 | 음수 재고 금지 | 검색 재고 표시 지연 |
| 주문 목록 | 주문 생성 후 보이는 것 | 배송 상태 반영 지연 |
| 알림 | 중복 발송 최소화 | 발송 지연 |
| 정합성 계약을 분리하면 모든 데이터를 강하게 묶으려는 유혹을 줄일 수 있다. |
장애 중 운영
분산 transaction 장애에서는 “어느 단계에 멈췄는가”가 가장 중요하다. 필요한 지표는 다음과 같다.
- saga 상태별 주문 수
- outbox 미발행 건수와 oldest age
- consumer lag와 DLQ 건수
- compensation required 건수
- idempotency duplicate count
- pending 상태 평균 체류 시간 이 지표가 없으면 장애 중 데이터 정합성을 추측하게 된다.
개인 프로젝트 기준
- 주문 생성 flow를 상태 전이로 그린다.
- owner service와 write 권한을 표로 정리한다.
- outbox table과 idempotency table을 간단히 설계한다.
- 사용자 응답에 pending 상태를 포함한다.
- 분산 transaction을 쓰지 않는 이유를 eventual consistency와 운영 비용으로 설명한다.
기업 운영 기준
- service별 data owner와 schema 변경 승인자를 명시한다.
- saga 상태, outbox, inbox, DLQ, reconciliation dashboard를 운영한다.
- 보상 실패는 수동 처리 queue와 책임자를 가진다.
- 정합성 계약은 제품, CS, 운영팀과 합의한다.
- 재처리 runbook은 중복 실행 안전성을 먼저 검증한다.
위험 신호
- 다른 service DB를 직접 update한다.
- event 발행과 DB 저장 사이의 실패를 고려하지 않는다.
- pending/unknown 상태 없이 성공/실패만 반환한다.
- consumer가 중복 event를 처리할 수 없다.
- 보상 transaction이 실패할 경우의 상태가 없다.
- saga 상태를 저장하지 않아 장애 중 현재 위치를 알 수 없다.
확인 질문
확인 질문
- 확인 질문: MSA에서 데이터 owner는 무엇을 책임지는가?
- 답변: 해당 데이터의 write 권한, 불변식, schema 변경, API/event 계약, 장애 대응을 책임진다.
- 확인 질문: Outbox가 필요한 이유는 무엇인가?
- 답변: local DB 변경과 event 발행 사이의 실패를 재시도 가능한 기록으로 남기기 위해서다.
- 확인 질문: 분산 transaction에서 rollback 대신 무엇을 설계해야 하는가?
- 답변: saga 상태, 보상 command, idempotency, pending UX, reconciliation 절차를 설계해야 한다.