분산 트랜잭션
분산 트랜잭션은 여러 독립적인 시스템, 데이터베이스, 또는 서비스에 걸쳐 하나의 트랜잭션을 수행해야 할 때 발생한다.
트랜잭션은 원자성(Atomicity), 일관성(Consistency), 고립성(Isolation), 지속성(Durability)을 보장한다(ACID 특성). 그러나 MSA 환경에서는 여러 마이크로서비스가 각기 다른 데이터베이스를 사용하거나, 다른 물리적 위치에 있을 수 있기 때문에, 이러한 ACID 특성을 분산 환경에서 보장하는 것은 매우 복잡하다.
MSA에서의 분산 트랜잭션
MSA에서는 각 마이크로서비스가 독립적으로 배포되고 운영되기 때문에, 여러 서비스 간의 데이터 일관성을 유지해야 하는 상황에서 자연스럽게 분산 트랜잭션의 필요성이 발생할 수 있다. 예를 들어, 한 서비스에서 주문을 생성하고, 다른 서비스에서 결제를 처리해야 할 때, 두 작업이 모두 성공적으로 완료되거나, 그렇지 않으면 전체 트랜잭션이 롤백되어야 할 필요가 있다.
2-Phase Commit (2PC) 프로토콜
2-Phase Commit은 분산 트랜잭션에서 트랜잭션의 원자성을 보장하기 위한 프로토콜로, 다음 두 단계를 통해 모든 관련 시스템이 트랜잭션을 일관되게 처리하도록 한다.
1. 준비 단계 (Prepare Phase)
- 코디네이터(Coordinator): 분산 트랜잭션의 중앙 조정자는 트랜잭션에 참여하는 모든 참여자(노드)에게 트랜잭션을 커밋할 준비가 되었는지 묻는다. 이 요청은 "prepare" 메시지를 통해 전송된다.
- 참여자(Participants): 각 참여자는 트랜잭션을 커밋할 준비가 되었다면 "Yes" 응답을 하고, 준비되지 않았거나 실패한 경우 "No" 응답을 보낸다. "Yes"를 응답한 참여자는 트랜잭션을 로컬에서 로그에 기록하여 영구히 유지한다.
2. 커밋 단계 (Commit Phase)
- 커밋: 모든 참여자가 "Yes"를 응답한 경우, 코디네이터는 트랜잭션을 커밋할 것을 지시하는 "commit" 메시지를 모든 참여자에게 보낸다. 참여자들은 커밋을 완료한 후 이를 코디네이터에게 통보한다.
- 롤백: 만약 하나의 참여자라도 "No"를 응답하거나, 참여자의 응답이 타임아웃되는 경우, 코디네이터는 모든 참여자에게 트랜잭션을 롤백하라는 "rollback" 메시지를 보낸다. 그러면 각 참여자는 트랜잭션을 취소하고 이 사실을 코디네이터에게 알린다.
2PC의 장점과 한계
장점
- 분산 시스템에서 트랜잭션의 원자성을 보장한다.
- 모든 참여자가 트랜잭션을 성공적으로 완료하거나, 실패 시 일관되게 롤백하도록 한다.
한계
- 성능 저하: 모든 참여자가 커밋 또는 롤백을 완료할 때까지 기다려야 하기 때문에, 네트워크 지연이나 참여자 지연이 있을 경우 성능이 저하될 수 있다.
- 가용성 문제: 코디네이터가 장애를 일으키면 전체 트랜잭션이 불확실한 상태에 빠질 수 있다. 코디네이터가 복구될 때까지 참여자들은 대기 상태가 된다.
- 잠금 문제: 트랜잭션이 커밋되거나 롤백될 때까지 관련 자원들이 잠긴 상태로 유지되어야 하므로, 이로 인해 다른 작업이 지연될 수 있다.
사가 패턴(Saga Pattern)
트랜잭션을 여러 단계로 나누어 처리하고, 각 단계가 독립적으로 커밋되는 방식이다. 서비스 간 메시지 또는 이벤트를 주고 받으며, 실패 시 보상 트랜잭션을 실행하여 상태를 롤백한다.
사가 패턴과 이벤트 소싱(Event Sourcing)
이벤트 소싱은 상태 변화를 이벤트로 기록하고, 현재 상태를 추적하는 대신 과거의 모든 상태 변경 이벤트를 저장하는 방식이다.즉, 데이터베이스의 상태를 직접 저장하는 것이 아니라, 데이터의 변경 사항을 이벤트로 저장하여 시스템의 상태를 재구성할 수 있다.
사가 패턴은 각 서비스에서 독립적으로 트랜잭션을 처리하고, 그 결과를 바탕으로 다음 트랜잭션을 이어간다. 이 과정에서 이벤트 소싱을 사용하면 각 트랜잭션의 결과를 이벤트로 기록할 수 있다. 각 서비스는 트랜잭션 결과를 이벤트로 기록하고, 그 이벤트를 사가 패턴의 다음 단계로 넘기는 방식으로 트랜잭션을 이어갈 수 있다.
또한 이벤트 기반으로 사가 패턴을 구현하면 비동기 처리, 확장성, 느슨한 결합, 내결함성, 보상 트랜잭션 관리 등에서 장점을 얻을 수 있다.
시나리오 예시: 결제 실패 시 보상 트랜잭션의 실행 과정
1. 주문 생성
사용자가 상품을 주문한다. 이때 주문 서비스가 새로운 주문을 생성하고 주문 생성 이벤트를 발생시킨다.
- 주문 서비스: 주문 생성 (트랜잭션)
- 이벤트 브로커: OrderCreated 이벤트 발행
{
"event": "OrderCreated",
"orderId": "123",
"userId": "user001",
"productId": "productX",
"quantity": 5
}
2. 결제 처리 요청
OrderCreated 이벤트를 받은 결제 서비스는 결제 트랜잭션을 처리한다. 사용자의 결제 정보를 바탕으로 결제가 시도된다.
- 결제 서비스: 결제 시도 (트랜잭션)
- 이벤트 브로커: PaymentInitiated 이벤트 발행
{
"event": "PaymentInitiated",
"orderId": "123",
"paymentStatus": "Pending"
}
3. 재고 예약 요청
결제가 성공할 것이라는 가정 하에, 재고 서비스는 상품의 재고를 확인하고 예약을 진행한다. 재고가 충분하면 예약이 완료되고, 재고 예약 이벤트가 발생한다.
- 재고 서비스: 재고 예약 (트랜잭션)
- 이벤트 브로커: InventoryReserved 이벤트 발행
{
"event": "InventoryReserved",
"productId": "productX",
"quantity": 5,
"orderId": "123"
}
4. 결제 실패
결제 서비스가 외부 결제 게이트웨이와 통신하는 과정에서 결제가 실패합니다. 결제 서비스는 결제 실패 사실을 이벤트로 발행하고, 이를 통해 다음 단계로 넘어간다.
- 결제 서비스: 결제 실패 (결제 트랜잭션 실패)
- 이벤트 브로커: PaymentFailed 이벤트 발행
{
"event": "PaymentFailed",
"orderId": "123",
"reason": "Insufficient funds"
}
5. 보상 트랜잭션 실행 - 주문 취소
결제 실패 이벤트를 받은 주문 서비스는 주문 취소 트랜잭션을 실행한다. 사용자가 주문했던 내용을 취소하고, 이를 알리는 주문 취소 이벤트를 발행한다.
- 주문 서비스: 주문 취소 (보상 트랜잭션)
- 이벤트 브로커: OrderCancelled 이벤트 발행
{
"event": "OrderCancelled",
"orderId": "123"
}
6. 보상 트랜잭션 실행 - 재고 예약 취소
PaymentFailed 또는 OrderCancelled 이벤트를 받은 재고 서비스는 재고 예약 취소 트랜잭션을 실행한다. 예약했던 재고를 다시 풀어 재고를 복구하고, 재고 복원 이벤트를 발행한다.
- 재고 서비스: 재고 복구 (보상 트랜잭션)
- 이벤트 브로커: InventoryReleased 이벤트 발행
{
"event": "InventoryReleased",
"productId": "productX",
"quantity": 5,
"orderId": "123"
}
보상 트랜잭션 완료
이제 결제가 실패했기 때문에 트랜잭션을 롤백하는 보상 트랜잭션이 모든 서비스에서 완료되었다.
즉
- 주문이 취소되었고,
- 예약된 재고가 다시 원래 상태로 복구되었다.
이 과정에서 이벤트 브로커는 각 서비스 간에 상태 변경 이벤트를 전달하고, 서비스들은 이 이벤트를 기반으로 자신의 트랜잭션을 처리하거나 롤백하는 작업을 진행한다.
코레오그래피(Choreography) 사가 패턴
코레오그래피 사가패턴은 중앙 제어 없이, 서비스끼리 이벤트로 통신하는 패턴이다. 위 그림에서의 흐름은 다음과 같다.
- Order 서비스가 POST /orders 요청을 받아 PENDING 상태의 주문을 생성한다.
- 그 후, Order Created 이벤트를 발생시킨다.
- Customer 서비스의 이벤트 핸들러는 신용 한도를 예약하려고 시도한다.
- 그런 다음, 그 결과를 나타내는 이벤트를 발생시킨다.
- Order 서비스의 이벤트 핸들러는 그 이벤트를 받아, 주문을 승인하거나 거절한다.
이 패턴의 장점은 참여자가 적고 중앙제어가 필요 없는 경우에 적합하다. 또한 추가 서비스 생성 및 소멸에 간섭이 없어 구성이 간편하고, 역할 분산으로 단일 실패 시점이 존재하지 않는다.
그러나 명령 추적이 어렵기 때문에 워크플로우 파악이 힘들 수 있고, Saga 참가자간 순환 종속성 발생 가능성이 있다. 또한 통합 테스트가 어렵다.
오케스트레이션(Orchestration) 사가 패턴
중앙의 컨트롤러가 전체 흐름과 보상 작업을 제어하는 패턴이다. 위 그림에서의 흐름은 다음과 같다.
- Order 서비스가 POST /orders 요청을 받아 Create Order Saga 오케스트레이터를 생성한다.
- 사가 오케스트레이터는 PENDING 상태의 주문을 생성한다.
- 그런 다음, Customer 서비스로 Reserve Credit(신용 예약) 명령을 보낸다.
- Customer 서비스는 신용 예약을 시도한다.
- 그 후, 결과를 나타내는 응답 메시지를 사가 오케스트레이터에게 보낸다.
- 사가 오케스트레이터는 해당 결과에 따라 주문을 승인하거나 거절한다.
위 방법은 참여자가 많고, 복잡한 워크 플로우에서 적합하며, 프로세스 흐름의 제어가 가능하다.
또한 오케스트레이터의 존재로 순환 종속성 발생 우려가 없고, 참여자는 다른 참여자의 명령을 몰라도 된다.
단, 중앙 통제를 위한 로직이 복잡할 수 있고, 모든 워크플로우를 관리하므로 단일 실패지점이 될 우려가 있다.
2PC, 사가 패턴(Saga Pattern)의 ACID 원칙 비교
ACID 원칙 | 2PC | SAGA |
Atomicity(원자성) | 원자성을 보장. 트랜잭션이 모두 커밋되거나 모두 롤백됨. | 전체 트랜잭션이 원자적이지 않음. 각 단계는 독립적으로 커밋되며, 실패 시 보상 트랜잭션을 통해 복구. |
Consistency(일관성) | 트랜잭션이 성공하면 시스템은 일관된 상태 유지. | 최종적으로 일관된 상태를 보장하지만, 중간에 일관성이 깨질 수 있음. |
Isolation(격리성) | 격리성에서 문제가 발생할 수 있음. 자원 잠금으로 인해 성능에 영향을 줄 수 있음. | 완벽한 격리성을 보장하지 않음. 다른 트랜잭션이 중간 상태 데이터를 참조할 수 있음. |
Durability(지속성) | 트랜잭션이 커밋되면 데이터는 지속되며, 장애 시에도 손실되지 않음. | 각 트랜잭션이 완료되면 영구적으로 유지. 실패 시 보상 트랜잭션으로 복구 |
참조
- Saga pattern - Azure Design Patterns | Microsoft Learn
- Two-Phase Commit(2PC) - Distributed Design Patterns (linkedin.com)
- Pattern: Saga (microservices.io)
- [전자정부 표준 프레임워크 기반 CNA 설계 - MSA의 구현] 5. MSA 구현 패턴(1) - Saga 패턴 (youtube.com)
'ComputerScience > Database' 카테고리의 다른 글
[Database] PostgreSQL의 데드 튜플 문제 (0) | 2024.11.20 |
---|---|
[Database] 캐시 압력(Cache Pressure) (0) | 2024.09.20 |
[Database] CQRS와 분산 읽기/쓰기 DB 구성, 데이터 일관성 모델 (최종 일관성, 강력한 일관성) (0) | 2024.08.13 |
[Database] DBCP (DB connection pool)과 hikariCP, MySQL을 기준으로 (0) | 2024.07.13 |
[Database] DB MVCC와 PostgreSQL, MySQL의 동작 비교 (0) | 2024.04.27 |