ACID Transaction
원자성(Atomicity), 일관성(Consitency), 격리성(Isolation), 지속성(Durability)을 보장하는 트랜잭션

트랜잭션 관리

분산 트랜잭션(Distributed transaction)은 복수의 시스템 간의 트랜잭션 처리를 의미하며, 주로 서로다른 DB에 분산된 데이터에 대한 트랜잭션 관리를 의미한다. 분산 트랜잭션은 단일 트랜잭션과 마찬가지로 ACID(원자성, 일관성, 고립성, 지속성)을 지원해야 한다. 단일 데이터베이스에서는 단일 트랜잭션 범위 내에서 이 작업이 모두 수행 가능하도록 구현되어 있으나, 서로 다른 데이터베이스, 혹은 트랜잭션 단위로 처리되지 않는 No-SQL 등을 사용하면 ACID를 보장하는 것은 새로운 문제가 된다.

2 Phase Commit (2PC)

기존에는 이러한 분산 트랜잭션 문제를 해소하기 위하여 2PC(Two-Phase Commit) 패턴을 사용하였다. 2PC는 코디네이터(coordinator)와 여러 데이터베이스간의 합의를 통해 트랜잭션 커밋/롤백이 결정하는 방법으로, 글로벌 트랜잭션에 참여하는 모든 데이터베이스가 커밋이 가능한 상태 혹은 불가능한 상태임을 코디네이터에게 알리고(phase 1), 코디네이터가 커밋 또는 롤백을 수행(phase 2)하는 방식으로 동작한다.

출처: https://medium.com/cloud-native-daily/microservices-patterns-part-04-saga-pattern-a7f85d8d4aa3

그러나 2PC는 아래 사유들로 인해 MSA구조에서는 적합하지 않다.

  • Lock을 사용하므로 micro services 간에 의존성이 높아지고, 동기IPC 형태이므로 분산환경에서의 장애 대응(가용성이 낮아짐)이 어려워지며, lock으로 인한 대기로 인해 성능 측면에서 불리하다.
  • DBMS의 지원 혹은 메시지 인프라 등에서 2PC를 지원해야 하나, NoSQL을 포함한 이기종 DBMS를 사용하거나, 메시지 인프라(apache kafka 등)에서 지원하지 않기 떄문에 사용이 어려워지고 있다.
  • MSA는 각 마이크로 서비스가 각자의 데이터베이스를 소유하여 local transaction에만 ACID를 보장하므로(database per service), 여러 데이터베이스에 걸친 글로벌 트랜잭션을 하나의 코디네이터가 관리하는 구조는 적합하지 않다.
  • 코디네이터에 의존적이므로 코디네이터 장애 상황에서 각 데이터베이스는 커밋/롤백 여부를 스스로 결정할 수 없는 문제가 발생한다.

CAP 정리 by Eric Brewer
시스템은 일관성(consitency), 가용성(availability), 분할 허용성(partition tolerance) 중 두가지 속성 만 가질 수 있다.

Saga 패턴

Saga 패턴은 MSA와 같은 분산 아키텍처에서 데이터 일관성을 보장하기 위해 등장한 설계 패턴으로, 비동기 메시징을 이용하여 편성된 일련의 로컬 트랜잭션이다. 서비스 간 데이터 일관성은 연속된 개별 서비스의 로컬 트랜잭션이 이어져, 전체 비즈니스 트랜잭션을 구성하는 패턴이다. 첫번째 트랜잭션이 완료되면 두번째 트랜잭션이 트리거 되고, 두번째 트랜잭션이 완료되면 세번째 트랜잭션이 트리거되는 형태이다.
트랜잭션이 실패하면 2PC에서는 rollback을 수행했으나 이미 local DB에 commit 된 MSA에서는 rollback 처리가 불가능하다. 따라서 saga 패턴에서는 개별 서비스가 실패했을 때 보상 트랜잭션(compensating transaction)을 발생시켜 원래의 상태로 돌려주어야 한다. 즉, Saga 패턴에서는 데이터 일관성을 관리하는 주체가 DBMS가 아닌 애플리케이션이어야 한는 것이 Saga 패턴의 핵심이다. 보상 트랜잭션이 적용되기 전까지 일시적으로 데이터 정합이 깨져있을 수 있으나, 보상 트랜잭션이 완료되면 ‘결과적 정합성(eventually consistent)’을 보장할 수 있다.

