개발 일기

[분산 환경] 분산 환경에서의 트랜잭션 처리 본문

Back-End/Spring Cloud + MSA + Kubernetes

[분산 환경] 분산 환경에서의 트랜잭션 처리

개발 일기장 주인 2025. 3. 10. 15:16

드링클리 프로젝트에서 모놀리틱에서 마이크로서비스로 전환하게 되면서 한 가지 의문이 발생했었습니다.

기존 모놀리식 시스템에서는 단일 데이터베이스 내에서 모든 작업이 하나의 트랜잭션으로 묶여 원자성이 보장되었지만, 마이크로서비스 환경에서는 각 서비스가 독립적으로 트랜잭션을 가져가기 때문에 원자성이 깨졌고 해왔던것 처럼 처리했을때 일관성있게 관리하는데에 한계를 느꼈습니다.

그래서 MSA와 같은 분산환경에서는 어떻게 트랜잭션을 관리하는지 알아보고자 블로그를 작성하게 됐고 이때 토스에서 올린 세미나를 참고했다.

 

토스ㅣSLASH 24 - 보상 트랜잭션으로 분산 환경에서도 안전하게 환전하기


분산 트랜잭션이 필요한 이유?

해당 예시에서 상황은 기존 Corebanking 시스템은 하나의 데이터베이스를 바라보고 있는 모놀리식 아키텍처였고 배포 및 개발 경험, 확장성, 단일 장애점 등의 이슈로 인해 MSA로의 전환을 시도하고 있으며 도메인 별로 분리 중 이라고 한다.

원화 계좌  도메인 또한 기존 모놀리식 아키텍처에서 분리되어 기존 Corebanking이 바라보고 있는 데이터베이스를 바라보고 있다.

이때, 환전을 위한 외화 계좌 도메인의 경우 신규 상품이기에 Corebanking에 없었기 때문에 데이터베이스까지 분리된 MSA 서버에서 개발 중이라고 한다. 

 

 

만약 환전이 동일한 서버에서 동일한 데이터베이스에서 작업된다면 트랜잭션의 원자성이 지켜질 수 있다.

 

그러나 원화 계좌 서버와 외환 계좌 서버가 분리되고 각각 분리된 데이터베이스를 바라보고 있다면 출금 및 입금 로직 중 하나라도 실패한다면 큰 장애가 발생할 수 있다. 그렇기 때문에 이러한 분산 환경에서도 트랜잭션 원자성을 보장하기 위해 분산 트랜잭션이 구현되어야 한다.


분산 트랜잭션

분산 트랜잭션을 구하기 위해 구현하기 위해 아래 2개의 패턴이 사용된다고 한다.

 

Two Phase Commit 부터 알아보자.

 

1. Two Phase Commit(2PC)

이름과 같이 크게 두 단계로 나누어 진행되는 트랜잭션이다.

  1. 첫번째로 투표 단계를 거친다. Coordinator가 트랜잭션 참여자(데이터베이스들)에게 Commit 가능 여부를 질의 하고 각 트랜잭션 참여자들은 트랜잭션을 열고 commit 참여 여부를 반환한다.
  2. 두번째로  모든 트랜잭션 참여자들이 commit 가능이라고 응답한 경우 Coordinator가 commit 요청을 보내고 트랜잭션을 성공적으로 종료한다.
  3. 그러나 이때 하나의 참여자라도 commit 불가라고 응답한다면 Coordinator는 rollback요청을 통해 트랜잭션을 실패로 종료한다.

즉, 2PC는 모든 참여자가 트랜잭션을 커밋하거나 롤백하도록 강제하여 트랜잭션의 일관성을 보장

! 2PC의 단점 !
2PC는 트랜잭션 참여자가 많을수록 확장성이 떨어진다. 
트랜잭션을 진행하는 동안 모든 참여자가 동기화되어야 하므로, 트랜잭션 참여자가 많아지면 처리 시간이 길어지고, 트래픽이 많을 경우 성능 저하가 발생할 수 있다.
또한 모든 참여자가 결과를 결정하기 전까지 리소스를 잠그기 때문에, 트랜잭션이 길어지면 잠금 시간이 증가하고 시스템 자원이 낭비된다.

2. Saga Pattern

해당 방식을 각 서비스의 독립된 작은 트랜잭션들을 실행하면서 진행하는데 이때 특정 단계에서 실패하면 보상 트랜잭션을 실행한다.

 

 

강한 일관성을 가진 2PC보다는 트랜잭션 참여자를 추가적으로 확장성 있게 가져갈 수 있으며 트래픽이 클 경우 비동기적으로 처리할 수 있고 확장성 있는 Saga가 더 적합하다.

 

Saga에는 크게 두가지 방식 Choreography Saga(코레오 그래피 사가)와 Orchestration Saga(오케스트레이션 사가)가 있다.

 

Choreography Saga

