분산 트랜잭션 관리 방법(2) : SAGA Pattern

김현지·2024년 9월 13일
post-thumbnail

Microservice Architecture에서 분산 트랜잭션을 관리하기 위한 방법에는 이전 글에서 소개한 Two-phase Commit 외에도 SAGA Pattern이 있다.

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 State Machine

SAGA Pattern에서는 로컬 트랜잭션이 다음 로컬 트랜잭션이나 보상 트랜잭션을 트리거하기 때문에 State Machine으로 모델링하여 전체 Saga의 흐름을 관리하기에 적합하다. 주문 시스템을 예시로 들어 State Machine을 그려보면 다음과 같다.

초록색은 보상 가능 트랜잭션, 노란색은 피봇 트랜잭션, 파란색은 재시도 가능 트랜잭션, 빨간색은 보상 트랜잭션(이자 재시도 트랜잭션)이다. 파란색 화살표는 로컬 트랜잭션이 성공하는 경우, 빨간색 화살표는 실패하는 경우, 검은색 화살표는 반드시 실행되는 경우이다.

출처 : 나

  1. Saga가 시작되면 보상 가능 트랜잭션이 시작된다. 각 단계는 성공하면 로컬 트랜잭션을 커밋한 뒤 다음 단계로 넘어가고, 실패하면 로컬 트랜잭션을 롤백한 뒤 이전에 성공한 보상 가능 트랜잭션의 보상 트랜잭션을 실행한다. 여기서 주문 생성 단계는 이전에 실행된 보상 가능 트랜잭션이 없기 때문에 실패하면 로컬 트랜잭션을 롤백한 뒤 바로 Saga가 실패 상태가 된다. 재고 -1 변경 단계가 성공하면 다음 단계로 넘어가고, 실패하면 주문 생성 단계의 보상 트랜잭션인 주문 취소를 실행한다.
  2. 피봇 트랜잭션결제 단계는 보상 가능 트랜잭션과 동일한 로직으로 실행된다. 하지만 결제 단계에는 보상 트랜잭션이 존재하지 않기 때문에 여기서 피봇 트랜잭션은 보상 가능 트랜잭션도, 재시도 가능 트랜잭션도 아니다.
  3. 피봇 트랜잭션이 성공하면 재시도 가능 트랜잭션주문 확인 메일 전송 단계로 넘어가는데, 주문 확인 메일을 전송하는 것에 실패하더라도 계속 재시도하여 반드시 이 단계를 성공시켜야 한다. 모든 재시도 가능 트랜잭션이 완료되면 Saga는 성공 상태로 넘어간다.
  4. 보상 가능 트랜잭션이나 피봇 트랜잭션이 실패하면 보상 트랜잭션을 실행하는데, 보상 트랜잭션이 실행된다는 것은 전체 Saga가 실패했다는 의미이므로 보상 트랜잭션 또한 실패하더라도 계속 재시도하여 반드시 성공시켜야 한다. 모든 보상 트랜잭션이 완료되면 Saga는 실패 상태로 넘어간다.

구현 방식

SAGA Pattern은 SAGA의 흐름을 제어하는 방식에 따라 두 가지로 나뉜다.

Choreography-based SAGA

Choreography-based SAGA는 마이크로 서비스끼리 직접 통신하여 Saga를 진행시키는 방식이다. (통신에는 동기 방식과 비동기 방식을 모두 사용할 수 있다.)

ex) Product 서비스에서 재고 -1 변경 단계가 성공하면 Payment 서비스와 통신하여 다음 단계인 결제 단계를 트리거하고, 실패하면 Order 서비스와 통신하여 보상 트랜잭션인 주문 취소 단계를 트리거한다.

  • 장점
    • 중앙 코디네이터가 없기 때문에 구현이 비교적 간단하다.
  • 단점
    • 전체 Saga의 흐름을 파악하기 어렵다. → 서비스의 확장과 디버깅이 어렵다.
    • 마이크로 서비스들이 서로 의존성을 가지기 때문에 시스템의 결합도가 높아진다.
    • 마이크로 서비스 사이에서 순환 의존성이 발생할 가능성이 있다.

순환 의존성(Cyclic Dependency)

A가 B에 의존하고, B가 C에 의존하고, C가 A에 의존하여 결국 서로 순환적으로 의존하게 되는 것

Orchestration-based SAGA