Saga패턴
사가는 비동기 메시징을 이용하여 편성한 일련의 로컬 트랜잭션이다. 서비스 간 데이터 일관성은 사가로 유지한다.

출처: https://medium.com/cub3d/saga-pattern-for-managing-distributed-transactions-c5aee38a815e

Saga와 ACID의 중요 차이점

  • ACID 트랜잭션에 있는 격리성(Isolation)을 saga에서는 지원하지 않음
  • 로컬 트랜잭션마다 변경 사항을 커밋(commit)하므로 보상 트랜잭션을 통해 롤백 필요

Saga Transation 구조

Creating an Order using a saga. The createOrder() operation is implemented by a saga that consists of local transactions in several services.

FTGO의 애플리케이션 중 createOrder()를 처리하는 예제를 살펴보자. 주문 생성 요청에 대하여 애플리케이션은 총 6개의 로컬 트랜잭션을 쳐리하는 구조로 동작한다.

  1. 주문 서비스: 주문을 APPROVAL_PENDING 상태로 생성한다.
  2. 소비자 서비스: 주문 가능한 소비자인지를 확인한다.
  3. 주방 서비스: 주문 내역을 확인하고, 티켓을 CREATE_PENDING 상태로 생성한다.
  4. 회계 서비스: 소비자의 신용카드에 승인 처리를 한다.
  5. 주방 서비스: 티켓 상태를 AWAITING_ACCEPTANCE로 변경한다.
  6. 주문 서비스: 주문 상태를 APPROVED로 변경한다.

각 서비스는 로컬 트랜잭션이 정상적으로 완료되면 메시지를 발행하여 saga의 다음 단계를 실행시키는 형태로 메시지 기반으로 통신하여 서비스간 느슨하게(loosely) 결합시키고 트랜잭션이 완료될 수 있도록 보장한다. 메시지 브로커 등을 활용하여 일부 서비스의 장애시 메시지를 큐잉 하는 역할도 수행하게 된다.

Saga transaction은 다음의 3종의 트랜잭션으로 구성된다.

  • 보상 가능한 트랜잭션은 반대 효과를 가진 다른 트랜잭션을 처리하여 잠재적으로 되돌릴 수 있는 트랜잭션
  • 피벗 트랜잭션은 saga의 진행/중단 지점으로, 피벗 트랜잭션이 커밋되면 saga가 완료될 때까지 실행됨. 피벗 트랜잭션은 saga에서 보상 또는 다시 시도할 수 없는 트랜잭션이거나, 보상할 수 있는 마지막 트랜잭션이거나, 다시 시도할 수 있는 첫 번째 트랜잭션임
  • 재시도 가능 트랜잭션은 피벗 트랜잭션 이후에 오는 트랜잭션이으로 항상 성공을 보장하는 트랜잭션임
단계서비스트랜잭션보상 트랜잭션트랜잭션 유형
1Order ServicecreateOrder()rejectOrder()보상 가능 트랜잭션, 반드시 롤백 지원 필요
2Consumer ServiceverifyConsumerDetails()-보상 불필요 트랜잭션
3Kitchen ServicecreateTicket()rejectTicket()보상 가능 트랜잭션, 반드시 롤백 지원 필요
4Accounting ServiceauthorizeCreditCard()-피봇 트랜잭션, Saga 진행/중단 결정됨
5Restaurant Order ServiceapproveRestaurantOrder()-재시도 가능 트랜잭션, 반드시 성공(완료) 보장
6Order ServiceapproveOrder()-재시도 가능 트랜잭션, 반드시 성공(완료) 보장