Choreography Saga의 경우 중앙 제어자 없이 메시지 브로커를 통해 이벤트를 교환하며 진행하는 방식이다.

  • 중앙 제어자가 없기 때문에 단일 장애점이 없고 느슨한 결합이 될 수 있다.
  • 그렇지만 현재 진행 중인 트랜잭션에 대해 상태 추적 및 디버깅은 힘들 수 있다.

 

Orchestration Saga

Orchestration Saga는 Orchestrator가 각 서비스들에게 트랜잭션과 보상 트랜잭션을 명령하며 진행하는 방식이다.

  • Orchestrator가 단일 장애점이 된다.
  • 모든 서비스들이 결합된다.
  • 현재 진행 중인 트랜잭션을 추적하기 쉽다.

Saga 구현

 

항상 입 출금 트랜잭션이 동시에 성공하면 좋겠지만 항상 그럴 순 없다.

실패 시 아래와 같은 이유가 있을 수 있다.

입출금 실패

우선 정상적인 실패부터 살펴보자.

1. 환전 실패 - 출금 실패

: 추가적인 입금 요청 없이 트랜잭션 종료

 

2. 환전 실패 - 입금 실패

이미 출금이 된 상태이기 때문에 보상 트랜잭션이 필요하다.

이미 1300원이 출금된 이후에 1달러의 입금이 실패한 것이기 때문에 1300원을 다시 유저에게 돌려줘야한다.

 

그렇다면 왜 굳이 꼭 출금부터 해야하나?
Saga Pattern의 특징으로 중간 상태가 노출된다.
입금된 돈이 빠져나갈 수 있기 때문에 입금 취소는 자체적으로 처리하기 힘들 수 있다.

 

입출금 요청 시에 HTTP를 사용할지 메시징(이벤트 기반)으로 처리를 할지 선택 할 수 있다.

 

토스에서는 입금과 출금은 HTTP로 구현을 했는데 이는 출금 결과를 알고 입금으로 넘어가야하기 때문에 동기 처리가 필요했고 유저는 환전이 즉시 완료되기를 기대하기 때문에 타임아웃 구현이 필요하기 때문이다.

만약 입출금이 지연되는 경우 환전이 지연됐다고 유저에게 알려줘야하는데 이 경우 타임아웃이 필요하다.

타임아웃을 비동기 메시징으로 구현하기 위해서는 입출금 결과를 다시 메시지로 받고 폴링하는 등 구현이 복잡하기 때문에 HTTP 동기 방식이 유리하다.

 

그러나 출금 취소는 Messaging으로 구현했다. 출금 취소는 마지막 과정이고 유저가 기다릴 필요가 없기 때문(비동기)에 출금 취소에 에러 핸들링을 하기 싫고 결과적으로 정합성은 보장할 수 있기 때문이다.


ErrorHandling

비정상적인 실패 시 상태 트래킹 후 추가적인 후속 처리 필요하다.

 

그렇다면 요청을 보낸 서버가 정상적으로 결과를 반환하지 못한다면?

 

Kafka Message Scheduler를 활용한다!

메시지를 지연시켜 발행할 수 있는 것인데

일반적으로 메시지가 발행되면 바로 메시지 브로커에 토픽을 전달하고 컨슈머가 구독하게 된다.

그러나 지연 시간을 넣어 메시지를 발행한 경우 별도의 지연 토픽으로 메시지가 Kafka Message Scheduler로 전달되게 된다.

Kafka Message Scheduler는 지연시간만큼 보낸 후 원래의 토픽을 메시지를 재발행하여 지연 시간 이후 컨슈머 서버가 소비할 수 있도록 한다.

이때, 만약 Producer와 Consumer가 모두 환전 서버가 된다면 특정 동작을 지연 시간만큼 뒤로 예약할 수 있다. 이 기능을 통해 상대 입출금 계좌 서버에 회복할 시간을 벌어줄 수 있다.

 

예를들어 환전서버가 원화 계좌서버로 부터 출금 결과 확인을 실패한 경우 그 즉시 재 확인하는 것이 아니라 Kafka Message Scheduler를 통해 30초만큼 환전을 지연시킨 후 출금 결과 확인을 재시도 할 수 있다.

만약 다시 실패한 경우 1분 뒤에 확인을 하며 이렇게 정해진 횟수 만큼 출금 결과 확인 재시도를 요청할 수 있고 지연 이벤트 발행 시 지연 시간을 점차 늘리는 방식을 통해 원화 계좌 서버가 회복될 시간을 벌어줄 수 있다.

 

만약 정해진 횟수를 넘어간다면? 개발자가 직접 이벤트를 재발행하게 할 수도 있다.


그런데 이때 환전 서버의 Container OOM(컨테이너 오염) 이나 장비결함으로 인해 환전 지연 이벤트를 발행하지 못하고 서버가 죽어버린다면? 이 경우 배치를 통해 재 처리 가능하다고 한다.