MSA에서 분산 트랜잭션 롤백 전략이 필요한 이유
MSA로 구성하려면 서비스마다의 DB 분리가 필요하다. 그렇게 되면 원자성이 지켜지지 않을 가능성이 있다. 단일 DB인 경우는 단일 ACID DB 트랜잭션 범위 내에서 이 작업이 수행된다.
분산 DB인 경우 각 변경이 다른 DB에서 독립적으로 이루어지기 때문에 중간에 에러가 일어날 경우 데이터의 일관성이 지켜지지 않을 수 있다. 트랜잭션의 경계가 나뉘어짐에 따라 원자성이 지켜지지 않는데 이를 해결하기 위한 전략이 대표적으로 2-Phase-Commit, SAGA 패턴 두가지가 있다.
1. 2-Phase-Commit
분산 트랜잭션을 구현하는데 널리 사용된다. prepare 단계와 commit 단계로 구성된다.
- Prepare Phase: 관련된 모든 서비스는 commit을 준비하고, transatinon coordinator에 트랜잭션을 시작할 준비가 됐음을 알린다.
- Commit Phase: 이전 단계에서 트랜잭션을 시작할 준비가 됐다면, coordinator는 commit을 요청한다. 만약에 서비스 하나라도 실패가 발생한다면, coordinator 관련된 모든 서비스에 해당 트랜잭션을 콜백하도록 요청한다.
public interface BankService {
void prepareTransfer(int amount);
void commit();
void rollback();
}
@Component
public class AccountAService implements BankService {
// 계좌 A에서 금액을 차감하기 전의 준비 작업(계좌호출 차감 금액 확인등..)
@Override
public void prepareTransfer(int amount) {
if(balance >= amount) {
balance -= amount;
return true;
} else {
return false; // 잔액 부족
}
}
}
@Component
public class AccountBService implements BankService {
// 계좌 B로 입금하기 전의 준비 작업(입금 계좌 확인 및 금액 확인 등..)
@Override
public void prepareTransfer(int amount) {
balance += amount;
return true;
}
}
@Service
@RequiredArgsConstructor
public class TransferService {
private final AccountAService accountA;
private final AccountBService accountB;
public void transfer(int amount) {
boolean isPrepared = accountA.prepareTransfer(amount) && accountB.prepareTransfer(amount);
if (isPrepared) {
accountA.commit();
accountB.commit();
} else {
accountA.rollback();
accountB.rollback();
}
}
}
- Prepare Phase (준비 단계)
- 유저가 TransferService를 통해서 금액 이체를 요청한다.
- Coordinator(TransferService)는 계좌A(AccountAService)와 계좌B(AccountBService)에게 prepare 상태인지 확인을 요청한다. (=prepareTransfer 메서드)
- 각 계좌 서비스는 자신의 상태를 확인해서 prepare 상태면 준비 완료 응답을 반환하고 그렇지 않으면 실패 응답을 반환한다.
- Commit/Rollback Phase (커밋/롤백 단계)
-
Coordinator는 모든 계좌 서비스로부터 응답을 받는다.
-
만약 모든 서비스가 prepared 응답을 반환하면, Coorinator는 각 계좌 서비스에 커밋을 요청한다. (=commit 메서드 호출)
-
commit 호출에 따라 계좌 A에서는 출금이 이루어지고, 계좌 B에서는 입금이 이루어진다.
-
그러나 하나 이상의 서비스에서 prepared 실패 응답을 받으면, Coordinator는 롤백을 요청한다. (=rollback 메서드 호출)
-
rollback 호출에 따라 이전 상태로 복구된다.
1-2. 2-Phase-Commit 단점
- 트랜잭션의 책임이 Coordinator node에 있고 이 부분이 단일 실패지점(SPOF)이 될 수 있다.
- 전체 트랜잭션이 완료될 때까지 서비스에서 사용하는 리소스가 잠겨 있어 서비스가 완료될 때까지 대기해야 한다. 때문에 지연 시간이 늘어나고 리소스가 차단되어 확장이 어려워질 수 있다.
- NoSQL은 2PC-분산 트랜잭션을 지원하지 않는다.
2. SAGA 패턴
- SAGA 패턴은 MSA 환경에서 일관성을 지키기 어렵다는 것을 감안하여, 최정 일관성을 더 보장하는 패턴이다. 2PC에서는 트랜잭션을 하나의 트랜잭션으로 묶어서 처리하지만 SAGA 패턴은 긴 트랜잭션을 여러 개의 짧은 로컬 트랜잭션으로 분리하는 접근 방식이다. 각 트랜잭션은 다른 트랜잭션의 완료를 기다리지 않고 독립적으로 실행된다. 만약 중간에 문제가 발생하면 보상 트랜잭션을 진행한다. 보상 트랜잭션이란 서비스에서 트랜잭션 처리를 실패할 경우, 그 서비스의 앞선 다른 서비스에서 처리된 트랜잭션을 되돌리게 하는 트랜잭션이다.
SAGA 패턴은 Choreography-based saga와 Orchestration-based 두가지가 있다.
2-1. Choreography-based SAGA
- 각 로컬 트랜잭션이 다른 서비스의 로컬 트랜잭션을 이벤트 트리거 하는 방식
- 중앙집중된 지점이 없이 모든 서비스가 메시지 브로커(RabbitMQ, Kafka)를 통해 이벤트를 Pub/Sub 하는 구조다.
- 중앙 집중형 관리 방식이 아니어서 SPOF(Single Point Of Failure:단일 실패지점)이 없다.
- 트랜잭션을 시뮬레이션 하기 위해서 모든 서비스 실행이 필요해서 통합 테스트에 불리하다.
- 새로운 서비스 추가가 필요할 때 서비스간 연계성 파악이 중요하다. Cyclic Dependency 발생에 주의해야 한다.

- 사용자가 요청하면 OrderService는 Order 메서드를 수신하고 Pending 상태의 Order 이벤트를 생성한다.
- Order Created 이벤트를 발생시킨다.
- CustomerService의 이벤트 핸들러는 요청한 건에 대해 예약 시도한다.
시도한 결과는 이벤트로 발생시킨다.
- OrderService는 Order 이벤트를 승인하거나 거부한다.
2-2. Orchestration-based SAGA
- Orchestrator가 중앙 집중식 컨트롤러 역할을 하고 각 서비스에 실행할 트랜잭션을 아려주는 방법.
- Orchestrator는 요청을 실행, 각 서비스의 상태를 확인하고 실패에 대한 복구를 처리한다.
- 비교적 많은 서비스가 있는 복잡한 워크플로우에 적합하다.
- Orchestrator가 전체 워크플로우를 관리해서 실패 지점이 될 수 있다.

- 사용자가 요청하면 OrderService가 수신하고, Orchestrator는 Create Order를 생성한다.
- Orchestrator는 Pending 상태의 Order를 생성한다.
- CustormerService에 Reserve Credit 명령을 보낸다.
- CustermorService는 Reserve Credit를 시도한다.
- 시도한 결과는 메시지 통해 응답한다.
- Orchestrator는 최종적으로 Order를 승인하거나 거부한다.
reference
https://velog.io/@ch200203/MSA-%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C%EC%9D%98-%EB%B6%84%EC%82%B0-%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98-%EA%B4%80%EB%A6%AC2PC-SAGA-%ED%8C%A8%ED%84%B4
https://waspro.tistory.com/734
https://joobly.tistory.com/69