위 트랜잭션에서 verifyConsumerDetails() 같은 읽기 전용 트랜잭션은 전체 트랜잭션에 실패가 발생하더라도 변경 사항이 없으므로 보상 트랜잭션이 불필요하다. 반면에 approveTicket() 트랜잭션은 항상 성공하는 트랜잭션이므로 보상 트랜잭션이 불필요하며, 재시도 가능 트랜잭션(retriable transaction)으로 서비스의 장애 등으로 단기간 처리가 불가능하다 하더라도 사후에 재시도 등을 통해 처리가 가능한 트랜잭션 유형이다. 피벗 트랜잭션은 최종적으로 saga 트랜잭션의 성공, 실패를 결정하는 트랜잭션으로 피벗 트랜잭션이 성공하면 이후 모든 트랜잭션은 성공 또는 재시도를 통해 성공시켜야 하며, 피벗 트랜잭션이 실패하면 이전의 모든 보상가능 트랜잭션은 보상 처리를 통해 원래 상태로 변경시켜야 한다. 예를 들어 피봇 트랜잭션인 신용카드 승인(4단계)에서 실패하면 다음 순서로 보상 트랜잭션이 동작한다.

  1. 회계 서비스: 소비자의 신용카드에 승인 요청이 거부된다.
  2. 주방 서비스: 티켓 상태를 CREATE_REJECTED로 변경하는 보상 트랜잭션을 처리한다.
  3. 주문 서비스: 주문 상태를 REJECTED로 변경하는 보상 트랜잭션을 처리한다.

Saga 편성

Saga는 로직에 의해 처리 단계를 구성해야 하고, 각 구성을 순차적으로 처리해야 한다. 즉, 트랜잭션의 첫 시작이 진행되면 이후 이벤트를 발생시겨 다음 트랜잭션을 지속적으로 처리해야 하며, 처리에 실패가 발생하는 경우 롤백을 처리하는 절차도 순차에 맞게 진행되어야 하며, 이는 이벤트를 기반으로 처리하게 된다. 따라서 트랜잭선의 이벤트 발생 관리 및 순서 관리를 위해 두가지 종류의 편성을 진행하게 된다.

Choreography(연출) saga

이 방식은 참여자에게 어떤 일을 처리해야 한다는 것을 알려주는 중앙 편성자가 없는 형태이다. 따라서 각 서비스는 이벤트를 구독하고, 수신된 이벤트 중 자신이 처리해야 할 내용이 있으면 처리하고, 처리 결과를 다시 이벤트로 게시 하는 형태로 진행된다.

정상 처리 케이스

Implementing the Create Order Saga using choreography. The saga participants communicate by exchanging events.

  1. 주문 서비스: 주문을 APPROVAL_PENDING 상태로 생성 -> 주문생성 이벤트 발행
  2. 소비자 서비스: 주문생성 이벤트 수신 -> 소비자가 주문 가능한 소비자인지 검증 -> 소비자 확인 이벤트(Consumer verified) 발행
  3. 주방 서비스: 주문생성 이벤트 수신 -> 주문 내역 확인 -> Ticket을 CREATE_PENDING 상태로 생성 -> Ticket 생성됨 이벤트(Ticket created) 발행
  4. 회계 서비스: 주문 생성 이벤트 수신 -> 신용카드 승인을 PENDING 상태로 생성
  5. 회계 서비스: 티켓 생성 및 소비자 확인 이벤트 수신 -> 소비자 신용카드에 승인 요청 -> 신용카드 정상 승인시 승인 이벤트(Credit card authorized) 발행
  6. 주방 서비스: 신용카드 승인 이벤트 수신 -> 티켓 상태를 AWAITING_ACCEPTANCE로 변경
  7. 주문 서비스: 신용카드 승인 이벤트 수신 -> 주문 상태를 APPROVED로 변경 -> 주문 승인(Order approved) 이벤트 발행

원복 처리 케이스

