Saga 패턴
해당 글은 '마이크로 서비스 패턴' 이라는 책에서 다룬 사가 패턴에 대해 정리하였습니다.
분산 트랜잭션?
마이크로서비스 아키텍처에서 단일 서비스 내부의 트랜잭션은 ACID를 보장하지만, 여러 시스템에 얽힌 트랜잭션은 구현하기가 까다롭기 때문에, 여러 서비스에 걸쳐 트랜잭션을 관리할 수 있는 매커니즘이 필요하다.
예시로 '주문 생성'을 본다면(createOrder()), 소비자 서비스, 주문 서비스, 주방 서비스, 회계 서비스 등 여러 서비스에 있는 데이터에 접근해야 하는데, 여기서 여러 DB에 걸친 데이터의 일관성을 유지할 수 있는 수단이 필요하다는 것이다.
이러한 수단 중 하나가 바로 분산 트랜잭션이다. 널리 알려져 있는 분산 트랜잭션 관리의 사실상 표준으로는 X/Open DTP(Distributed Transaction Processing) 모델(X/Open XA)이 있다. XA는 2단계 커밋을 이용해 전체 트랜잭션 참여자가 반드시 커밋이 아니면 롤백을 하도록 보장한다. 자바 애플리케이션은 JTA 기술을 이용해 분산 트랜잿션을 수행할 수 있다.
분산 트랜잭션의 문제점
- NoSQL DB와 현대 메시지 브로커(RabbitMQ, 카프카 등)은 분산 트랜잭션을 지원하지 않는다.
- 동기 IPC(프로세스간 통신)형태라서 가용성이 떨어진다.
- 가용성은 트랜잭션 참여자의 가용성을 모두 곱한 값인데, 분산 트랜잭션은 참여한 서비스가 모두 가동중이어야 커밋할 수 있기 때문에 가용성이 떨어질 수 밨에 없다.
- 글로벌 트랜잭션으로 하나라도 실패하면 모두 롤백해야 한다.
Saga Pattern (사가 패턴)
정의
사가는 마이크로서비스 아키텍처에서 분산 트랜잭션 없이 데이터 일관성을 유지하는 매커니즘이다.
여러 서비스의 데이터를 업데이트하는 시스템 커맨드마다 사가를 하나씩 정의한다.
여기서 사가는 비동기 메시징을 이용해 편성한 일련의 로컬 트랜잭션이다. 서비스 간 데이터 일관성은 사가로 유지한다.
사가 와 트랜잭션의 ACID 차이점
- ACID 트랜잭션에 있는 격리성이 사가에는 없다.
- 사가는 로컬 트랜잭션마다 변경분을 커밋하므로 보상 트랜잭션을 걸어 롤백해야 한다.
예시: 주문 생성
주문 생성 사가는 각각의 서비스에서 6개의 로컬 트랜잭션으로 구성된다.
- 주문 서비스 : 주문을 PENDING 상태로 생성한다.
- 소비자 서비스 : 주문 가능한 소비자인지 확인한다.
- 가계 서비스 : 주문 내역을 확인하고 티켓을 CREATE_PENDING 상태로 생성한다.
- 결제 서비스 : 소비자 신용카드를 승인한다.
- 가게 서비스 : 티켓 상태를 AWAITING_ACCEPTANCE로 변경한다.
- 주문 서비스 : 주문 상태를 APROVED로 변경한다.
서비스는 로컬 트랜잭션이 완료되면 메시지를 발행해 다음 사가 단계를 트리거 한다.
메시지를 통해 사가 참여자를 느슨하게 결합하고 사가가 반드시 완료되도록 보장하는 것이다.
메시지 수신자가 일시 불능 상태라면, 메시지 브로커는 다시 메시지를 전달할 수 있을 때까지 메시지를 버퍼링한다.
But. 도중에 에러가 발생하면 변경분을 어떻게 롤백시킬까?
보상 트랜잭션
ACID 트랜잭샨은 비즈니스 로직 실행 도중 규칙에 위배되면 쉽게 롤백이 가능하지만, 사가는 단계마다 로컬 DB에 변경분을 커밋하므로 자동 롤백은 불가능하다.
ex) 4번 단계에서 신용카드 승인이 실패하면 1~3번째 단계에서 적용된 변경분을 명시적으로 undo해야 한다.
즉 보상 트랜잭션을 미리 작성해야 한다. 보상 가능한 트랜잭션은 반대 효과를 가진 다른 트랜잭션을 처리해 잠재적으로 되돌릴 수 있는 트랜잭션을 의미한다.
하지만 모든 단계에서 보상 트랜잭션이 필요한 것은 아니다. 읽기 전용 단계나, 항상 성공하는 단계 다음에 이어지는 신용 카드 승인 같은 단계는 보상 트랜잭션이 필요 없다.
주문 생성 사가의 1~3번째 단계는 시패할 가능성이 있는 단계 다음에 있으므로 보상 트랜잭션, 4번 단계는 절대로 실패하지 않는 단계 다음에 있으므로 피봇 트랜잭션, 5~6번 단계는 항상 성공하기 때문에 재시도 가능 트랜잭션이라고 한다.
예를 들어 4번 단계 (신용카드 승인)이 실패하면 보상 트랜잭션은 다음 순서대로 작동될 것이다.
- 주문 서비스 : 주문을 PENDING 상태로 생성한다.
- 소비자 서비스 : 주문 가능한 소비자인지 확인한다.
- 가계 서비스 : 주문 내역을 확인하고 티켓을 CREATE_PENDING 상태로 생성한다.
- 결제 서비스 : 소비자 신용카드를 승인 요청이 거부된다.
- 가게 서비스 : 티켓 상태를 CREATED_REJECTED로 변경한다.
- 주문 서비스 : 주문 상태를 REJECTED로 변경한다.
여기서 5~6번 단계는 가게 서비스, 주문 서비스가 수행한 업데이트를 undo 하는 보상 트랜잭션이다.
여기서 일반 트랜잭션과 보상 트랜잭션의 순서화는 사가 편셩 로직에 따라 달라질 수 있다.
사가 편성
코레오그래피 사가
의사 결정과 순서화를 사가 참여자들에게 맡긴다. 사가 참여자들은 주로 이벤트 교환 방식으로 통신한다.
코레오 그래피 방식은 사가 참여자가 할 일을 알려주는 중앙 편성자가 없고, 사가 참여자가 서로 이벤트를 구독해 이에 따라 반응한다.
즉 각 로컬 트랜잭션이 다른 서비스에서 로컬 트랜잭션을 트리거하는 도메인 이벤트를 게시하는 방식이다.
장점
- 단순함 : 비즈니스 객체를 생성, 수정, 삭제할 때 서비스가 도메인 이벤트를 발행할 뿐이다.
- 느슨한 결합 : 참여자는 이벤트를 구독할 뿐 서로를 직접 알지 못한다.
- 책임이 사가 참여자에게 분산되므로 단일 실패 지점을 피할 수 있다.
단점
- 이해하기 어려움 : 사가를 한 곳에 정의한 것이 아니라 여러 서비스에 구현 로직이 흩어져 있기 때문에 이해하기 어려울 수 있다.
- 서비스 간 순환 의존성 : 사가 참여자가 서로의 이벤트를 구독하는 특성상, 순환 의존성이 발생할 위험이 있다. (순환 의존성은 잠재적인 설계 취약점이다.)
- 단단히 결합될 위험성 : 사가 참여자는 자신에게 영향을 미치는 이벤트를 모두 구독해야 하는데, 관련된 서비스들이 업데이트될 때 같은 주기로 업데이트 되어야 할 위험성이 있다.
- 트랜잭션을 시뮬레이션하기 위해 모든 서비스를 실행해야 하므로 통합 테스트가 어렵다.
간단한 사가라면 코레오그래피 방식도 충분하지만, 복잡한 사가에는 오케스트레이션 방식이 유리하다.
오케스트레이션 사가
사가 편성 로직을 사가 오케스트레이터에 중앙화 한다. 오케스트레이터는 커맨드/비동기 응답 상호 작용을 하며 참여자와 통신을 한다. 사가 오케스트레이터는 사가 참여자에게 커맨드 메시지를 보내 수행할 작업을 지시한다. 사가 팜여자가 작업을 마치고 응답 메시지를 오케스트레이터에 주면, 오케스트레이터는 응답 메시지를 처리한 후 다음 사가 단계를 어느 참여자가 수행할지 결정한다.
장점
- 의존 관계 단순화 : 오케스트레이터는 참여자를 호출하지만, 참여자들은 오케스트레이터를 호출하지 않으므로 단방행 의존성을 유지할 수 있다. 즉 순환 의존성을 피한다.
- 낮은 결합도 : 각 서비스는 오케스트레이터가 호출하는 API를 구현할 뿐이다. 참여자들이 발행하는 이벤트는 몰라도 된다.
- 관심사를 더 분리하고 비즈니스 로직을 단순화 : 사가 편성 로직이 오케스트레이터 한 곳에만 있으므로 도메인 객체는 더 단순해지고, 자신이 참여한 사가에 대해서는 알지 못한다. 때문에 비즈니스 로직이 훨씬 단순해진다.
단점
- 오케스트레이터에 너무 많이 중앙화 하면 다른 비즈니스 로직을 가지게 될 수 있다. (순서화만 담당할 수 있도록 주의하자)
- 오케스트레이터가 단일 실패 지점이 될 수 있다.
참고자료
- [마이크로 서비스 패턴] 4장. 트랜잭션 관리 사가
- https://learn.microsoft.com/ko-kr/azure/architecture/reference-architectures/saga/saga