Orchestration-based SAGA는 Saga의 흐름을 제어하는 중앙 코디네이터인 오케스트레이터(Orchestrator)가 마이크로 서비스들과 통신하여 Saga를 진행시키는 방식이다. 각 마이크로 서비스는 오케스트레이터의 명령을 받아 실행되고, 그 결과를 다시 오케스트레이터에게 전달한다.

ex) 오케스트레이터가 Product 서비스의 재고 -1 변경 단계를 트리거하고, Product 서비스로부터 받은 결과에 따라 Payment 서비스의 결제 단계를 트리거하거나 Order 서비스의 주문 취소 단계를 트리거한다.

  • 장점
    • 오케스트레이터가 Saga를 제어하기 때문에 전체 Saga의 흐름을 쉽게 파악할 수 있다. → 디버깅이 쉽다.
    • 마이크로 서비스가 서로 어떤 일을 하는지 알 필요가 없고 서로 의존성을 가지지 않아 시스템의 결합도가 낮아진다.
    • 오케스트레이터는 Saga 로직만 처리하고, 마이크로 서비스는 도메인 로직만 처리하도록 분리하여 로직이 단순해진다.
  • 단점
    • 오케스트레이터가 단일 장애 지점이 되기 때문에, 오케스트레이터에 장애가 발생하면 전체 시스템에 문제가 발생할 수 있다.
    • 오케스트레이터에 많은 트래픽이 몰릴 수 있다.

의존성과 결합도, 응집도

  • 의존성(Dependency) : A 모듈이 B 모듈을 사용할 때, A는 B에 의존한다고 말한다. (A→B) 이때, B에 변경사항이 생기면 A에도 변경사항이 생길 수도 있다. 의존성이 높아질수록 하나의 변경이 다른 부분에 미치는 영향이 커진다.

  • 결합도(Coupling) : 두 모듈 간의 의존하는 정도. 결합도가 낮을수록 서로 독립적으로 동작하고, 좋은 시스템이라고 할 수 있다.

  • 응집도(Cohesion) : 한 모듈 내의 구성 요소들이 연관되어있는 정도. 응집도가 높을수록 모듈이 하나의 명확한 역할을 잘 수행하고, 좋은 시스템이라고 할 수 있다.


의존성과 결합도의 관계 : 두 모듈 간의 의존성이 많아질수록 결합도가 높아진다.

결합도와 응집도의 관계 : 결합도와 응집도는 상호 보완적인 관계로, 결합도가 낮고 응집도가 높아질수록 좋은 시스템이다.


어떻게 ACID를 보장하는가?

  • Atomicity : Saga가 종료되고 나면 모든 로컬 트랜잭션이 commit 또는 rollback 된다.
  • Consistency : 각 로컬 트랜잭션에서 제약조건 등에 의해 일관성이 보장된다.
  • Isolation : 로컬 트랜잭션은 데이터베이스에 커밋되고 다음 단계로 넘어가기 때문에 다른 분산 트랜잭션에서 변경된 데이터에 접근할 수 있어 격리성이 보장되지 않는다.
  • Durability : 각 로컬 트랜잭션은 Redo log와 Undo log를 기록하여 영속성을 보장한다.

Isolation을 보장하기 위한 방법

Semantic Lock

분산 트랜잭션의 격리성을 보장하는 방법 중의 하나는 어플리케이션 레벨의 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을 사용하는 흐름은 아래와 같다.

  1. 보상 가능 트랜잭션에서 레코드를 생성하거나 변경하면 Saga 진행 중이라는 의미의 플래그를 설정한다.
  2. Saga가 종료되기 전에 Saga 진행 중이 아님이라는 의미의 플래그로 다시 바꿔준다. 성공 시에는 재시도 가능 트랜잭션이 플래그를 바꾸고, 실패 시에는 보상 트랜잭션이 플래그를 바꾼다.

재시도 가능 트랜잭션의 멱등성

멱등성(Idempotency)이란 동일한 연산을 여러 번 수행해도 결과가 달라지지 않는 성질을 말한다. 예를 들어, a = a + 3이라는 연산은 멱등하지 않지만, a = 3이라는 연산은 멱등하다. 재시도 가능 트랜잭션은 실패해도 계속 재시도하여 반드시 성공시켜야 하기 때문에 멱등성을 보장하도록 구현해야 한다.

멱등성을 보장하기 위한 방법에는 여러가지가 있는데, 로컬 트랜잭션에 UUID와 같은 고유한 멱등키를 부여하여 해당 멱등키로 이미 처리되었는지 확인하는 로직을 추가하여 멱등성을 보장할 수 있다.


참고 자료

profile
백엔드 개발자😎

0개의 댓글