The sequence of event in the CreateOrderSaga when the authorization of the consumer's credit card fails. Accounting Service publishes the Credit Card Authorization Failed event, which causes Kitchen Service to reject the Ticket, and Order Service to reject the Order.

  1. 주문 서비스: 주문을 APPROVAL_PENDING 상태로 생성 -> 주문생성 이벤트 발행
  2. 소비자 서비스: 주문생성 이벤트 수신 -> 소비자가 주문 가능한 소비자인지 검증 -> 소비자 확인 이벤트(Consumer verified) 발행
  3. 주방 서비스: 주문생성 이벤트 수신 -> 주문 내역 확인 -> Ticket을 CREATE_PENDING 상태로 생성 -> Ticket 생성됨 이벤트(Ticket created) 발행
  4. 회계 서비스: 주문 생성 이벤트 수신 -> 신용카드 승인을 PENDING 상태로 생성
  5. 회계 서비스: 티켓 생성 및 소비자 확인 이벤트 수신 -> 소비자 신용카드에 승인 요청 -> 신용카드 승인 실패로 승인실패(Credit card authorization failed) 이벤트 발행
  6. 주방 서비스: 신용카드 승인 실패 이벤트 수신 -> 티켓 상태를 REJECTED로 변경
  7. 주문 서비스: 신용카드 승인 이벤트 수신 -> 주문 상태를 REJECTED로 변경

이벤트 발행시 고려 사항

Saga 참여자가 자신의 DB를 업데이트 하고, DB 트랜잭션의 일부로 이벤트를 발행해야 한다. 트랜잭션은 처리 되었으나 이벤트 발행이 누락되면 트랜잭션의 이후 작업 진행이 누락되고, DB 트랜잭션이 처리 되지 않았는데 이벤트가 발행되어도 정상 처리가 불가능하므로 원자적으로 처리를 보장해야 하는 것이다. 따라서 Saga 참여자간 통신에 transactional messaging 을 사용 필요하다.
Saga 참여자는 자신이 수신한 이벤트와 기존에 처리해서 가지고 있는 데이터와 연관을 찾아 처리가 가능해야 한다. 신용카드 승인 이벤트를 받았을때 어떤 주문에의해 발생한 카드 승인 이벤트인지를 찾을 수 있어야 티켓 상태 변경, 주문 상태 변경 등의 트랜잭션이 가능하게 된다. 따라서 이벤트에는 관련 정보를 확인하기 위한 상관관계ID(correlation ID)를 포함시켜 이벤트를 발행해야 한다.

장점

  • 단순함: 비즈니사 객채의 생성, 수정, 삭제시 서비스가 이벤트를 발행하므로 단순한 처리 가능
  • 느슨한 결합: saga 참여자는 상호간 직접 연계가 필요 없이 이벤트를 구독하는 것으로 처리 가능

단점

  • 이해의 어려움: saga transaction이 별도로 정의되어 있지 않아 전체 흐름 파악이 어려움
  • 서비스간 순환 의존성: 각 saga참여자간 메시지를 주고받으면서 순환 의존성(cycle dependancy)가 발생할 수 있어 설계상 무한 반복 오류 등에 빠지기 쉬움
  • 강한 결합 위험성: saga 참여자는 모든 이벤트를 구독해야 하는 형태로 상호 업데이트가 연계되는 위험성

Orchestration Saga

오케스트레이션 사가(Orchestration saga)는 중앙의 saga Orchestrator가 참여자들에게 어떤 로컬 트랜잭션을 실행해야하는지 알려주는 방식으로, 오케스트레이터는 커맨드/비동기 응답에 상호 작용을 관리하며 모든 트랜잭션에 대한 처리 흐름과, 필요시 보상 트랜잭션을 발생시켜 롤백을 시도를 수행한다.
Saga orchestrator는 state machine으로 모델링에 적합하며, local transaction이 완료되는 시점에 상태 전이가 trigger되어, local transaction의 결과에 따라 상태 전의 및 action을 수행할 수 있어 모델링을 쉽게 할 수 있다.

정상 처리 케이스

