
Microservice Architecture에서 분산 트랜잭션을 관리하기 위한 방법에는 이전 글에서 소개한 Two-phase Commit 외에도 SAGA Pattern이 있다.
‘Saga’는 아이슬란드어로 ‘이야기’라는 뜻이다.
SAGA Pattern은 일련의 로컬 트랜잭션으로 이루어진 분산 트랜잭션의 일관성을 관리하는 방법이다. 로컬 트랜잭션이 성공하면 다음 단계의 로컬 트랜잭션을 실행하고, 로컬 트랜잭션이 실패하면 이전에 실행한 로컬 트랜잭션을 롤백하는 보상 트랜잭션(Compensate Transaction)을 실행한다. SAGA Pattern은 Saga 단계가 모두 완료되기 전까지는 일관성이 유지되지 않지만, 전체 Saga가 성공하거나 실패하여 완료된 후에는 일관성을 유지하는 결과적 일관성을 보장한다.
결과적 일관성(Eventual Consistency)
분산 환경에서 고가용성을 위해 모든 노드에서 항상 즉각적으로 일관된 상태를 유지하는 것을 포기하고, 시간이 지나면 언젠가 반드시 일관된 상태에 도달하는 일관성 모델. 여기서 일관성이란 모든 노드에서 같은 시간에 같은 데이터를 볼 수 있는 것을 말함. (분산 시스템에서의 일관성과 ACID 트랜잭션의 일관성은 정의가 다름)
가용성(Availability)
서버, 네트워크, 프로그램 등의 시스템이 정상적으로 사용 가능한 정도
Two-phase Commit과는 달리 SAGA Pattern에서는 각 로컬 트랜잭션이 실행을 마치면 데이터베이스에 커밋한 후에 다음 단계의 로컬 트랜잭션으로 넘어간다. 그래서 Saga가 실패했을 경우 이미 커밋된 로컬 트랜잭션의 실행을 취소할 보상 트랜잭션을 실행하여 직접 롤백을 해주어야 한다. 보상 트랜잭션은 데이터베이스가 제공하는 롤백 기능을 어플리케이션 레벨에서 구현한 것이다.
피봇 트랜잭션(Pivot Transaction)은 전체 Saga의 성공과 실패를 결정하는 트랜잭션이다.
피봇 트랜잭션 이전의 트랜잭션은 Saga의 결과에 따라서 롤백할 가능성이 존재하기 때문에 보상 가능 트랜잭션(Compensatable Transaction)이라고 부른다. 물론 피봇 트랜잭션 이전이라도 조회만 하는 트랜잭션의 경우에는 보상 트랜잭션이 필요 없다.
피봇 트랜잭션 이후의 트랜잭션은 Saga가 성공했으므로 반드시 실행되어야하기 때문에 재시도 가능 트랜잭션(Retriable Transaction)이라고 부른다. 보상 트랜잭션 또한 Saga가 실패했으므로 반드시 실행되어야하기 때문에 재시도 가능 트랜잭션이다.
피봇 트랜잭션은 일반적으로 보상 가능 트랜잭션과 재시도 가능 트랜잭션 모두 아니지만, 보상 가능 트랜잭션의 마지막 단계나 재시도 가능 트랜잭션의 첫 단계로 볼 수도 있다.
SAGA Pattern에서는 로컬 트랜잭션이 다음 로컬 트랜잭션이나 보상 트랜잭션을 트리거하기 때문에 State Machine으로 모델링하여 전체 Saga의 흐름을 관리하기에 적합하다. 주문 시스템을 예시로 들어 State Machine을 그려보면 다음과 같다.
초록색은 보상 가능 트랜잭션, 노란색은 피봇 트랜잭션, 파란색은 재시도 가능 트랜잭션, 빨간색은 보상 트랜잭션(이자 재시도 트랜잭션)이다. 파란색 화살표는 로컬 트랜잭션이 성공하는 경우, 빨간색 화살표는 실패하는 경우, 검은색 화살표는 반드시 실행되는 경우이다.

