사가 패턴으로 마이크로서비스 트랜잭션 오케스트레이션하기
Wenhao Wang
Dev Intern · Leapcell

소개
현대 소프트웨어 개발의 진화하는 환경에서 마이크로서비스는 확장성, 복원력 및 독립적인 개발 측면에서 탁월한 이점을 제공하는 탁월한 아키텍처 스타일로 부상했습니다.
그러나 이러한 모듈성은 중요한 과제를 안고 있습니다. 바로 여러 서비스에 걸쳐 트랜잭션 일관성을 관리하는 것입니다.
단일 데이터베이스에서 ACID 속성이 본질적으로 보장되는 모놀리식 애플리케이션과 달리, 마이크로서비스는 종종 별도의 데이터베이스를 사용하여 분산 트랜잭션을 악명 높게 복잡하게 만듭니다.
전자 상거래 주문 프로세스를 상상해 보세요. 주문 생성, 재고 업데이트, 결제 처리.
일반적인 전자 상거래 주문 프로세스를 상상해 보세요. 주문 생성, 재고 업데이트, 결제 처리.
이러한 각 단계는 별도의 서비스에서 발생할 수 있습니다.
만약 어떤 단계든 실패하면 데이터 무결성을 유지하기 위해 전체 워크플로우를 올바르게 취소해야 합니다.
분산 환경에서 강력한 트랜잭션 무결성에 대한 이러한 중요한 요구는 바로 이 문제를 해결하도록 설계된 강력한 접근 방식인 사가 패턴으로 이어지며, 이는 별개의 서비스에 걸쳐 있더라도 비즈니스 프로세스가 일관되게 유지되도록 보장합니다.
분산 트랜잭션 딜레마와 사가 솔루션
사가 패턴 자체에 대해 자세히 알아보기 전에, 그 필요성과 작동을 뒷받침하는 몇 가지 기본 개념을 명확히 해 봅시다.
핵심 용어
- 마이크로서비스 아키텍처: 각 서비스를 독립적으로 개발, 배포 및 확장하는 느슨하게 결합된 서비스 컬렉션으로 애플리케이션을 구조화하는 아키텍처 스타일.
- 분산 트랜잭션: 여러 독립 시스템 또는 서비스를 포함하고 해당 작업에 대한 운영을 실행하는 트랜잭션. 로컬 트랜잭션과 달리 표준 ACID 보장은 직접 유지하기 어렵거나 불가능합니다.
- CAP 정리: 분산 컴퓨팅의 기본 정리로, 분산 데이터 저장소에서 일관성, 가용성 및 파티션 내결함성의 세 가지 보장 중에서 두 개 이상을 동시에 제공하는 것은 불가능하다고 명시합니다. 마이크로서비스 아키텍처는 종종 가용성과 파티션 내결함성을 우선시하여 최종 일관성을 제공합니다.
- 최종 일관성: 특정 데이터 항목에 대한 새로운 업데이트가 없는 경우, 결국 해당 항목에 대한 모든 액세스가 마지막 업데이트 값을 반환하는 일관성 모델. 이는 분산 시스템에서 일반적인 절충입니다.
- 보상 트랜잭션: 이전 작업을 의미론적으로 취소하는 작업. 이는 반드시 작업을 되돌리는 것이 아니라 그 효과를 보상하는 새로운 작업을 만드는 일종의 보상 작업입니다. 예를 들어, 계좌에서 돈이 인출되었다면, 보상 트랜잭션은 그 돈을 다시 입금하게 됩니다.
사가 패턴 설명
사가 패턴은 각 서비스가 자체 데이터베이스를 유지 관리하는 여러 서비스에 걸친 분산 트랜잭션을 관리하는 방법입니다.
여러 서비스에 걸친 단일 원자 트랜잭션(마이크로서비스에서 문제가 됨) 대신, 사가는 트랜잭션을 로컬 트랜잭션 시퀀스로 나눕니다.
각 로컬 트랜잭션은 자체 서비스 데이터베이스를 업데이트하고 이벤트를 게시하여 시퀀스에서 다음 로컬 트랜잭션을 트리거합니다.
로컬 트랜잭션이 실패하면, 사가는 이전의 성공적인 로컬 트랜잭션의 효과를 취소하기 위해 일련의 보상 트랜잭션을 실행합니다.
사가를 조정하는 두 가지 주요 방법이 있습니다.
-
안무 기반 사가:
- 각 서비스는 이벤트를 생성하고 소비하여 로컬 트랜잭션을 실행할지 여부와 시기를 결정합니다.
- 중앙 조정자는 없습니다. 서비스는 이벤트를 듣고, 작업을 수행한 다음, 새로운 이벤트를 발생시킵니다.
- 장점: 느슨하게 결합되어 간단한 워크플로를 구현하기 쉽습니다.
- 단점: 서비스 수가 증가함에 따라 특히 장기 실행 사가를 모니터링하고 디버깅하기 어려울 수 있습니다. 전체 흐름이 덜 투명합니다.
-
오케스트레이션 기반 사가:
- 중앙 오케스트레이터(전용 서비스)가 사가를 조정하는 책임을 맡습니다. 이 오케스트레이터는 각 참여 서비스에 실행할 로컬 트랜잭션이 무엇인지 알려줍니다.
- 오케스트레이터는 사가의 상태를 유지하고 보상 트랜잭션 실행을 포함한 다음 단계를 결정합니다.
- 장점: 전체 프로세스를 더 잘 제어하고 사가의 진행 상황을 모니터링하기 쉬우며 복잡한 워크플로를 관리하기 더 쉽습니다.
- 단점: 오케스트레이터는 신중하게 설계되지 않으면 단일 실패 지점 또는 병목 현상이 될 수 있습니다. 추가 관리 서비스를 도입하게 됩니다.
실제 구현 예시 (오케스트레이션 기반)
Python과 유사한 의사 코드를 사용하여 오케스트레이션 기반 사가로 주문 처리 예시를 단순화하여 설명해 보겠습니다.
주문 서비스
, 재고 서비스
, 결제 서비스
의 세 가지 서비스가 있습니다.
시나리오: 고객이 주문
- 주문 서비스:
PENDING
상태로 새 주문을 생성합니다. - 재고 서비스: 요청된 품목을 예약합니다.
- 결제 서비스: 결제를 처리합니다.
- 주문 서비스: 주문을
APPROVED
또는REJECTED
로 업데이트합니다.
사가 오케스트레이터
# Kafka 또는 RabbitMQ와 같은 메시지 큐를 통신용으로 가정 class OrderCreationOrchestrator: def __init__(self, order_id): self.order_id = order_id self.state = "INITIATED" self.context = {"order_id": order_id, "items": [], "total_amount": 0.0} # 서비스 간에 필요한 주문 세부 정보 저장 def start_saga(self, order_details): print(f"Orchestrator: Starting Saga for Order {self.order_id}") self.context.update(order_details) self.state = "CREATE_ORDER" self._send_command_to_order_service(self.context) def _send_command_to_order_service(self, payload): # 주문 서비스에 'create_order' 명령 시뮬레이션 print(f"Orchestrator: Sending 'create_order' command to Order Service with data: {payload}") # 실제 시스템에서는 메시지 큐에 메시지를 게시합니다. self._simulate_order_service_response(payload) def _send_command_to_inventory_service(self, payload): # 재고 서비스에 'reserve_inventory' 명령 시뮬레이션 print(f"Orchestrator: Sending 'reserve_inventory' command to Inventory Service with data: {payload}") self._simulate_inventory_service_response(payload) def _send_command_to_payment_service(self, payload): # 결제 서비스에 'process_payment' 명령 시뮬레이션 print(f"Orchestrator: Sending 'process_payment' command to Payment Service with data: {payload}") self._simulate_payment_service_response(payload) def _simulate_order_service_response(self, payload): # 주문 서비스가 주문을 생성하고 이벤트를 게시하는 것을 시뮬레이션 print(f"Order Service: Order {payload['order_id']} created in PENDING state.") # 성공 시, 오케스트레이터가 진행합니다. self.handle_event("order_created", {"order_id": payload["order_id"], "items": payload["items"]}) def _simulate_inventory_service_response(self, payload, success=True): if success: print(f"Inventory Service: Items {payload['items']} reserved for Order {payload['order_id']}.") self.handle_event("inventory_reserved", {"order_id": payload["order_id"]}) else: print(f"Inventory Service: Failed to reserve inventory for Order {payload['order_id']}!") self.handle_event("inventory_reservation_failed", {"order_id": payload["order_id"]}) def _simulate_payment_service_response(self, payload, success=True): if success: print(f"Payment Service: Payment processed for Order {payload['order_id']} with amount {payload['total_amount']}.") self.handle_event("payment_processed", {"order_id": payload["order_id"]}) else: print(f"Payment Service: Failed to process payment for Order {payload['order_id']}!") self.handle_event("payment_failed", {"order_id": payload["order_id"]}) def _send_compensate_order_service(self, payload): print(f"Order Service: Compensating - Canceling Order {payload['order_id']}.") # 실제 시스템에서는 주문 상태를 'CANCELED'로 변경합니다. pass def _send_compensate_inventory_service(self, payload): print(f"Inventory Service: Compensating - Unreserving items for Order {payload['order_id']}.") # 실제 시스템에서는 예약된 품목을 해제합니다. pass def _send_compensate_payment_service(self, payload): print(f"Payment Service: Compensating - Refunding payment for Order {payload['order_id']}.") # 실제 시스템에서는 환불을 시작합니다. pass def handle_event(self, event_type, event_data): print(f"Orchestrator: Received event: {event_type} for Order {self.order_id}. Current state: {self.state}") if event_type == "order_created" and self.state == "CREATE_ORDER": self.state = "RESERVE_INVENTORY" self._send_command_to_inventory_service(self.context) elif event_type == "inventory_reserved" and self.state == "RESERVE_INVENTORY": self.state = "PROCESS_PAYMENT" self._send_command_to_payment_service(self.context) elif event_type == "payment_processed" and self.state == "PROCESS_PAYMENT": self.state = "SAGA_COMPLETED" print(f"Orchestrator: Saga for Order {self.order_id} completed successfully!") # 주문 서비스에서 주문 최종화 (예: 상태를 'APPROVED'로 설정) print(f"Order Service: Order {self.order_id} status updated to APPROVED.") elif event_type == "inventory_reservation_failed": self.state = "SAGA_FAILED_INVENTORY" print(f"Orchestrator: Inventory reservation failed. Initiating compensation.") self._send_compensate_order_service(self.context) # 주문 생성 보상 print(f"Orchestrator: Saga for Order {self.order_id} failed and compensated.") elif event_type == "payment_failed": self.state = "SAGA_FAILED_PAYMENT" print(f"Orchestrator: Payment failed. Initiating compensation.") self._send_compensate_inventory_service(self.context) # 재고 예약 보상 self._send_compensate_order_service(self.context) # 주문 생성 보상 print(f"Orchestrator: Saga for Order {self.order_id} failed and compensated.") # --- 사가 실행 --- if __name__ == "__main__": order_id = "ORDER-XYZ-123" order_details = { "customer_id": "CUST-001", "items": [{"item_id": "ITEM-A", "quantity": 2}, {"item_id": "ITEM-B", "quantity": 1}], "total_amount": 150.00 } orchestrator = OrderCreationOrchestrator(order_id) orchestrator.start_saga(order_details) print("\n--- 실패 시나리오 시뮬레이션 (예: 결제 실패) ---") orchestrator_failure = OrderCreationOrchestrator("ORDER-XYZ-FAIL") order_details_failure = { "customer_id": "CUST-002", "items": [{"item_id": "ITEM-C", "quantity": 1}], "total_amount": 50.00 } # 실패 및 보상을 시연하기 위해 수동으로 이벤트 시뮬레이션 orchestrator_failure.start_saga(order_details_failure) # 이 시점에서 order_created 이벤트는 정상적으로 처리됩니다. # 그런 다음 inventory_reserved 이벤트도 정상적으로 처리됩니다. # 이제 결제 실패를 직접 시뮬레이션합니다. orchestrator_failure.handle_event("payment_failed", {"order_id": "ORDER-XYZ-FAIL", "reason": "Insufficient funds"})
코드 예시 설명:
OrderCreationOrchestrator
: 사가 오케스트레이터 역할을 합니다. 전체 트랜잭션의 상태(self.state
)와 후속 단계를 위한 컨텍스트(self.context
)를 유지합니다.start_saga
:Order Service
에 첫 번째 명령을 전송하여 워크플로를 시작합니다._send_command_to_X_service
: 다양한 마이크로서비스로 메시지(명령)를 전송하는 것을 시뮬레이션합니다. 실제 애플리케이션에서는 메시지 브로커(예: Kafka, RabbitMQ)에 메시지를 게시하는 것을 포함합니다._simulate_X_service_response
: 개별 마이크로서비스가 로컬 트랜잭션을 완료한 후의 응답을 시뮬레이션합니다. 이것들은 오케스트레이터가 처리하는 이벤트를 발생시킵니다.handle_event
: 오케스트레이터의 핵심 로직입니다. 수신된 이벤트와 현재 사가 상태를 기반으로 다음 액션을 결정합니다. 즉, 다음 단계로 진행할지, 사가를 완료할지, 또는 보상 워크플로를 시작할지 결정합니다.- 보상 로직:
payment_failed
와 같은 이벤트가 수신되면handle_event
메서드는_send_compensate_X_service
호출 시퀀스를 트리거합니다. 이 호출은 이전에 성공했던 서비스에 해당 작업을 의미론적으로 취소하도록 지시합니다.
적용 시나리오
사가 패턴은 특히 마이크로서비스 아키텍처에서 다음과 같은 시나리오에 적합합니다.
- 비즈니스 프로세스가 여러 서비스와 데이터베이스에 걸쳐 있는 경우: 전자 상거래 주문 이행, 호텔 예약 시스템, 항공편 예약.
- 모든 서비스에 걸쳐 실시간으로 강력한 일관성이 엄격하게 요구되지는 않지만, 최종 일관성과 원자성 보장이 중요할 때: 시스템이 최종적으로 일관된 상태에 도달하거나 완전히 롤백되는 동안 시스템이 일시적으로 불일치해도 허용됩니다.
- 전통적인 분산 트랜잭션(XA 트랜잭션) 솔루션이 실행 불가능하거나 너무 많은 오버헤드를 초래할 때: XA 트랜잭션은 종종 매우 밀접하게 결합되어 있으며 매우 분산된 자율 마이크로서비스 환경에서 성능이 저하됩니다.
- 서비스가 느슨하게 결합된 상태로 유지되어야 할 때: 사가 패턴은 서비스가 데이터베이스 트랜잭션을 직접 조정할 필요 없이 독립적으로 발전할 수 있도록 합니다.
주요 고려 사항
- 멱등성: 모든 명령과 보상 트랜잭션은 멱등해야 합니다. 동일한 명령을 여러 번 보내는 것은 한 번 보내는 것과 동일한 효과를 가져야 합니다. 이는 메시지 기반 시스템의 복원력에 중요합니다.
- 모니터링 및 관찰 가능성: 사가는 오래 실행될 수 있으며 많은 단계를 포함합니다. 강력한 모니터링, 로깅 및 추적이 사가의 상태를 이해하고 실패를 진단하는 데 필수적입니다.
- 오류 처리 및 재시도: 서비스가 일시적인 오류를 어떻게 처리할지 고려해야 합니다. 오케스트레이터 또는 개별 서비스에서 재시도 메커니즘이 필요할 수 있습니다.
- 상태 관리: 오케스트레이터는 충돌로부터 복구하고 실행을 계속하려면 사가 상태를 유지해야 합니다.
- 타임아웃: 서비스 응답 실패로 사가가 영원히 중단되는 것을 방지하기 위해 사가의 각 단계에 대한 타임아웃을 구현하는 것이 중요합니다.
결론
사가 패턴은 복잡한 마이크로서비스 아키텍처에서 분산 트랜잭션을 관리하기 위한 실용적이고 강력한 솔루션을 제공합니다.
원자 연산을 일련의 로컬 독립 트랜잭션으로 분해하고 강력한 보상 메커니즘을 제공함으로써, 마이크로서비스의 이점을 희생하지 않고도 분산 서비스 전반에 걸쳐 데이터 일관성을 보장합니다.
조정 및 오류 처리와 관련된 자체 복잡성을 도입하지만, 복원력 있고 확장 가능한 방식으로 트랜잭션 무결성을 유지하는 능력은 사가를 강력한 분산 시스템 구축에 필수적인 도구로 만듭니다.
이는 지능적인 설계가 분산 컴퓨팅의 본질적인 과제를 어떻게 극복하여 최종 일관성과 운영 보장을 현대 애플리케이션 개발의 전면에 가져올 수 있는지를 보여주는 증거입니다.