Implementing the Create Order Saga using orchestration. Order Service implements a saga orchestrator, which invokes the saga participants using asynchronous request/response.

  1. 주문 서비스: createOrder()를 호출받아 Order(주문)Orchestrator를 생성
  2. 주문서비스의 Orchestrator: Verify consumer 명령을 소비자 서비스에 전송
  3. 소비자 서비스: 소비자 확인(Consumer Verified) 메시지를 응답
  4. 주문서비스의 Orchestrator: Create Ticket 명령을 주방 서비스에 전송
  5. 주방 서비스: Ticket Created 메시지를 응답
  6. 주문 서비스의 Orchestrator: Authorize Card 명령을 회계 서비스에 전송
  7. 회계 서비스: 신용카드가 정상 승인되면 Card Authorized 메시지 응답
  8. 주문 서비스의 Orchestrator: 주방 서비스에 Approve Ticket 명령을 주방 서비스에 전송
  9. 주문 서비스의 Orchestrator: Approve Order 명령을 주문 서비스에 전송
  10. 주문 서비스: 주문 상태를 APPROVED로 변경

Finite State Machine

State machine은 상태와 전이로 구성되며, 전이가 발생될 때마다 액션이 일어나게 된다. Saga에서의 action은 다른 참여자를 호출하는 것으로, 상태간 전이는 지역 트랜잭션을 완료하는 시점에 trigger되며, 트랜잭션의 결과에 따라 다음 상태와 어떤 액션을 취할지를 결정하게 된다. 주문 생성 saga의 FSM 모델은 다음과 같다.

The state machine model for the Create Order Saga

  • 소비자 확인:초기 상태로, 소비자 서비스에 의해 주문 가능한 소비자 여부 확인 시점까지 유지
  • 티켓 생성: 티켓 생성 커맨드에 대한 응답 대기
  • 신용카드 승인: 회계 서비스가 신용카드 승인까지 대기
  • 주문 승인됨: saga의 주문 생성이 정상적으로 완료되었을때의 최종 상태
  • 주문 거부됨: saga의 주문 생성이 비정상적으로 완료(saga 참여자 중 하나가 주문 생성을 거부)되었을때의 최종 상태

메시지 발행시 고려 사항

Saga 참여자는 choreography saga와 마찬가지로 자신의 DB를 업데이트 하고, DB 트랜잭션의 일부로 응답 메시지를 발행해야 한다. 트랜잭션은 처리 되었으나 응답 발행이 누락되면 트랜잭션의 이후 작업 진행이 누락되고, DB 트랜잭션이 처리 되지 않았는데 응답이 발행되어도 정상 처리가 불가능하므로 원자적으로 처리를 보장하는 것이 필요하다. 따라서 Saga 참여자간 통신에 transactional messaging 을 사용 필요하다.

장점

  • 의존관계 단순화: 오케스트레이터는 참여자를 호출하지만, 참여자는 오케스트레이터를 호출하지 않아 순환 의존성이 발생 불가하며, 트랜잭션 흐름이 명확함
  • 낮은 결합도: 참여자는 오케스트레이터거 호출하는 API만 구현하고, 다른 참여자가 발행하는 이벤트에 대한 정보 불필요
  • 분리된 관심사로 비즈니스 로직의 단순화: 오케스트레이터가 트랜잭션 흐름을 관리하여 다른 서비스의 상태 변화에 관심이 없어 비즈니스 로직을 단순화 가능

단점

  • 오케스트레이터에 중앙 집중되어 확장성/유연성 저하
  • 오케스트레이터가 전체 워크플로를 관리하기 때문에 추가 실패 지점 발생

Saga 패턴의 비격리(Non-isolation) 문제