주문 생성 단계는 이전에 실행된 보상 가능 트랜잭션이 없기 때문에 실패하면 로컬 트랜잭션을 롤백한 뒤 바로 Saga가 실패 상태가 된다. 재고 -1 변경 단계가 성공하면 다음 단계로 넘어가고, 실패하면 주문 생성 단계의 보상 트랜잭션인 주문 취소를 실행한다.결제 단계는 보상 가능 트랜잭션과 동일한 로직으로 실행된다. 하지만 결제 단계에는 보상 트랜잭션이 존재하지 않기 때문에 여기서 피봇 트랜잭션은 보상 가능 트랜잭션도, 재시도 가능 트랜잭션도 아니다.주문 확인 메일 전송 단계로 넘어가는데, 주문 확인 메일을 전송하는 것에 실패하더라도 계속 재시도하여 반드시 이 단계를 성공시켜야 한다. 모든 재시도 가능 트랜잭션이 완료되면 Saga는 성공 상태로 넘어간다.실패 상태로 넘어간다.SAGA Pattern은 SAGA의 흐름을 제어하는 방식에 따라 두 가지로 나뉜다.
Choreography-based SAGA는 마이크로 서비스끼리 직접 통신하여 Saga를 진행시키는 방식이다. (통신에는 동기 방식과 비동기 방식을 모두 사용할 수 있다.)
ex) Product 서비스에서 재고 -1 변경 단계가 성공하면 Payment 서비스와 통신하여 다음 단계인 결제 단계를 트리거하고, 실패하면 Order 서비스와 통신하여 보상 트랜잭션인 주문 취소 단계를 트리거한다.
순환 의존성(Cyclic Dependency)
A가 B에 의존하고, B가 C에 의존하고, C가 A에 의존하여 결국 서로 순환적으로 의존하게 되는 것
Orchestration-based SAGA는 Saga의 흐름을 제어하는 중앙 코디네이터인 오케스트레이터(Orchestrator)가 마이크로 서비스들과 통신하여 Saga를 진행시키는 방식이다. 각 마이크로 서비스는 오케스트레이터의 명령을 받아 실행되고, 그 결과를 다시 오케스트레이터에게 전달한다.
ex) 오케스트레이터가 Product 서비스의 재고 -1 변경 단계를 트리거하고, Product 서비스로부터 받은 결과에 따라 Payment 서비스의 결제 단계를 트리거하거나 Order 서비스의 주문 취소 단계를 트리거한다.
의존성과 결합도, 응집도
의존성(Dependency) : A 모듈이 B 모듈을 사용할 때, A는 B에 의존한다고 말한다. (A→B) 이때, B에 변경사항이 생기면 A에도 변경사항이 생길 수도 있다. 의존성이 높아질수록 하나의 변경이 다른 부분에 미치는 영향이 커진다.
결합도(Coupling) : 두 모듈 간의 의존하는 정도. 결합도가 낮을수록 서로 독립적으로 동작하고, 좋은 시스템이라고 할 수 있다.
응집도(Cohesion) : 한 모듈 내의 구성 요소들이 연관되어있는 정도. 응집도가 높을수록 모듈이 하나의 명확한 역할을 잘 수행하고, 좋은 시스템이라고 할 수 있다.
의존성과 결합도의 관계 : 두 모듈 간의 의존성이 많아질수록 결합도가 높아진다.
결합도와 응집도의 관계 : 결합도와 응집도는 상호 보완적인 관계로, 결합도가 낮고 응집도가 높아질수록 좋은 시스템이다.
분산 트랜잭션의 격리성을 보장하는 방법 중의 하나는 어플리케이션 레벨의 Lock 기법인 Semantic Lock이다. 이를 위해 모든 테이블에는 Semantic Lock을 위한 플래그가 필요하다. 플래그가 설정된 레코드는 Saga 단계가 진행 중이라는 의미이므로 다른 Saga에서 접근하려고 할 때 처리를 해주어야 한다.
Semantic Lock의 플래그에서 가질 수 있는 상태는 Saga 진행 중, Saga 진행 중이 아님 뿐만 아니라 레코드가 가지는 상태와 합쳐져 다양한 상태를 정의할 수 있다. 예를 들어, Order가 APPROVED, CANCELED, COMPLETED의 상태를 갖고 있다고 하면, 플래그를 추가하여 APPROVED_PENDING, APPROVED, CANCELED_PENDING, CANCELED, COMPLETED_PENDING, COMPLETED의 상태를 가질 수 있다. 물론 비즈니스 로직을 정의하는 과정에서 더 다양한 플래그를 정의할 수도 있다.
Saga에서 Semantic Lock을 사용하는 흐름은 아래와 같다.
Saga 진행 중이라는 의미의 플래그를 설정한다.Saga 진행 중이 아님이라는 의미의 플래그로 다시 바꿔준다. 성공 시에는 재시도 가능 트랜잭션이 플래그를 바꾸고, 실패 시에는 보상 트랜잭션이 플래그를 바꾼다.멱등성(Idempotency)이란 동일한 연산을 여러 번 수행해도 결과가 달라지지 않는 성질을 말한다. 예를 들어, a = a + 3이라는 연산은 멱등하지 않지만, a = 3이라는 연산은 멱등하다. 재시도 가능 트랜잭션은 실패해도 계속 재시도하여 반드시 성공시켜야 하기 때문에 멱등성을 보장하도록 구현해야 한다.
멱등성을 보장하기 위한 방법에는 여러가지가 있는데, 로컬 트랜잭션에 UUID와 같은 고유한 멱등키를 부여하여 해당 멱등키로 이미 처리되었는지 확인하는 로직을 추가하여 멱등성을 보장할 수 있다.
참고 자료