본문 바로가기
MSA

[MSA][Saga] 4장. Saga를 이용한 트랜잭션 관리

by Real Iron 2023. 12. 22.

4장. Saga를 이용한 트랜잭션 관리

2019. 12. 17. 0:01

 이웃추가
본문 기타 기능

아래는 크리스 리차드슨이 저술한 'Microservices Patterns with Examples in Java(자바 예제로 보는 마이크로서비스 패턴들)'라는 책 4장을 읽고 그 내용을 정리해 본 것이다. 상기 도서는 매닝 출판사의 온라인 사이트에서 e-Book으로 구매할 수 있다. (https://www.manning.com/books/microservices-patterns)


주요 내용
· 왜 분산 트랜잭션이 최신 애플리케이션에 적합하지 않은지 이해하기
· 마이크로서비스 아키텍처에서 사가(Saga) 패턴 이용하여 데이터 정합성을 유지하기
· 코레오그래피(Choreography)와 오케스트레이션(Orchestration) 구현하기
· 격리성 부족을 해결하기 위한 대응책(Countermeasure) 사용하기

마이크로서비스 아키텍처를 검토할 때 가장 큰 고민 중 하나는 복수의 서비스에 걸쳐 있는 트랜잭션을 어떻게 처리할 것인가이다. 하나의 서비스 안에서는 ACID(Atomicity, Consistency, Isolation, Durability) 트랜잭션을 사용할 수 있지만, 데이터 정합성을 유지하면서 여러 개의 서비스에 분산되어 있는 데이터들을 일괄 수정하려면 ACID 트랜잭션 대신 Saga를 이용해야 한다. 마이크로서비스 아키텍처에서 트랜잭션 관리를 위해 Saga를 이용해야 하는 이유와 이를 구현하는 2가지 방법인 코레오그래피(Choreography)와 오케스트레이션(Orchesgtration), 그리고 Saga를 이용할 때 발생하는 격리성 부족에 대해 알아보자.

1. 마이크로서비스 아키텍처에서 트랜잭션 관리하기

1.1 마이크로서비스 아키텍처의 트랜잭션 관리

하나의 데이터베이스를 사용하는 모노리틱 애플리케이션에서는 트랜잭션 관리가 간단하다. 예를 들어 소비자가 주문 가능한 상태인지, 주문 상세내역은 이상이 없는지를 검증하고 신용카드 결제를 처리한 후 데이터베이스에 주문을 저장하는 createOrder()라는 오퍼레이션을 생각해보자. 모노리틱 애플리케이션이라면 주문 검증을 위해 필요한 데이터가 모두 접근 가능하기 때문에 데이터 정합성을 보장하기 위해서는 ACID 트랜잭션을 사용하면 된다.

하지만 마이크로서비스 아키텍처에서는 주문 검증을 위해 필요한 데이터들이 여러 개의 서비스에 흩어져 있다. 즉, 상기의 createOrder() 오퍼레이션은 소비자 서비스에 있는 데이터를 읽어야하고 주문 서비스, 주방 서비스 그리고 회계 서비스에 있는 데이터를 업데이트해야 한다. 결국 마이크로서비스 아키텍처에서는 하나의 트랜잭션이 여러 개의 서비스에 흩어져 있는 데이터에 접근해야 하기 때문에 좀 더 정교한 트랜잭션 관리 메커니즘이 필요하다.

1.2 분산 트랜잭션의 문제점.

복수의 서비스, 데이터베이스, 메시지 브로커에 걸쳐있는 트랜잭션에서 데이터 정합성을 유지하기 위한 전통적인 방법은 분산 트랜잭션을 이용하는 것이다. 분산 트랜잭션은 통상 트랜잭션 안의 모든 작업이 일괄 커밋되거나 롤백되도록 보장하기 위해 2단계 커밋(two-phase commit, 2PC)을 사용한다.

그런데 분산 트랜잭션은 몇가지 문제점을 가지고 있다. 우선 MongoDB, Cassandra와 같은 NoSQL 데이터베이스들과 RabbitMQ나 Apache Kafka와 같은 최신 메시지 기술들이 분산 트랜잭션을 지원하지 않는다. 결국 분산 트랜잭션의 사용하려면 몇가지 최신 기술을 포기해야 한다.

또 다른 문제는 분산 트랜잭션이 가용성을 감소시킨다 점이다. 분산 트랜잭션을 커밋하기 위해서는 이에 참여하는 서비스들이 모두 가용해야 한다. 가용성은 트랜잭션에 참여하는 서비스 가용성의 곱에 비례하기 때문에 분산 트랜잭션에 참여하는 서비스가 늘어날 수록 가용성은 감소한다.

표면적으로는 분산 트랜잭션이 로컬 트랜잭션과 유사하기 때문에 상당한 유용한 것처럼 보이지만, 앞서 언급된 문제들로 인해 최신 애플리케이션에는 적합하지 못하다. 따라서 마이크로서비스 아키텍처에서는 트랜잭션의 데이터 정합성을 유지하기 위해 느슨한 연결과 비동기 서비스에 기반한 메커니즘인 Saga를 이용해야 한다.

1.3 Saga를 이용하여 데이터 정합성 유지하기

Saga는 순차적으로 실행되는 로컬 트랜잭션의 모음으로, 시스템 오퍼레이션에서 부터 시작하여 다수의 로컬 트랜잭션을 순차적으로 실행하는데 각 단계의 시작과 종료는 비동기 메시징을 통해 조율되며, 각각의 로컬 트랜잭션은 ACID 트랜잭션을 통해 데이터를 업데이트한다. 그리고 비동기 메시징은 Saga에 참여하는 모든 단계가 빠짐없이 실행되도록 보장해준다.

▶ 주문 생성 사가 (Create Order Saga)

아래는 Saga를 이용하여 createOrder() 오퍼레이션을 구현하는 주문 생성 사가(Create Order Saga)의 예제이다. 주문 생성에 대한 외부 요청으로부터 Saga의 첫 번째 로컬 트랜잭션이 시작되며, 이어지는 5개의 로컬 트랜잭션은 각각 선행 트랜잭션이 종료되면 시작된다. 즉, 하나의 로컬 트랜잭션이 종료되면 서비스는 메시지를 게시하는데 이 메시지는 Saga의 다음 단계가 시작되도록 만든다.

출처: Microservices Patterns with Examples in Java

1. 주문 서비스 – APPROVAL_PENDING 상태로 주문을 생성한다.

2. 소비자 서비스 – 소비자가 주문을 생성할 수 있는 지를 검증한다.

3. 주방 서비스 – 주문 상세 내역을 검증하고 CREATE_PENDING 상태로 Ticket을 생성한다.

4. 회계 서비스 – 소비자의 신용카드를 인증한다.

5. 주방 서비스 – Ticket의 상태를 AWAITING_ACCEPTANCE로 변경한다.

6. 주문 서비스 – 주문의 상태를 APPROVED로 변경한다.

 

여기서 비동기 메시징은 Saga 참여자들이 느슨하게 연결되도록 해주며 또한 메시지 수신자가 일시적으로 가용하지 않으면 메시지를 전달할 수 있을 때까지 버퍼링하기 때문에 Saga의 모든 단계가 수행되도록 보장해준다.

▶ 보상 트랙잭션(Compensating Transaction)

Saga를 사용하는 것이 겉으로 보기에는 간단해 보이지만 몇가지 어려운 점이 있다. 그 중 한가지는 오류가 발생했을 때 변경사항을 원복하는 것이다. 전통적인 ACID 트랜잭션에서는 오류가 발생하면 롤백 명령을 통해 지금까지의 변경사항을 모두 일괄적으로 원복하면 된다. 하지만 Saga에서는 이전 단계의 변경사항이 커밋으로 로컬 데이터베이스에 이미 반영되어서 롤백을 통해 자동으로 원복되지 않기 때문에 별도의 보상 트랜잭션을 실행해야 한다.

보상 트랜잭션(Compensating Transaction)은 원본 트랜잭션을 역으로 실행하여 취소 처리하는 트랜잭션으로, 예를 들어 주문 취소는 주문 생성에 대한 보상 트랜잭션이 된다. 만약 로컬 트랜잭션이 실패하면 Saga의 조율 메커니즘은 이전 단게에서 실행되었던 트랜잭션을 취소하는 보상 트랜잭션들을 실행한다. 아래의 표는 Create Order Saga의 각 단계에 대한 보상 트랜잭션을 보여준다.

출처: Microservices Patterns with Examples in Java

그런데 모든 단계가 다 보상 트랜잭션을 필요로 하는 것은 아니다. 예를 들어 VerifyConsumerDetails()와 같은 조회용 트랜잭션은 보상 트랜잭션을 필요로 하지 않으며, 또한 authorizedCreditCard()와 같이 항상 성공하는 트랜잭션도 그렇다. Create Order Saga에서 앞의 3단계는 실패할 수 있는 단계들로 보상 트랜잭션이 필요한 원복 가능 트랜잭션(compensatable transactions), 4번째 단계는 이후 단계들이 절대 실패하지 않는 단계들이어서 피봇 트랜잭션(pivot transaction), 그리고 나머지 단계들은 항상 성공하는 단계들이어서 재시도 트랜잭션(retriable transactions)이라고 부른다.

아래의 시나리오는 소비자의 신용카드 인증이 실패했을 경우 이전 단계의 트랜잭션들을 모두 원복하기 위해 보상 트랜잭션을 실행하는 사례이다.

1. 주문 서비스 – APPROVAL_PENDING 상태로 주문을 생성한다.

2. 소비자 서비스 – 소비자가 주문을 신청할 수 있는지 확인한다.

3. 주방 서비스 – 주문의 상세내역을 검증하고 CREATE_PENDING 상태로 티켓을 생성한다.

4. 회계 서비스 – 소비자의 신용카드를 인증하는 과정에서 실패한다.

5. 주방 서비스 – 티켓의 상태를 CREATE_REJECTED로 변경한다.

6. 주문 서비스 – 주문의 상태를 REJECTED로 변경한다.

상기 시나리오에서 1-3번 트랜잭션은 정상적으로 수행된 트랜잭션이고, 5-6번 트랜잭션이 주문 서비스와 주방 서비스가 만든 변경사항을 원복하기 위한 보상 트랜잭션이다.

2. Saga를 구현하는 방법

Saga는 Saga에 참여하는 각 단계들의 실행을 조율하는 로직들로 구성된다. 시스템 커맨드에 의해 Saga가 시작되면, Saga는 제일 먼저 실행될 참여자를 찾아서 해당 참여자의 로컬 트랜잭션을 실행하도록 지시하고, 해당 트랜잭션이 종료되면 다음 참여자를 찾아서 호출한다. 이 프로세스는 Saga에 참여하는 모든 단계가 실행될 때까지 계속되는데, 만약 중간에 로컬 트랜잭션이 실패하면 Saga는 보상 트랜잭션을 역순으로 실행한다.

Saga를 구현하는 방법에는 두 가지가 있다. 하나는 각 단계의 실행 및 순서 관리를 Saga 참여자들에게 분산하는 코레오그레피(Choreography)로 참여자들은 이벤트 메시지를 통해 상호 통신한다. 또 하나는 Saga의 조율 로직을 중앙에서 관리하는 오케이스트레이션(Orchestration)으로, 여기서는 Saga Orchestrator가 참여자들에게 어떤 오퍼레이션을 실행해야 할 지를 지시하는 커맨드 메시지를 전송한다.

2.1 코레오그레피 사가 (Choreography Saga)

코레오그레피 사가(Choreography Saga)는 Saga 참여자들이 무엇을 해야 하는지를 지시하는 중앙의 조율자를 가지고 있지 않다. 대신 Saga의 참여자들은 서로의 이벤트를 구독하고 이에 따라 반응하게 된다.

아래는 Create Order Saga를 코레오그래피 기반으로 구현한 모습으로, 주문 서비스부터 시작하여 각 참여자들은 로컬 데이터베이스를 업데이트하고 다음 참여자를 호출하기 위한 이벤트를 게시한다.

출처: Microservices Patterns with Examples in Java

① 주문 서비스는 APPROVAL_PENDING 상태로 주문을 생성하고 OrderCreated 이벤트를 게시한다.

② 소비자 서비스는 OrderCreated 이벤트에 반응하여 소비자가 주문을 생성할 수 있는 상태인지 확인한 후 ConsumerVerified 이벤트를 게시한다.

③ 주방 서비스는 OrderCreated 이벤트에 반응하여 주문을 검증하고 CREATE_PENDING 상태로 티켓을 생성한 후 TicketCreated 이벤트를 게시한다.

④ 회계 서비스는 OrderCreated 이벤트에 반응하여 CreditCardAuthorization을 PENDING 상태로 생성한다.

⑤ 회계 서비스는 TicketCreated 이벤트와 ConsumerVerified 이벤트에 반응하여 소비자의 신용카드로 비용을 결제하고 티켓의 상태를 AWAITING_ACCEPTANCE로 변경한다.

⑥ 주문 서비스는 CreateCardAuthorized 이벤트에 반응하여 주문의 상태를 APPROVED로 변경하고 OrderApproved 이벤트를 게시한다.

 

Create Order Saga는 Saga 참여자들이 주문을 거절하고 실패 이벤트를 게시하는 시나리오도 다를 수 있어야 한다. 예를 들어 소비자의 신용카드 인증이 실패하면 Saga는 이미 실행된 것을 원복하기 위한 보상 트랜잭션들을 실행해야 한다. 아래는 소비자의 신용카드 인증에 실패했을 때의 이벤트 흐름을 보여준다.

출처: Microservices Patterns with Examples in Java

① ~ ④ : 앞서 기술한 정상적인 이벤트 흐름과 동일

⑤ 회계 서비스는 TicketCreated 이벤트와 ConsumerVerified 이벤트에 반응하여 소비자의 신용카드로 비용의 결제를 시도하였으나 실패하여 CreditCardAuthorizationFailed 이벤트를 게시한다.

⑥ 주방 서비스는 CreditCardAuthorizationFailed 이벤트에 반응하여 Ticket의 상태를 REJECTED로 변경한다.

⑦ 주문 서비스는 CreditCardAuthorizationFailed 이벤트에 반영하여 주문의 상태를 REJECTED로 변경한다.

▶ 코레오그레피 구현 시 고려사항

코레오그레피를 구현할 때 고려해야 하는 이슈가 두가지 있다. 첫번째는 코레오그레피에서 각 단계들이 모두 데이터베이스를 업데이트하고 이벤트를 게시해야 하는데 데이터베이스 업데이트와 이벤트 게시는 원자적으로 실행되어야 한다는 것이다. 따라서 이를 위해 Saga 참여자들은 데이터베이스를 업데이트하고 데이터베이스 트랜잭션의 일부로 이벤트를 게시하는 트랜잭션 메시징 패턴을 이용해야 한다.

 

두 번째로 고려해야 하는 이슈는 Saga 참여자들이 자신이 수신한 메시지와 자신의 데이터를 매핑할 수 있어야 한다. 예를 들어 주문 서비스가 CreditCardAuthorized 이벤트를 수신했을 때 이에 대응되는 주문을 찾을 수 있어야 한다. 이를 위해 Saga 참여자는 연관 데이터를 찾을 수 있도록 연관 아이디(Correlation Id)를 포함하여 이벤트를 게시해야 한다.

 

▶ 코레오크레피의 장단점

코레오그레피는 비즈니스 객체를 생성, 수정 또는 삭제할 때 이벤트를 게시하면 된다는 단순함(Simplicity)과 참여자들이 서로의 이벤트를 구독할 뿐 서로에 대해 자세히 알 필요가 없기 때문에 느슨한 연결(Loose Coupling)이 보장된다는 장점이 있다.

반면 오케스트레이션과는 달리 전체 프로세스를 조율 관리하는 로직이 한 곳이 모여 있지 않고 여러 서비스에 분산되어 있기 때문에 어떻게 동작하는 지를 이해하는 것이 쉽지 않다. 또한 Saga 참여자들은 서로의 이벤트들을 구독하다 보니 때때로 순환적 의존성(Cyclic Dependency)이 만들어 지는 경우도 발생한다. 그리고 참여자들이 자신에게 영향을 주는 이벤트를 모두 구독해야 하기 때문에 강한 연결이 나타날 위험이 존재한다.

간단한 Saga는 코레오그레피에서도 잘 동작한다. 하지만 상기와 같은 단점으로 인해 복잡한 경우에는 통상 오케스트레이션을 사용하는 것이 더 나은 방법이다.

2.2 오케스트레이션 사가 (Orchestration Saga)

오케스트레이션 사가(Orchestration Saga)는 중앙의 오케스트레이터가 참여자들에게 무엇을 실행해야 하는 지를 지시하는 방식으로 오케스트레이터는 커맨드/비동기 응답 방식의 상호작용을 통해 참여자들과 통신한다. 즉, 오케스트레이터는 참여자에게 커맨드 메시지를 보내서 실행해야 하는 오퍼레이션을 알려주고, 참여자는 오퍼레이션의 실행 후에 오케스트레이터에게 응답 메시지를 보낸다. 그러면 오케스트레이터는 다음에 실행할 단계를 결정한다.

 

아래는 오케스트레이션을 이용하여 구현한 Create Order Saga의 모습이다. 여기서 오케스트레이터는 참여자들을 비동기 요청/응답 방식으로 호출하면서 통합 관리한다. 즉, 오케스트레이터는 프로세스를 계속 추적하면서 소비자 서비스와 같은 Saga 참여자들에게 커맨드 메시지를 전송하고, 응답 채널로부터 응답 메시지를 읽어서 다음에 수행할 단계를 결정한다.

출처: Microservices Patterns with Examples in Java

① Saga Orchestrator는 소비자 서비스에 VerifyConsumer 커맨드를 전달한다.

② 소비자 서비스는 ConsumerVerified라는 메시지로 응답한다.

③ Saga Orchestrator는 주방 서비스에 CreateTicket 커맨드를 전송한다.

④ 주방 서비스는 TicketCreated 메시지로 응답한다.

⑤ Saga Orchestrator는 회계 서비스에 AuthorizeCard 커맨드를 전송한다.

⑥ 회계 서비스는 CardAuthorized 메시지로 응답한다.

⑦ Saga Orchestrator는 주방 서비스에 ApproveTicket 커맨드를 전송한다.

⑧ Saga Orchestrator는 주문 서비스에 ApproveOrder 커맨드를 전송한다.

마지막 단계에서 오케스트레이터는 자신이 주문 서비스의 일부 컴포넌트임에도 불구하고 주문 서비스에 커맨드 메시지를 전송한다. 원칙적으로 Create Order Saga는 직접 주문을 수정할 수도 있다. 하지만 정합성을 유지하기 위하여 주문 서비스를 단지 하나의 참여자로 취급하는 것이다.

▶ 오케스트레이터를 상태 머신으로 모델링하기

Saga Orchestrator를 모델링하는 방법 중 하나는 이것을 상태 머신(state machine)으로 다루는 것이다. 하나의 상태 머신은 일련의 상태(State)와 이벤트들에 의해 촉진되는 상태 사이의 전이(Transition)로 구성된다. 상태 전이는 전이를 일으키는 행위, 즉 Saga 참여자에 대한 호출에 의해 발생하는데, 이는 Saga 참여자들이 수행하는 로컬 트랜잭션의 종료에 의해 촉발된다. 즉, 현재의 상태와 로컬 트랜잭션의 결과물에 따라 상태 전이와 수행할 행위가 결정된다. 아래는 Create Order Saga에 대한 상태 머신 모델을 보여준다.

출처: Microservices Patterns with Examples in Java

· Verifying Consumer : 최초의 상태로 Saga는 소비자가 주문 가능한 상태인지 확인될 때까지 대기한다.

· CreatingTicket : CreateTicket 커맨드에 대한 응답을 기다리고 있는 상태

· Authorizing Card : 회계 서비스가 소비자의 신용카드 인증을 기다리는 상태

· Order Approved : Saga가 성공적으로 완료되었음을 나타내는 최종 상태

· Order Rejected : 참여자들 중 하나에 의해 주문이 거절되었음을 나타내는 최종 상태

 

▶ 오케스트레이션의 장단점

오케스트레이션은 몇 가지 장점을 가지고 있다. 우선 오케스트레이션에서는 오케스트레이터만 참여자들을 호출하고 참여자들이 오케스트레이터를 호출하지 않기 때문에 순환적 의존성이 발생하지 않는다. 또한 Saga에 참여하는 서비스들이 오케스트레이터에 의해 호출될 API를 구현하기 때문에 상대적으로 느슨한 연결이 보장된다. 그리고 Saga의 조율 로직을 오케스트레이터에서 구현하기 때문에 도메인 객체의 비즈니스 로직은 좀 더 단순해지고 관심사의 분리(Separation of concerns)가 개선된다.

하지만 오케스트레이션에는 단점도 있다. 우선 과도하게 많은 비즈니스 로직이 오케스트레이터 안에 들어갈 위험이 있다. 이것은 결국 오케스트레이터가 서비스가 수행해야 하는 오퍼레이션을 일일이 지시하는 형태의 설계로 이어질 수 있다. 이것을 피하기 위해서는 오케스트레이터가 오퍼레이션의 순서에 대해서만 책임을 지고 그 어떤 비즈니스 로직도 포함하지 않도록 설계해야 한다.

3. 격리성의 부족 해결하기

ACID에서 I는 격리성(Isolation)으로 복수의 트랜잭션을 동시에 실행시킨 결과가 해당 트랜잭션들을 순차적으로 실행시킨 결과와 같도록 보장하는 것을 의미한다. 그런데 Saga를 사용하면 Saga에 참여하는 로컬 트랜잭션에 의해 만들어진 변경사항이 커밋과 동시에 외부에 노출되기 때문에 격리성 저하가 발생하고 이로 인해 두 가지 문제가 발생한다. 첫째 특정 Saga가 실행되는 동안 Saga에 의해 수정된 데이터가 다른 Saga에 의해 변경할 수 있다. 둘째 특정 Saga가 변경사항을 업데이트하는 동안에 다른 Saga에서 해당 데이터를 읽어 갈수도 있다.

결국 Saga를 사용하면 격리성의 부족으로 인해 데이터의 부정합이 발생할 수 있는데, 이를 데이터베이스 용어로 이상 현상(Anomaly)라 부른다. 따라서 격리성 부족으로 인해 발생하는 이상 현상을 보완하기 위해서는 Saga를 설계할 때 별도의 대응책이 필요하다.

3.1 격리성 부족으로 인한 이상 현상들

격리성의 부족은 아래와 같이 Lost Updates, Dirty Reads 그리고 Fussy/Nonrepeatable reads라는 이상 현상을 유발할 수 있다.

▶ Lost Updates

특정 Saga에서 수정 중인 데이터를 다른 Saga에서 덮어 씌어 버리는 경우이다. 예를 들어 Create Order Saga의 첫 단계에서 주문을 생성한 후 Cancel Order Saga가 상기 주문을 취소해 버렸는데 다시 Create Order Saga의 마지막 단계에서 주문을 승인하는 경우가 이에 해당한다. 이 시나리오에서 Create Order Saga는 Cancel Order Saga가 만든 변경사항을 무시하고 덮어 씌어 버리게 되고 결국 취소된 주문이 배송되는 결과가 발생한다.

▶ Dirty Reads

Dirty Read는 원래 다른 트랜잭션에 의해 수정됐지만 아직 커밋되지 않은 데이터를 읽는 것을 말한다. 이 경우 변경을 가한 트랜잭션이 최종적으로 롤백되면 그 값을 읽은 트랜잭션은 부정합 상태에 놓이게 된다. Saga의 경우 특정 Saga가 완료되지 않은 상태에서 수정이 진행 중인 데이터를 읽어 오게 되면 이와 같은 이상 현상이 발생한다.

Fussy/Nonrepeatable reads

하나의 Saga를 구성하는 2개의 단계가 동일한 데이터를 읽었으나 중간에 다른 Saga에서 수행한 업데이트로 인해 다른 결과가 나오는 이상 현상을 의미한다.

3.2 격리성 부족에 대한 대응책

Saga의 트랜잭션 모델은 ACD이고 이로 인한 격리성 부족은 앞서 설명한 이상 현황(anomalies)을 유발한다. 따라서 이상 현상을 막거나 비즈니스 영향이 최소화되도록 Saga를 설계해야 하는데, 이와 같이 격리성 부족으로 인해 발생하는 이상 현상을 해결하기 위한 주요 대응책으로는 Semantic Lock, Commutative Updates, Pessimistic View, Reread Value, Version File, By Value 등이 있다.

▶ Saga의 구조

이상 현상을 막기 위한 대응책들을 살펴보기 전에 우선 Saga의 구조에 대해 알아보자. Saga는 아래와 같이 3가지 유형의 트랜잭션으로 구성된다.

· 원복 가능 트랜잭션(Compensatable transactions) – 트랜잭션 실행 이전으로 원복 가능한 트랜잭션들

· 피봇 트랜잭션(Pivot Transaction) – Saga에서 Go와 No Go를 결정하는 분기점이 되는 트랜잭션

· 재시도 트랜잭션(Retriable Transaction) – 피봇 트랜잭션 이후에 오는 성공이 보장된 트랜잭션.

예를 들어 Create Order Saga에서 createOrder(), verifyConsumerDetails(), createTicket()는 원복 가능 트랜잭션이다. 이중 createOrder()와 createTicket()은 변경사항을 원복할 수 있는 원복 트랜잭션을 가지고 있지만, VerifyConsumerDetails()는 조회용이어서 원복 트랜잭션이 없다. authorizeCreditCard()는 피봇 트랜잭션으로 이 신용카드 트랜잭션이 승인되면 Saga는 최종 단계까지 모든 단계의 실행이 보장된다. approveOrder()와 approveTicket()은 피봇 트랜잭션에 이어지는 재시도 트랜잭션들이다.

▶ Semantic Lock

Semantic Lock을 사용하면 Saga의 원복 가능 트랜잭션들은 신규 생성하거나 수정하는 모든 레코드에 플래그를 설정한다. 이 플래그는 해당 레코드가 현재 수정 중으로 커밋되지 않았기 때문에 향후 변경될 수 있다는 것을 표시하는 것으로 다른 트랜잭션이 이 레코드에 접근하는 것을 막는 잠금장치 역할 또는 다른 트랜잭션이 이 레코드를 취급할 때 최종 데이터가 아닐 수 있다는 점을 고려해야 한다는 것을 알려주는 역할을 한다. 이 플래그는 Saga의 최종 단계에서 실행되는 재시도 트랜잭션 또는 Saga를 원복하는 원복 트랜잭션에 의해 클리어된다.

Semantic Lock을 사용하려면 잠금장치(Lock)를 설정하는 것 외에 레코드가 잠겨 있는 경우 어떻게 처리할 것인지에 대해서도 결정해야 한다. 예를 들어 클라이언트에서 APPROVAL_PENDING 상태에 있는 주문의 취소를 요청하기 위해 cancelOrder()라는 오퍼레이션을 호출했다고 생각해 보자. 이 경우 한가지 옵션은 cancelOrder()를 실패 처리하고 클라이언트에게 나중에 다시 시도하라고 응답하는 것이다. 또 하나의 옵션은 레코드의 잠금이 해제될 때까지 cancelOrder()를 대기상태로 만드는 것이다.

▶ Commutative Updates

격리성 부족에 대한 간단한 대응책 중 하나는 업데이트 오퍼레이션이 교환적이 되도록 설계하는 것이다. 만약 오퍼레이션이 순서를 바꿔서 실행해도 된다면 해당 오퍼레이션은 교환적(commutative)라고 할 수 있다. 예를 들어 회계의 Credit()과 Debit()은 교환적인 오퍼레이션이다. 이 대응책을 사용하면 Lost Update 문제를 해소할 수 있다.

▶ Pessimistic View

또 다른 대응책으로 Pessimistic View가 있는데, 이것은 Dirty Read로 인한 비즈니스 영향을 최소화하기 위하여 Saga에 참여하는 단계들의 순서를 조정하는 것이다. 예들 들어 Create Order Saga에서 가용한 신용한도를 조회할 때 Dirty Read가 발생하여 신용 한도를 초과하는 주문이 생성되는 위험을 막기 위하여 Cancel Order Saga의 순서를 아래와 같이 조정할 수 있다.

변경 전
변경 후
[1] 소비자 서비스 – 가용한 신용한도 증가
[2] 주문 서비스 : 주문의 상태를 Cancelled로 변경
[3] 배송 서비스 – 배송 취소 처리
[1] 주문 서비스: 주문의 상태를 Cancelled로 변경
[2] 배송 서비스 : 배송 취소 처리
[3] 소비자 서비스 : 가용한 신용한도 증가

상기와 같이 Saga의 순서를 조정하면 가용한 신용한도는 마지막에 실행되는 재시도 트랜잭션에 의해 증가되기 때문에 Dirty Read의 위험을 감소시킬 수 있다.

▶ Reread Value

Reread Value는 Lost Update이 발생하지 않도록 해주는 대응책이다. 이 대응책을 적용하면 Saga는 데이터베이스의 레코드를 업데이트하기 전에 해당 레코드를 다시 읽어서 변경되지 않았는 지를 확인한 후에 업데이트를 진행한다. 만약 변경이 발생하였다면 Saga는 중단되고 경우에 따라 재시작된다. 이 대응책은 일종의 Optimistic Offline Lock 패턴과 유사하다.(https://martinfowler.com/eaaCatalog/optimistic OfflineLock.html)

▶ Version File

Version File은 비교환적인 오퍼레이션을 교환적 오퍼레이션으로 변경하는 대응책이다. 예를 들어 Create Order Saga와 Cancel Order Saga가 동시에 실행되는 시나리오를 생각해보자. Semantic Lock을 사용하지 않는다면 Cancel Order Saga는 Create Order Saga가 신용카드 승인을 처리하기 전에 신용카드 승인을 취소할 수 있다. 회계 서비스가 이 비정상적인 요청을 처리하는 한가지 방법은 요청이 도착했을 때 오퍼레이션을 기록하였다가 제대로 된 순서대로 그것들을 실행하는 것이다. 이 시나리오에서는 먼저 Cancel Authorization 요청을 기록한다. 그런 후 회계 서비스가 Authorize Card 요청을 수취했을 때 Cancel Authorization 요청이 수취되었음을 알리고 신용카드 승인을 건너뛰는 것이다.

▶ By Value

마지막 대응책은 By Value로, 이것은 비즈니스 리스크에 기반하여 선택하는 동시성 메커니즘 전략이다. 이 대응책을 사용하는 애플리케이션은 각 요청의 속성에 따라 Saga와 분산 트랜잭션 중 어떤 것을 사용할 지를 결정한다. 저위험 요청에 대해서는 앞에서 설명한 대응책들이 적용된 Saga를 이용하지만, 고위험 요청에 대해서는 분산 트랜잭션을 사용한다. 이 전략을 통해 비즈니스 위험성, 가용성 그리고 확장성 사이의 균형을 동적으로 조정할 수 있다.