Search

8주차 - MSA 환경을 위한 분산 트랜잭션 처리 방안 고민

생성일
2025/04/22 14:34
태그

개요

서비스의 수요가 많아지고 트래픽이 증가하면, 도메인의 복잡성 증가나 서버의 스레드 풀, 데이터베이스의 커넥션, 메모리/디스크 용량 등 많은 이유로 인해 하나의 (모놀리식)서비스에서 처리하기 힘들어진다. 따라서 서비스가 커질수록 자연스럽게 서비스를 분리하여 MSA 환경으로 가게된다.
우리의 서비스가 이러한 상황으로 인해 각 도메인을 서비스로 분리해야하는 상황을 가정해보고, MSA 환경에서 트랜잭션을 어떻게 처리할 지 학습해보고 직접 구현해보자.

트랜잭션 처리 방식과 MSA 환경

여태까지 작성하던 방식은 위 그림처럼 다양한 로직을 하나의 트랜잭션으로 묶어서, 일부 로직에서 실패하거나 예외가 발생하면 트랜잭션 내에서 처리했던 모든 로직을 롤백하는 방식이였다. 이렇게 트랜잭션으로 묶는 것이 가능한 이유는 같은 서비스에 도메인이 존재하고 모든 도메인이 하나의 데이터베이스를 보고 있어 가능하다.
하지만 MSA 환경에서는 위 그림처럼 각 도메인이 서비스로 분리되어 있고, 각자 자신의 데이터베이스를 보고 있기 때문에 하나의 트랜잭션으로 묶어서 처리하는 것이 불가능하다. 이러한 문제를 해결하기 위한 방안으로는 2PC 방식과 SAGA 패턴 등이 있다.

분산 트랜잭션 및 보상 트랜잭션 방안

위에서 제시한 2PC 방식과 SAGA 패턴에 대해 위의 포인트 차감 → 결제 생성 → 예약 만료 기한 삭제 → 좌석 처리 실패 → 롤백의 흐름을 가진 결제 로직을 예시로 이해해보자.

2-Phase Commit(2PC)

2-Phase 커밋 방식은 분산 트랜잭션 환경에서 원자성을 보장하기 위해 두 단계로 구분되어 모든 시스템(서비스)이 일괄적으로 트랜잭션을 처리하도록 하는 방식이다.
1.
준비 단계(Prepare Phase)
먼저 코디네이터가 각 서비스에 트랜잭션 수행 가능 여부를 확인(prepare)한다. 모든 서비스에서 준비 상태(yes 응답)가 된다면 커밋 단계로 넘어간다.
2.
커밋 단계(Commit Phase)
코디네이터는 각 서비스에 트랜잭션 커밋 요청을 보내고, 모든 참여자가 Yes를 받기를 기다린다. 위 그림처럼 하나의 참여자라도 No를 응답한다면,
이처럼 전체 참여자에 롤백 요청을 보낸다.
장점
분산 시스템에서 모든 서비스의 트랜잭션을 일괄적으로 실행하기 때문에, 데이터의 원자성을 보장하고 정합성이 깨지지 않는다.
단점
모든 참여자가 성공하거나 롤백 되기를 기다려야하기 때문에 응답이 지연된다.
코디네이터가 단일 장애 포인트가 될 수 있다.
각 참여자들은 다른 모든 참여자들의 트랜잭션이 커밋되거나 롤백될 때까지 잠금을 유지하고 있어야하기 때문에 다른 작업이 지연될 수 있다.

SAGA 패턴 - Choreography 방식

Choreography 방식은 이벤트 기반으로 분산 트랜잭션 환경에서 일관성을 유지하는 방법이다.
위 그림처럼 요청을 받은 서비스부터 작업을 처리 후에 완료 이벤트를 발행 → 다음 작업자(서비스)가 해당 이벤트를 구독(Listen/subscribe) → 작업 처리 → 완료 후 이벤트 발행 → … 을 반복하는 이벤트 기반의 처리 방식이다.
만약 그 과정에서 실패하거나 예외가 발생한다면, 실패 이벤트를 발행하고 해당 작업 플로우에 참여하는 모든 서비스가 해당 실패 이벤트를 받아 롤백 처리하여 결과적 일관성을 유지하도록 한다.
장점
구현이 단순하고 간단하여, 참여자가 적고 중앙제어가 필요없는 경우에 효율적이다.
이벤트 기반으로 역할이 분리되어있기 때문에, 단일 장애 포인트가 없다.
단점
전체적인 흐름을 보기가 어렵고 추적이 쉽지않아 디버깅의 난이도가 높다.
참여자 간의 순환 종속성이 발생할 수 있다.

SAGA 패턴 - Orchestration 방식

orchestration 방식은 분산 트랜잭션 환경에서, 흐름을 제어하는 오케스트레이터가 전체적인 흐름과 트랜잭션을 관리해 일관성을 유지하는 방식이다.
그림처럼 오케스트레이터가 각 호출 혹은 이벤트 발행을 담당하여, 각 로직의 수행과 그 결과(성공/실패)를 처리하는 방식이다.
장애가 발생한 경우에도, 오케스트레이터가 참여자들에게 보상 트랜잭션을 통한 롤백 요청을 보내 결과적 일관성을 유지한다. 위 그림에서는 직접 호출처럼 그렸지만, 이벤트 기반으로 처리하는 것도 가능하다.
장점
구현이 상대적으로 복잡하기 때문에, 참여자가 많고 복잡한 워크플로우에 적합하다.
오케스트레이터로 인해 순환 참조가 발생할 일이 없고, 전체적인 흐름을 파악하기 쉽다.
단점
중앙 통제를 위한 로직이 복잡하고 구현 난이도가 높다.
오케스트레이터가 단일 실패지점이 될 수 있다.

구현해보며 느낀점

사실 오케스트레이션 패턴은 전부터 구현해보고 싶어서 이전 주차에 미리 구현을 했었던 내용인데, 단점으로 나온 구현이 복잡하다는게 어떤 느낌인지 정확히 이해했다. 어느 플로우까지 진행되었고, 각 플로우마다 어떤 롤백을 시켜줘야하는지를 직접 구현하는 것이 쉽지 않았다.
비교적 간단한 예약 로직에 코레오그래피 방식도 적용해보았는데, 이 역시 흐름을 파악하기 어렵다는 단점을 이해할 수 있었다. 예약은 좌석 선점, 예약 생성의 단순한 로직인데도 그런 불편함을 체감할 수 있었는데, 복잡한 로직이라면 추적이 얼마나 어려울지 상상되었다.
오케스트레이션 패턴은 ThreadLocal을 통해 보상 트랜잭션에 필요한 데이터를 오케스트레이터가 들고 있도록 구현했는데, 좋은 방법인지는 차치하고 일단 outbox 패턴 없이도 보상 트랜잭션 구현이 가능하다. 하지만 코레오그래피 방식은 서로 이벤트를 통해서만 주고 받기 때문에, 로직이 조금만 더 복잡해진다면 보상 트랜잭션을 위해서는 outbox 패턴이 필수라고 느껴졌다.
또 다른 불편함도 있었는데, 예약 로직에서 좌석 선점 → 예약 생성을 수행할 때, 예약 생성이 좌석 선점 완료 이벤트에서 시작되니까 좌석 선점에는 user 정보가 필요없지만 예약 생성에 필요하다는 이유로 좌석 선점 로직에 user 정보를 넘겨주어야 했다.

참고