분산 환경에서의 트랜잭션
모놀리식 아키텍처(Monolithic Architecture)의 일관성을 보장하기 위해 관계형 데이터베이스를 공유하는 것이 보통이다. 하나의 DB 트랜잭션으로 처리하는 경우에는 개발자가 ACID(원자성(Atomicity), 일관성(Consistency), 격리성(Isolation), 지속성(Durability))을 보장하는 작업이 매우 단순해진다. 마이크로서비스 아키텍처도 마찬가지로 단일 서비스 내부의 트랜잭션은 ACID를 보장하지만, 여러 서비스의 데이터를 업데이트하는 트랜잭션을 구현 시 각 서비스마다 데이터베이스를 가지고 있기 때문에 데이터 일관성을 보장하기 까다로워진다. 분산 시스템 환경에서 트랜잭션과 데이터 일관성을 다루는 방법에 대해 알아보자.
2PC
하나의 트랜잭션으로 데이터 일관성을 보장할 수 없는 문제를 해결하기 위해 널리 사용되는 방법 중 하나로 2단계 커밋(2-phase commit, 2PC)가 있다. 2PC는 이름처럼 두 단계로 나눠 커밋을 진행하는 방식으로 분산 시스템에서 한 트랜잭션에 여러 노드의 프로세스가 관여할 수 있다. 첫 번째 단계는 준비단계로 조정자(Coordinator)가 각 트랜잭션 참여자들에게 커밋 가능 여부를 질의한다. 각 트랜잭션 참여자들은 트랜잭션을 열고 커밋 가능 여부를 응답한다.
두 번째 단계는 커밋 단계로 모든 참여자들이 트랜잭션 커밋 가능이라고 응답할 경우 조정자(Coordinator)가 커밋 요청을 보내 트랜잭션을 성공으로 종료한다. 그런데 단 하나의 서비스라도 트랜잭션 커밋 불가능 응답을 했을 경우에는 조정자(Coordinator)가 롤백 요청을 보내 트랜잭션을 실패로 종료한다.
2PC의 장점
2PC의 장점은 분산 트랜잭션에서도 원자성(Atomicity)를 보장해준다는 것이다. 2PC는 트랜잭션에서 모든 참여자가 커밋이나 롤백에 대해 합의할 때만 트랜잭션을 완료하기 때문에 여러 노드 및 자원에 걸쳐 하나의 트랜잭션이 수행될 때 강한 데이터 일관성을 보장해줄 수 있는 것이 장점이다.
2PC의 단점
2PC에서 준비 단계를 실행하려면 데이터베이스 트랜잭션 실행 방식을 변경해야 한다. 예를 들어, 2PC를 실행하려면 모든 데이터베이스가 X/Open XA 표준을 만족해야 한다. XA는 2PC를 통한 분산 트랜잭션을 처리하기 위한 X-Open에서 명시한 표준으로 전체 트랜잭션 참여자가 반드시 커밋 아니면 롤백을 하도록 보장한다. XA 호환 DB, XA 전역 트랜잭셩 ID 전파, 자바 EE 애플리케이션의 JTA 기술 등을 분산 트랜잭션을 수행할 수 있지만 NoSQL의 MongoDB, 카산드라나 메시지 브로커의 RabbitMQ, Kafka 등이 X/Open XA 표준을 지원하지 않기 때문에 사용하기 힘들다는 단점이 있다.
그리고 2PC의 가장 큰 문제점은 다른 노드의 메시지를 기다리는 동안 장애난 노드의 장애가 복구될 때까지 진행이 중단되어 락이 오랫동안 잠긴 상태로 남을 수 있어 성능이 좋지 않다는 것이고, 조정자(Coordinator)가 SPOF(Single-Point-Of-Failure), 즉 단일 장애 지점이 될 수 있다.
Saga Pattern
사가는 마이크로서비스 아키텍처에서 분산 트랜잭션 없이 데이터 일관성을 유지하는 메커니즘이다. 각 서비스의 작은 트랜잭션들을 실행하면서 진행하는데 특정 단계에서 실패하면 이전 커밋된 트랜잭션들에게 보상 트랜잭션을 실행하도록 해야한다.
2PC vs Saga
2 Phase Commit | Saga | |||||
장점 | 강한 일관성 | 높은 가용성 | ||||
높은 확장성 | ||||||
단점 | 낮은 가용성 | 중간 상태 노출 | ||||
낮은 확장성 | 보상 트랜잭션 구현 필요 |
2PC는 모든 참여자들이 트랜잭션을 열고, 가장 느린 참여자의 준비까지 기다려야 한다는 점에서 낮은 가용성과 확장성을 갖는 문제가 있다. 반면, 사가 패턴의 경우 각 서비스들의 로컬 트랜잭션들만 진행한다는 점에서 높은 확장성과 가용성을 갖지만 일부 트랜잭션들만 커밋된 중간 상태가 노출되고, 롤백을 위한 보상 트랜잭션을 직접 구현해야 한다는 단점이 있다.
상품 주문 서비스에서의 사가 패턴
위 그림처럼 상품 주문 시에 주문을 받는 서비스, 사용자의 결제를 담당하는 서비스, 사용자가 주문하고자 하는 상품의 재고가 있는지 확인하는 서비스 있다고 가정해보자. 그러면 주문 생성 사가는 간단히 표현하면 4개의 로컬 트랜잭션으로 구성된다고 할 수 있다.
- 주문 서비스: 주문을 APPROVAL_PENDING 상태로 생성한다.
- 재고 서비스: 주문의 재고가 있는지 확인한다.
- 결제 소비스: 소비자 신용카드를 승인한다.
- 주문 서비스: 주문 상태를 APPROVED로 변경한다.
서비스는 로컬 트랜잭션이 완료되면 메시지를 발행하여 다음 사가 단계를 트리거한다. 메시지를 통해 사가 참가 참여자를 느슨하게 결합하고 사가가 반드시 완료되도록 보장하는 것이다. 메시지 수신자가 일시 불능 상태라면, 메시지 브로커는 다시 메시지를 전달할 수 있을 때까지 메시지를 버퍼링할 수 있다. 여기서 발생할 수 있는 단점으로는 위에서 언급했듯이 주문의 재고가 있는지 확인한 후 재고를 차감시켜 커밋한 경우 주문한 사용자의 분산 트랜잭션이 모두 성공하지 않은 상태에서 다른 사용자가 해당 상품의 재고를 확인했을 때 재고가 차감된 상태로 볼 수 있는 중간 상태에 노출될 수 있다는 단점이 있다.
그리고 도중에 에러가 발생하면 변경분을 어떻게 롤백시킬 수 있을까?
단계 | 서비스 | 트랜잭션 | 보상 트랜잭션 | ||||||
1 | 주문 서비스 | createOrder() | rejectOrder() | ||||||
2 | 재고 서비스 | deductStock() | restoreStock() | ||||||
3 | 결제 서비스 | authorizeCreditCard() | |||||||
4 | 주문 서비스 | approveOrder() |
주문 생성 사가가 실패하는 이유에는 여러 가지가 있을 수 있다.
- 재고가 부족하여 상품을 구매할 수 없는 경우
- 소비자 신용카드가 승인 거절되었을 경우
사가는 단계마다 로컬 DB에 변경분을 커밋하므로 자동 롤백이 불가능하다. 따라서 주문 생성 사가 3번째 결제 서비스가 실패하면 1, 2번 단계에서 적용된 변경분을 명시적으로 언두 즉, 보상 트랜잭션(compensating transaction)을 미리 작성해야 한다. 사가 패턴에서는 (n + 1)번째 사가 트랜잭션이 실패하면 이전 n개의 트랜잭션을 언두해야 한다.
그리고 결제 서비스의 신용카드 승인은 피봇 트랜잭션으로 사가 흐름에서 결정적인 단계이다. 성공 시 사가를 계속 진행하지만 실패하면 사가 전체를 중단하고 롤백해야 하는 트랜잭션이다. 피봇 트랜잭션은 사가 흐름에서 커밋 시점에 해당하며, 이 단계까지 성공적으로 완료되면 이후 단계는 사가 패턴을 통해 최종 일관성을 보장받게 된다. 따라서 위 단계처럼 주문 생성 사가에서 결제 성공하면 다음 트랜잭션들이 네트워크 오류나 일시적인 서비스 중단 같은 상황이 발생하여 실패하더라도 재시도 가능한 방식으로 설계하여 시스템 신뢰성을 높일 수 있다.
사가를 시작할 때 로컬 트랜잭션을 완료하면 다음 참여자의 로컬 트랜잭션을 지시하고, 도중 하나라도 로컬 트랜잭션이 실패하면 사가는 보상 트랜잭션을 역순으로 실행한다. 사가 편성 로직에는 두 가지 종류가 있다.
코레오그래피(choreography) 사가
의사 결정과 순서화를 사가 참여자에게 맡겨 중앙 제어 장치 없이 참여자(participant)가 각자 서로 이벤트 교환으로 통신하는 방식이다. 위 그림처럼 사가 참여자는 서로 이벤트를 주고 받으며 소통하고, 주문 서비스를 시작으로 각 참여자는 각 자신의 DB를 업데이트하고 다음 참여자를 트리거하는 이벤트를 발행한다. 코레오그래피 사가의 경우 다음 순서대로 진행된다.
- 주문 서비스: 주문을 APPROVOAL_PENDING 상태로 생성 -> 주문 생성 이벤트를 발행
- 재고 서비스: 주문 생성 이벤트 수신 -> 소비자가 주문 할 수 있는지 확인(재고 확인) -> 재고 확인 이벤트 발행
- 결제 서비스: 주문 생성 이벤트 수신 -> 신용카드 승인을 PENDING 상태로 생성
- 결제 서비스: 재고 확인 이벤트 수신 -> 신용카드 결제 성공 -> 신용카드 승인됨 이벤트 발행
- 주문 서비스: 신용카드 승인됨 이벤트 수신 -> 주문 상태를 APPROVED로 업데이트 -> 주문 승인됨 이벤트 발행
만약 결제 서비스에서 실패 이벤트가 발행되는 경우에는 다음과 같은 순서로 진행된다.
- 주문 서비스: 주문을 APPROVOAL_PENDING 상태로 생성 -> 주문 생성 이벤트를 발행
- 재고 서비스: 주문 이벤트 수신 -> 소비자가 주문 할 수 있는지 확인(재고 확인) -> 재고 확인 이벤트 발행
- 결제 서비스: 주문 생성 이벤트 수신 -> 신용카드 승인을 PENDING 상태로 생성
- 결제 서비스: 재고 확인 이벤트 수신 -> 신용카드 결제 실패 -> 신용카드 승인 실패 이벤트 발행
- 재고 서비스: 신용카드 승인 실패 이벤트 수신 -> 재고를 다시 원래 재고 차감 전 상태로 업데이트
- 주문 서비스: 신용카드 승인됨 이벤트 수신 -> 주문 상태를 REJECT로 업데이트
위 과정처럼 코레오그래피 사가 참여자는 발행/구독 방식으로 소통하기 때문에 두 가지 통신 이슈를 고려해야 한다. 먼저, DB를 업데이트하는 작업과 이벤트를 발행하는 작업이 원자적으로(atomically) 일어나야 한다. 따라서 사가 참여자가 서로 확실하게 통신하려면 트랜잭셔널 메시징을 사용해야 한다. 그리고, 사가 참여자는 자신이 수신한 이벤트와 자신이 가진 데이터를 연관 지을 수 있어야 한다.
코레오그래피(choreography) 사가 장점
- 높은 확장성(Scalability)
- 각 서비스가 독립적으로 이벤트를 발행하고 구독하기 때문에 수평 확장이 용이하다.
- 느슨한 결합(Loose Coupling)
- 서비스들이 직접적으로 호출하거나 의존하지 않고, 이벤트를 비동기로 연결되므로 서비스 간 결합도가 낮다.
- 다른 서비스가 일시적으로 중단되거나 장애가 발생하더라도 서비스가 복구된 후 이어서 이벤트를 처리할 수 있다.
코레오그래피(choreography) 사가 단점
- 복잡한 상태 관리로 인한 트랜잭션 추적의 어려움
- 서비스가 독립적으로 상태를 관리하는 만큼 트랜잭션 전반의 흐름을 추적하고 이해하기 어려워 디버깅이나 장애 대응이 복잡해질 수 있다. 이를 위해 분산 트레이싱 도구나 로그 수집 시스템이 필요하다.
- 순서 제어의 어려움
- 이벤트 기반 비동기 처리이기 때문에 이벤트가 예상된 순서대로 도착하지 않거나, 특정 이벤트가 지연될 경우 트랜잭션 흐름에 혼선이 발생할 수 있어 이를 방지하기 위한 추가적인 설계가 필요하다. 예를 들어, 위에서 결제 서비스에서 주문 확인 이벤트가 주문 생성 이벤트보다 먼저 들어온 경우 없는 주문에 대해 결제를 진행할 수도 있다.
오케스트레이션(orchestration) 사가
사가 오케스트레이터(중앙 제어 장치)가 참여자가 해야 할 일을 지시하는 방식이다. 사가 오케스트레이터는 사가 참여자에게 커맨드/비동기 응답 상호 작용을 하며 참여자와 통신한다. 즉, 사가 단계를 실행하기 위해 해당 참여자가 무슨 일을 해야 하는지 커맨드 메시지에 적어 보낸다. 사가 참여자가 작업을 마치고 응답 메시지를 오케스트레이터에 주면, 오케스트레이터는 응답 메시지를 처리한 후 다음 사가 단계를 어느 참여자가 수행할지 결정한다. 위 그림처럼 주문 서비스에서 주문 생성 사가 오케스트레이터를 생성할 수 있다. 그러면 다음과 같이 진행될 것이다.
- 주문 서비스: 주문을 APPROVOAL_PENDING 상태로 생성한다.
- 주문 서비스: 주문 사가 오케스트레이터에게 주문 트랜잭션 시작을 요청한다.
- 주문 사가 오케스트레이터: 재고 확인 커맨트를 재고 서비스에 전송한다.
- 재고 서비스: 재고를 확인하고 차감한 다음 주문 사가 오케스트레이터에 재고 확인 메시지를 응답한다.
- 주문 사가 오케스트레이터: 재고가 업데이트되었다는 정보와 함께 주문 확인 커맨드를 주문 서비스에 전송한다.
- 주문 서비스: 주문의 상태를 PAIDING으로 변경한다.
- 주문 사가 오케스트레이터: 신용카드 승인 커맨드를 결제 서비스에 전송한다.
- 결제 서비스: 주문을 결제한 다음 주문 사가 오케스트레이터에 신용카드 승인됨 커맨드를 응답한다.
- 주문 사가 오케스트레이터: 결제 완료 정보와 함께 주문 승인 커맨드를 주문 서비스에 전송한다.
- 주문 서비스: 주문 상태를 APPROVED로 변경한다.
제일 마지막 단계에서 사가 오케스트레이터는 자신도 주문 서비스의 한 컴포넌트이지만 커맨드 메시지를 주문 서비스에 전송한다. 물론 주문 생성 사가가 주문을 직접 업데이트해서 승인 처리해도 되지만, 일관성 차원에서 주문 서비스가 마치 다른 참여자인 것처럼 취급한다.
오케스트레이션 사가는 DB를 업데이트하는 서비스와 메시지를 발행하는 서비스가 단계마다 있다. 예를 들어, 주문 서비스는 주문 및 주문생성 사가 오케스트레이터를 생성한 후 1번 사가 참여자(재고 서비스)에게 메시지를 보낸다. 사가 참여자는 자신의 DB를 업데이트한 후 응답 메시지를 보내는 식으로 커맨드 메시지를 처리한다. 그러면 다시 주문 서비스는 사가 오케스트레이터 상태를 업데이트한 후 커맨드 메시지를 다음 사가 참여자(결제 서비스)에게 보낸다. 이때 DB 업데이트와 메시지 발행 작업을 원자적으로 처리해야 하기 때문에 트랜잭셔널 메시지를 사용해야 한다.
오케스트레이션(orchestration) 사가의 장점
- 중앙화된 트랜잭션 관리
- 트랜잭션의 전체 흐름을 하나의 오케스트레이터가 관리하여 트랜잭션의 상태를 일관되게 추적할 수 있어 오류가 발생할 경우 빠르게 대응할 수 있다.
- 오케스트레이터가 각 단계의 성공 여부를 확인하면서 다음 단계로 전환하므로, 서비스 간 순서 보장이 필요할 때 더욱 안전하게 일관성을 유지할 수 있다.
- 보다 간단한 오류 처리 및 보상 트랜잭션
- 오케스트레이터는 트랜잭션 흐름을 완전히 통제하고 있으므로, 특정 단계에서 오류가 발생할 경우 보상 트랜잭션을 자동으로 호출할 수 있다. 이로 인해 서비스 내에서 복잡한 오류 처리 로직을 구현할 필요가 없으며, 보상 트랜잭션을 명확하게 설계하고 관리할 수 있다.
오케스트레이션(orchestration) 사가의 단점
- 중앙 집중화로 인한 단일 장애 지점
- 오케스트레이터가 중앙에서 트랜잭션을 제어하므로, 오케스트레이터에 장애가 발생하면 트랜잭션 전체가 중단될 수 있다. 이를 방지하기 위해 오케스트레이터의 고가용성 설정이 필요하지만 인프라 비용이 증가할 수 있다.
- 성능 저하 가능성
- 코레오그래피에 비해 오케스트레이터가 모든 트랜잭션을 관리하기 때문에 많은 서비스 간의 트랜잭션 요청이 발생할 경우 처리 성능이 저하될 수 있다.
코레오그래피 사가와 오케스트레이션 사가
위 내용을 토대로 요약하자면 오케스트레이션 사가는 중앙 오케스트레이터가 트랜잭션 흐름을 제어해 일관성 유지와 오류 처리가 용이하며, 트랜잭션 상태 추적이 중요한 경우에 적합하다고 할 수 있다. 다만, 오케스트레이터의 복잡성 증가와 단일 장애 지점 가능성이 단점이기 때문에 순서 보장 및 상태 추적이 그렇게 중요하지 않은 서비스에서는 코레오그래피 사가를 쓰는 것이 좋다.
출처
토스 | SLASH 24 - 보상 트랜잭션으로 분산 환경에서도 안전하게 환전하기
Orchestration Saga Pattern With Spring Boot
Choreography Saga Pattern With Spring Boot
[Microservices Architecture] What is SAGA Pattern and How important is it?
'기타 > MSA' 카테고리의 다른 글
Kafka 핵심 개념 (1) | 2024.11.13 |
---|---|
MSA(MicroService Architecture)를 시작하기 위해 알아야 할 사전 지식! (1) | 2024.10.22 |