Monolothic 구조에서는 단일 DB를 사용하므로, 어플리케이션에서는 DB에서 제공하는 ACID(Atomicity, Consitency, Isolation, Durability)를 이용하여 트랜잭션을 개발자가 익숙한 형태로 개발할 수 있다. 그러나 분산된 Saga transaction 간에는 각 마이크로서비스가 고유의 DB를 갖고 있으므로 격리성(isolation)을 제공할 수 없다.

  • 원자성(atomicity): saga는 트랜잭션을 모두 완료하거나, 보상 트랜잭션을 통해 모든 변경분을 rollback(undo) 처리
    참고: DB에서의 원자성은 ALL OR NOTHING을 의미하는 것으로, 트랜잭션은 모두 실행하거나 모두 실행하지 않음의 두 가지 상태만 존재해야 한다. 트랜잭션은 논리적으로 쪼개질 수 없는 작업 단위이기 때문에 내부의 SQL문들이 모두 성공하고나, 중간에 어떤 하나의 SQL문이라도 실패하면, 지금까지 작업을 모두 취소해 이전 상태로 롤백해야 한다. 즉, 일부만 성공하는 상태는 존재해서는 안된다.
  • 일관성(consitency): microservice 내부의 무결성은 local DB가 제공하며, 여러 서비스에 걸친 무결성은 saga패턴의 서비스가 처리
    참고: 트랜잭션 이전과 이후에 DB는 항상 consistent한 상태여야 한다는 규칙이다. 트랜잭션은 DB 상태를 consistent 상태에서 또 다른 consistent 상태로만 전이된다. 만약, constraints, trigger 등을 통해서 DB에 정의된 규칙을 트랜잭션이 위반했다면 롤백해야 한다. 예를 들어, 계좌에 잔액을 업데이트하는 트랜잭션 결과로 잔액이 마이너스로 가버렸다면 그냥 롤백해야 한다.
  • 지속성(durability): local DB에 저장된 내역은 지속 유지 가능
    참고: 지속성 또는 영속성은 커밋된 트랜잭션의 결과는 데이터베이스에 영구적으로 저장되어야 한다는 의미로, DB System에 어떤 이벤트가 발생한다 하더라도, 커밋된 결과는 계속 DB에 유지되어야 한다.
  • 격리성(isolation): saga 패턴에서는 제공 불가하여 트랜잭션이 격리(isolation)되지 않아 비정상(anomalities) 문제가 발생 가능하여 보완 설계 필요
    참고: 격리성은 여러 트랜잭션이 동시에 실행될 때에도, 개별 트랜잭션이 혼자 실행되는 것처럼 격리되어 동작하게 만들어야 한다는 의미로, 동시에 실행되는 여러 트랜잭션은 서로 영향을 주지 않고 독립적으로 실행되어야 한다.

Saga 패턴에서의 비정상 처리

Saga 패턴에서 격리성을 보장하지 않아 발생할 수 있는 문제로, 특정 Saga가 실행되는 동안 참여자에 의해 수정된 데이터가 다른 참여자에 의해 변경거나 다른 참여자가 읽어 가는 등의 경우에 발생될 수 있으며, 다음 유형의 문제가 발생할 수 있다.

  • 소실된 갱신(Lost Updates): 한 saga에서 처리하던 변경분을 다른 saga에서 덮어쓰는 경우 기존 갱신분의 유실 발생
  • 무효 읽기(Dirty Reads): 한 saga에서 처리중이나 아직 업데이트 되지 않은 변경사항을 타른 saga나 트랜잭션에서 읽어가 잘 못 된 값을 읽어가는 문제
  • 퍼지/반복불가 읽기(Fussy/Nonrepeatable reads): 한 saga의 서로 상이한 두 단계에서 같은 데이터를 읽었음에도 불구하고 결과가 달라지는 현상으로, 다른 saga에서 해당 정보를 업데이트 하게 되는 경우 발생

