[Saga 패턴] 마이크로서비스에서 Saga 패턴이란?
회사에서 Kafka 스터디에 참여하면서, 알아보았던 Saga 패턴에 대해서 정리한다. Saga 패턴에 대해 처음 알게 되었는데, MSA 구조를 고려한다면 꼭 알고 있어야 하는 구조이다. Saga 패턴 이전에 어떠한 패턴으로 사용되었는지, 기존 패턴의 문제는 무엇인지 알아본다.
1. Distributed Transaction 소개
1.1 Monolithic에서의 Transaction 처리
모놀리식 아키텍처에서는 사용자가 요청을 보내면, 단일 데이터베이스에서 트랜잭션을 생성하여 상태 확인 및 요청 처리가 진행된다. 만약 이 과정에서 실패한다면, 해당 트랜잭션은 롤백된다.
1.2 Microservice에서의 Transaction 처리
Microservice 기반 분산된 아키텍처에서는 각 서비스마다 별도의 데이터베이스를 구성한다. 사용자가 요청을 보내면, 1개 이상의 서비스가 호출될 있고 각 데이터베이스에 변경 사항을 처리하게 된다.
Microservice 기반 분산된 아키텍처에서는 각 서비스가 서로 다른 데이터베이스를 가지고 있어 단순하게 ACID(Atomic, Consistent, Isolated, Durable) 트랜잭션을 유지하기 어렵다. 아래 문제를 포함하여 Microservice 기반 애플리케이션을 설계/구축하는 동안 고려되어야 한다.
- 만약 InventoryMicroservice가 실패하면, OrderMicroservice는 어떻게 처리할지?
- 서로 다른 서비스가 동일한 데이터에 접근할 때 요청은 어떻게 처리할지?
(ACID 속성)
- Atomicity : 모든 작업이 성공하거나 모두 실패한다. 트랜잭션은 부분적으로 실행되다가 중단되지 않도록 보장한다.
- Consistency : 트랜잭션 실행 후 성공적으로 완료하면 일관성 있는 데이터베이스 상태를 유지한다. 데이터의 상태가 트랜잭션 전후의 모든 제약 조건을 준수한다.
- Isolation : 동시 트랜잭션이 발생하여도 서로 방해하거나 영향을 미치지 않는다.
- Durability : 트랜잭션이 성공적으로 완료되면 변경 결과는 저장되도록 보장한다.
2. Solution & Pattern (2PC, Saga)
2.1 2PC (Two-Phase Commit)
2PC는 분산 트랜잭션을 구현하는 데 널리 사용되는 패턴이며, Prepare 단계와 Commit 단계로 구성된다.
- Prepare Phase : 관련된 모든 서비스는 Commit을 준비하고, Transaction Coordinator에 트랜잭션을 시작할 준비가 되었음(Commit 할 준비가 되었음)을 알린다.
- Commit Phase : 이전 단계에서 트랜잭션을 시작할 준비가 되었다면, Coordinator는 Commit을 요청한다. 만약 서비스 하나라도 실패가 발생한다면, Coordinator는 관련된 모든 서비스에 해당 트랜잭션을 롤백하도록 요청한다.
다음은 Two-Phase Commit 방안을 적용한 예시이다.
- 사용자가 요청을 보내면, Transaction Coordinator는 모든 콘텍스트 정보를 기반으로 트랜잭션을 시작한다.
- OrderMicroservice에 Prepare 명령을 보낸다.
- 그 후 InventoryMicroservice에 Prepare 명령을 보낸다.
- Coordinator가 두 서비스가 트랜잭션 처리 준비가 되었음을 확인하면, Commit을 요청한다.
다음은 Two-Phase Commit 방안에서 실패한 경우 예시이다. 도중 서비스가 실패하면, Coordinator는 해당 트랜잭션을 중단 및 롤백한다. 아래 예시에서 InventoryMicroservice는 준비가 되었다고 응답했지만, OrderMicroservice는 실패하였고 이 경우 역시 Coordinator는 중단하여 InventoryMicroservice가 변경사항을 롤백하고 데이터베이스 Rock을 해제한다.
2PC 방안은 트랜잭션의 원자성을 보장한다. 모든 서비스는 성공하거나 또는 실패하여 기존 상태로 롤백 및 종료한다.
2PC는 분산 트랜잭션 처리를 위한 전통적인 방법이지만, Transaction Coordinator에 의존하여 모든 서비스에 대해 준비 상태를 확인하고 Rocking 상태로 변경하는 등 성능 측면에서 효율적인 방법이 아니다. 또한 NoSQL 등 일부 구현에도 지원하지 않아 제약이 있다.
2.2 Saga Pattern
Saga 패턴은 Saga 패턴은 각 서비스의 로컬 트랜잭션을 순차적으로 처리한다. 각 로컬 트랜잭션은 데이터베이스를 업데이트한 다음에 다음 로컬 트랜잭션을 트리거하는 메시지 또는 이벤트를 게시한다. 트랜잭션의 결과에 따라 롤백이 필요한 경우, 보상 트랜잭션을 진행한다. 보상 트랜잭션은 서비스에서 트랜잭션 처리에 실패할 경우, 그 서비스의 앞선 다른 서비스에서 처리된 트랜잭션을 되돌리게 하는 트랜잭션이다.
Saga 패턴은 크게 두 가지로 구분할 수 있다.
2.2.1 Choreography-based saga
각 로컬 트랜잭션이 다른 서비스의 로컬 트랜잭션을 이벤트 트리거 하는 방식이다. 중앙 집중된 지점 없이 이벤트를 교환하며, 모든 서비스가 메시지 브로커를 통해 이벤트를 Publish/Response 한다.
Choreography 접근 방식을 구현한 E-Commerce 시스템을 예로 들어본다.
- 사용자가 요청하면 OrderService는 Order 메서드를 수신하고 Pending 상태의 Order 이벤트를 생성한다.
- Order Created 이벤트를 발생시킨다.
- CustomerService의 이벤트 핸들러는 요청한 건에 대해 예약 시도한다.
- 시도한 결과는 이벤트로 발생시킨다.
- OrderService는 Order 이벤트를 승인하거나 거부한다.
(장점)
Single point of failure가 없다. 서비스가 많지 않은 간단한 워크플로우에 적합하다.
(단점)
새로운 서비스 추가가 필요할 시 서비스 간 연계성 파악이 중요하다. 서비스 간 이벤트를 주고받기 때문에 Cyclic Dependency 발생에 주의해야 한다.
(관련 프레임워크 : Axon Saga, Eclipse MicroProfile LRA, Eventuate Tram Saga, Seata)
2.2.2 Orchestration-based saga
Orchestrator가 중앙 집중식 컨트롤러 역할을 하고, 각 서비스에 실행할 트랜잭션을 알려주는 방법이다.
Orchestrator는 요청을 실행, 각 서비스의 상태를 확인, 실패에 대한 복구 처리한다.
Orchestration 접근 방식을 구현한 E-Commerce 시스템을 예로 들어본다.
- 사용자가 요청하면 OrderService가 수신하고, Orchestrator는 Create Order를 생성한다.
- Orchestrator는 Pending 상태의 Order를 생성한다.
- CustermerService에 Reserve Credit 명령을 보낸다.
- CustermerService는 Reserve Credit를 시도한다.
- 시도한 결과는 메시지 통해 응답한다.
- Orchestrator는 최종적으로 Order를 승인하거나 거부한다.
(장점)
비교적 많은 서비스가 있는 복잡한 워크플로우에 적합하다. 서비스 및 워크플로우의 제어가 필요한 경우 또한 적합하다.
(단점)
Orchestrator가 전체 워크플로우를 관리하여 실패 지점이 될 수 있다.
(관련 프레임워크 : Camunda, Apache Camel)
2.2.3 Saga 실패 시 처리 방안
Saga에서 설명하는 복구 방법으로는 Backward/Forward Recovery 두 가지가 있다.
- Saga Backward Recovery
Backward recovery는 트랜잭션 실패 시 롤백을 처리하는 방식이다. 아래는 Forward Recovery 시나리오 예시이다.
- abort-saga 명령으로 Backward recovery가 필요한 상황이다.
- T1 -> T2 트랜잭션이 진행되었고, T3이 진행 중이다.
- 현재 진행 중인 T3를 중지한다.
- T3는 롤백되고 해당하는 보상 트랜잭션을 실행한다.
- 보상 트랜잭션이 시작되고 종료될 때의 로그들은 기록되며, 보상 트랜잭션 종료된다.
- Saga Forward Recovery
실패가 발생한 지점에서 중지하지 않고 계속 처리해야 하는 경우가 있을 수 있다. (실패가 발생한 시점에 즉시 복구가 중요하지 않은 서비스, 실패한 내용이 성공한 부분보다 사소한 부분일 경우, 보상 트랜잭션을 구성하지 않고도 실패에 대해 알림으로 처리 가능한 경우 등). 이러한 경우 Forward Recovery를 적용할 수 있다.
Forward Recovery를 위해서는 신뢰 있는 코드와 save-point가 필요하다. 아래는 Forward Recovery 시나리오 예시이다.
- T1 -> T2 -> save-point command -> T3 -> T4 순으로 트랜잭션이 실행된다.
- T4 트랜잭션이 작동하는 동안 실패가 발생한다.
- 첫 번째로 역방향 복구를 save-point까지 진행한다.
- T3, T4 작동 코드가 사용 가능하다면, 트랜잭션을 다시 시작한다.
요약
Saga Pattern은 분산 트랜잭션 환경에서 서비스 간 결합도를 낮추고 보상 트랜잭션을 실행하도록 설계하는 패턴 중 하나이다. 데이터의 일관성을 보장하고, 보상 트랜잭션 구현을 위해 고려해야 한다. 보상 트랜잭션은 서비스와 서비스 간의 비즈니스 흐름을 고려하여 최소화하는 것이 바람직하다.
Saga Pattern을 구현하는 방안 선택의 기준은 명확히 제시된 것은 없지만, 일반적으로 프로젝트의 규모를 고려할 수 있다. 단순하게 Loosley Coupled 지향의 Choreographed Saga는 대규모 프로젝트에서 각 팀 간의 결합도를 낮추고 개발에 영향을 최소화하기 위해 선택하기 용이하다. 반면에 Orchestrated Saga는 단일팀 내에서 개발 가능한 경우 중앙 관리 방식으로 개발하는 것이 효과적일 수 있다. 또한 Tightly Coupled 지향의 트랜잭션 처리, Cyclic Dependency 발생이 있는 워크플로우에서 Saga Pattern 설계는 적합하지 않을 수도 있다.
References
[1] https://microservices.io/patterns/data/saga.html
[2] https://learn.microsoft.com/ko-kr/azure/architecture/reference-architectures/saga/saga
[3] https://www.baeldung.com/cs/saga-pattern-microservices
[4] https://ibm-cloud-architecture.github.io/refarch-eda/patterns/saga/
[5] https://levelup.gitconnected.com/modelling-saga-as-a-state-machine-cec381acc3ef