MSA 전환에 의한 보상 트랜잭션의 필요성

na.ram·2024년 11월 28일

all in auction

목록 보기
12/14
post-thumbnail

모놀리식에서 MSA로 전환하며 필연적으로 서버 간 통신이 필요하게 되었습니다.

이 때, 만약 호출한 곳에서 트랜잭션 실패가 발생했을 때, 호출당한 곳에서 롤백이 되어야 하기 때문에 롤백을 어떻게 처리할지가 고민이었습니다.

서버 간 통신은 몇몇 API에서 발생하지만 그 중, 입찰 API를 예로 들어 설명해보겠습니다.


입찰 API 플로우

R을 제외하고, CUD 위주로 플로우를 정리해보았습니다.

  1. [Point Server] 포인트 차감, 포인트 차감 이력 생성
  2. [Auction Server, Redis] 보증금 (재)예치
  3. [Auction Server, Redis] 이전 최고 입찰자 보증금 환불
  4. [Auction Server, Redis] 입찰 기록 생성
  5. [Auction Server] 경매 최고가 갱신 (+ 마감 시간 연장)
  6. [Auction Server, Redis] 경매 랭킹 갱신

보상 트랜잭션의 필요성

포인트 차감, 포인트 차감 이력 생성에 실패하게 된다면 이후 로직들은 실행되지 않기 때문에 롤백이 필요하지는 않습니다.

그러나 경매 최고가 갱신에 실패하게 된다면 포인트가 차감된 건에 대해서 꼭 롤백이 필요합니다.

이를 해결하기 위해 보상 트랜잭션이 필요합니다.

Redis의 롤백은 어떻게 적용되나요?
Redis는 @Transactional이 커밋되지 않으면 실제 명령어가 실행되지 않도록 설정했습니다.
그렇기 때문에 모든 플로우가 성공적으로 진행되어 커밋되었을 때 실제 명령어가 실행됩니다.


SAGA Pattern 의사 결정

SAGA Pattern은 두가지 방식으로 구현할 수 있습니다.

Orchestration SAGA

  • 중앙 컨트롤러에서 모든 보상 트랜잭션을 관리하다보니 SPOF의 위험이 존재합니다.
  • 매니징할 인스턴스를 추가로 하나 더 올려야 하기 때문에 비용 부담이 있습니다.
  • 구현을 위해 Axon Framework를 사용할 수 있어야 하므로 높은 러닝 커브가 따르고, 그에 비해 시간이 부족합니다.

Choreography SAGA

  • 역할이 분산되어 SPOF의 위험이 없습니다.
  • 이미 카프카 인스턴스가 존재하기 때문에 추가 인스턴스를 올릴 필요가 없습니다.
  • 구현을 위해 Kafka 등에 대한 추가 공부가 필요한데 해당 부분은 팀원 모두 어느정도 숙지가 되어 있어 러닝 커브가 낮습니다.

기술 결정

Orchestration SAGA는 비용 부담과 제한된 시간 내에 새로운 기술을 익혀야 한다는 점에서 제외하였습니다.
Choreography SAGA는 프로젝트의 규모가 작아 워크플로우 파악이 상대적으로 용이하고, 팀원들의 Kafka 이해도로 인해 빠르게 도입이 가능합니다.

그렇기 때문에 Kafka 기반의 Choreography SAGA 방식을 사용하여 보상 트랜잭션을 구현하기로 결정하였습니다.


SAGA Pattern의 구현

  • Auction 서버에서 경매 최고가 갱신에 실패
  • Kafka에서 실패에 대한 이벤트를 발행
  • Point 서버에서 해당 이벤트를 구독
  • Point 서버에서 포인트 원복, 포인트 차감 이력 삭제

문제점

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 기반 보상 트랜잭션은 제거했습니다.

0개의 댓글