오더 관련하여 각 비정상 상황의 예(https://happycloud-lee.tistory.com/154)를 살펴보면 다음과 같다.

Lost updates

  • 새 피자주문Saga가 시작되고, 바로 주문취소 Saga가 시작되었다.
  • 피자주문 -> 주문취소 순서로 진행되야 하는데 어떠한 이유로 주문취소가 먼저 수행되었다.
  • 피자주문Saga에서 다시 주문데이터가 생성되어 고객은 취소한 피자를 받게 된다.

Dirty Read

  • 피자주문Saga1은 3개 주문처리를 시작했고, 바로 이어 피자주문Saga2가 1개로 정정주문을 시작했다.
  • 피자주문Saga1의 포인트결재 트랜잭션이 고객포인트를 차감시켰다. 고객포인트가 0이 되었다.
  • 피자주문Saga1이 완료가 안된 상태에서 피자주문Saga2의 고객검증 트랜잭션이 고객포인트가 부족하다고 주문을 거절할 수 있다.

Fuzzy/Unrepeatable Read

  • 고객 포인트는 현재 3으로 피자3개 주문이 가능하다.
  • 피자주문Saga1은 1개 주문처리를 시작했고, 바로 이어 피자주문 Saga2가 3개로 정정주문을 시작했다.
  • 피자주문Saga2의 고객검증 트랜잭션이 먼저 처리되었다. 3점이므로 포인트 검증은 성공한다. 그리고 바로 피자주문Saga1이 포인트결재 트랜잭션이 실행되어 포인트를 2로 만들어 버렸다.
  • 피자주문Saga2는 고객 포인트 검증이 성공 했으므로 포인트 결제를 시도한다. 이때 고객 포인트를 읽어 보니 검증시에는 3점이었는데 2점으로 리턴된다.
  • 피자주문Saga2가 처음에 포인트 값을 읽었을땐 3점으로 유효하지만, 피자주문Saga1이 포인트 차감 후 다시 읽어보면 포인트값이 달라지기 때문에 Unrepeatable read라고 할 수 있다.

따라서 격리성(isolation)의 제공 불가로 인한 비정상 상태가 되는 것을 방지하기 위해 어플리캐이션을 작성해야 한다. 어플리에캐이션 작성의 방법으로 분산 트랜잭션을 사용하지 않는 다중 DB 구조에서 처리하는 방법을 준용하게 된다. 따라서 semantic lock, cummutative update, pessimistic view, reread value, version file, by value 등의 방법을 도입할 수 있다.

비정상 처리의 해소 방안

Semantic Lock: 어플리캐이션 수준의 lock

보상 가능 트랜잭션이 생성/수정하는 레코드에 flag를 세팅하여 레코드가 커밋 전이며, 변경 가능함을 표기하여 일종의 lock(접근 차단 혹은 경고)을 표기하고, 트랜잭션이 완료되거나 혹은 보상 트랜잭션이 실행될때 해제되는 형태로 동작한다. 주로 데이터 레코드에 상태 정보 항목에 *_PENDING 상태를 표기하여 트랜잭션이 진행 중임을 표기하고, 처리가 완료되면 *_APPROVED 혹은 롤백 처리 되는 경우 *_REJECTED 로 변경한다.
Semantic lock이 걸려 있는 경우 개별 케이스에 대한 대응 로직을 설계해야 한다. 예를 들어 APPROVAL_PENDING 상태의 주문에 대하여 고객이 취소 요청을 하는 경우, semantic lock이 해제될때까지 client가 대기하거나, 혹은 client에 에러(예외)를 응답하고, client에서 재시도 로직 등을 개발하는 등의 lock 제어 기능을 어플리캐이션에서 개발해야 한다. 또한 deadlock이 발생될 수 있으므로 saga를 rollback 시켜서 deadlock 해소하고 재실행 할 수 있도록 조치해야 한다.

Commutative update: 순서에 무관하게 동일한 결과 유지

업데이트 동작을 교환적으로 구성하는 것으로, Updates: A->B로 실행되던 B->A로 실행되던 결과가 동일한 transaction이 있다면 Saga transaction 순서 배치할 때 A와 B는 붙여서 배치하여 lost update 현상을 해소하는 방법이다. 예를 들어 debit()과 credit()은 두 트랜잭션이 실행되기만 하면 결과적으로 동일한 값을 얻을 수 있다. 물론 이 경우 overdraft에 대한 대응은 추가적으로 처리가 필요하다.

pessimistic view: 순서를 조정하여 리스크 최소화

중요한 트랜잭션은 위험 최소화를 위해 처리순서를 맨 마지막으로 바꾸는 방법으로 중요 트랜잭션 이후에는 repeatable transaction만 수행하여 항상 데이터가 갱신되는 것을 보장하여 trasaction이 완료된 결과만 보이게 한다. Dirty read를 최소화하기 위해 주문 생성 및 주문 취소가 겹쳐서(interleaved) 실행되는 중에 발생될 수 있는 문제 야기할 수 있는 잔액 처리 마이크로서비스를 가장 마지막 혹은 뒤에는 repeatable 트랜잭션만 유지함으로 써 해결할 수 있다. 예를 들어, 고객의 주문 취소 요청이 아직 주문 생성 saga가 완료 전에 들어왔으나, 이미 주문 취소가 불가능한 상황(배달원 할당 완료 등)이 되어 주문 취소 saga와 주문 생성 saga가 동시에 실행되는 경우에 발생될 수 있다.

  • 주문 생성 saga 흐름: 고객 잔액 조회 -> 주문 승인 상태 변경 -> 배달 승인 -> 고객 잔액 차감 순
  • 기존 주문 취소 saga 흐름: 고객의 잔액 증가 -> 주문 취소로 상태 변경 -> 배달 취소로 상태 변경 순
    위 순서로 트랜잭션이 병행 처리되는 경우, 1. 주문 취소 saga에 의해 고객의 잔액이 0->1로 증가하고, 2. 주문 생성 saga는 잔액이 1이므로 주문 다음 단계인 주문 승인 상태로 변경을 진행하는 등 이후 트랜잭션을 진행한다. 즉, 주문생성 saga는 dirty read(잔액이 0이었으나, 주문취소 saga가 먼저 처리되어 잔액이 1로 증가되어 있는 임시적인 상태를 주문 생성 saga에서 읽음)가 발생하여, 잔액이 1로 늘어나 있는 상태이므로 정상적으로 주문을 생성하게 된다. 그러나 고객은 애초에 잔액이 0인 상태였으므로, 주문 생성이 취소되어 원복(rollback)이 발생되었어야 하나 dirty read로 인해 오류가 발생하게 되었다. 이후 주문 취소 saga에서는 주문취소 상태 변경에서 오류(예시에 따라)를 응답받고 주문 rollback을 수행하면 기존에 증가시켰던 잔액을 1만큼 축소 시키게 되며, 이때 주문 생성 saga에서 잔액을 차감하여 0이 되었으므로 -1의 잔액으로 변화하게 되는 문제가 발생된다.
    이 문제를 해결하기 위해 중요 트랜잭션을 가장 마지막(이후 단계는 모두 성공만 발생)으로 옮겨 두는 형태로 주문 취소 saga의 흐름을 변경해 보자.
  • 신규 주문 취소 saga 흐름: 배달 취소 상태로 변경 -> 주문 취소로 상태 변경 -> 고객 잔액 증가
    이제 주문 취소 saga가 완료되기 이전에 고객의 잔액은 증가하지 않으므로 주문생성 saga는 dirty read가 없어 고객 잔액 조회 서비스에서 잔액 부족으로 transaction을 거절하게 될 것이다.

위 케이스를 해소하기 위해 주문변경Saga의 순서도 고객 잔액을 처리하는 기능을 가장 마지막으로 전환한다면 2번의 케이스가 발생되지 않으므로 잔액을 초과하여 주문을 수용하는 경우가 발생하지 않게될 수 있다.

reread value: 값 다시 읽기

이 방법은 lost updates를 해소하기 위한 방법으로, saga 처리 중 값을 갱신하기 전에 기존의 값을 다시 읽어 변경이 발생되었는지를 확인하는 방법이다. 만약 값을 다시 읽었는데 기존의 값과 다른 것을 확인했다면 트랜잭션의 동시성 제어가 필요한 상황이 되므로 현재 진행중인 saga를 rollback 후 나중에 재 시작하도록 조치가 가능하다. 이 방법은 일종의 optimistic offline lock 패턴으로 볼 수 있다.

version file: 버전 파일

이 방법은 비교환적(noncommutative)인 작업을 교환적(commutative)인 작업으로 변환하는 방법이다. 순서가 맞지 않는 작업 요청을 받았다면, 앞선 작업이 도착할때까지 처리를 대기하였다가, 앞전 순번을 처리한 이후 수행하여 트랜잭션의 순서를 보장하는 형태이다.

by value: 값에 의한

이 방법은 위험성이 낮은 요청은 saga 패턴을 적용하되, 은행 트랜잭션 중 특정 금액 이상의 대형 트랜잭션은 2PC등 분산 트랜잭션을 이용하는 방법이다.

profile
다시 시작해보자!

0개의 댓글