
모놀리식에서 MSA로 전환하며 필연적으로 서버 간 통신이 필요하게 되었습니다.
이 때, 만약 호출한 곳에서 트랜잭션 실패가 발생했을 때, 호출당한 곳에서 롤백이 되어야 하기 때문에 롤백을 어떻게 처리할지가 고민이었습니다.
서버 간 통신은 몇몇 API에서 발생하지만 그 중, 입찰 API를 예로 들어 설명해보겠습니다.
R을 제외하고, CUD 위주로 플로우를 정리해보았습니다.
포인트 차감, 포인트 차감 이력 생성에 실패하게 된다면 이후 로직들은 실행되지 않기 때문에 롤백이 필요하지는 않습니다.
그러나 경매 최고가 갱신에 실패하게 된다면 포인트가 차감된 건에 대해서 꼭 롤백이 필요합니다.
이를 해결하기 위해 보상 트랜잭션이 필요합니다.
Redis의 롤백은 어떻게 적용되나요?
Redis는@Transactional이 커밋되지 않으면 실제 명령어가 실행되지 않도록 설정했습니다.
그렇기 때문에 모든 플로우가 성공적으로 진행되어 커밋되었을 때 실제 명령어가 실행됩니다.
SAGA Pattern은 두가지 방식으로 구현할 수 있습니다.
Orchestration SAGA는 비용 부담과 제한된 시간 내에 새로운 기술을 익혀야 한다는 점에서 제외하였습니다.
Choreography SAGA는 프로젝트의 규모가 작아 워크플로우 파악이 상대적으로 용이하고, 팀원들의 Kafka 이해도로 인해 빠르게 도입이 가능합니다.
그렇기 때문에 Kafka 기반의 Choreography SAGA 방식을 사용하여 보상 트랜잭션을 구현하기로 결정하였습니다.
Auction 서버에서 경매 최고가 갱신에 실패했을 때 이벤트를 발행하게 되는데, try/catch를 통해 catch 했을 때 이벤트를 발행하면 이후 예외처리가 애매해지는 문제가 있었습니다.
@Transactional
public BidCreateResponse registerBid() {
...
// 포인트 차감, 포인트 차감 이력 생성
pointService.decreasePoint();
try {
// 경매 최고가 갱신 (+ 마감 시간 연장)
auctionRepository.save(auction);
} catch(Exception e) {
// kafka 포인트 롤백 이벤트 발행
// 원래 실패하게 된다면 예외를 던져야 하므로
throw new ApiException(...);
}
}
그러나 try/catch의 catch문 내에서 다시 exception을 던지는 것은 적절하지 않기 때문에 이후, 프론트엔드와의 협의를 가정하고, null을 반환하는 것으로 수정했습니다.
이후 플로우가 꼭 경매 조회 → 포인트 차감, 포인트 차감 이력 생성 → ... → 경매 최고가 갱신 (+ 마감 시간 연장) 순서로 진행되지 않아도 되는 API임을 깨닫고, 경매 조회 → ... → 경매 최고가 갱신 (+ 마감 시간 연장) → 포인트 차감, 포인트 차감 이력 생성 순으로 실행되도록 수정하였습니다.
입찰 API 뿐만 아니라, 다른 모든 API들을 확인 후, 위처럼 실행 순서를 수정하였고, 플로우의 마지막에 Open Feign을 통해 통신하기 때문에 보상 트랜잭션이 필요하지 않게 되었습니다.
그렇기 때문에 구현했던 kafka 기반 보상 트랜잭션은 제거했습니다.