이번 글은 Toss Tech에서 발표한 "보상 트랜잭션으로 분산 환경에서도 안전하게 환전하기"를 정리했습니다.
분산 환경이 만들어진 이유
토스 뱅크의 초기 시스템은 거대한 단일 서버가 단일 데이터베이스를 바라보고 있는 모놀리틱 시스템으로 구성되어 있습니다.
기존 코어 뱅킹 서비스에 존재하는 도메인이 분리된 MSA서버로 꺼내고 있습니다.
예시로, 원화 계좌 서비스가 기존과 동일한 코어 뱅킹 데이터 베이스르 바라보고 분리 되는 부분이 있습니다.
외화 계좌와 같이 기존 코어 뱅킹 서비스에 존재하지 않는 신규 상품인 경우에는 DB까지 분리되는 MSA 서버에 개발을 시작합니다.
이로 인해, 환전은 DB와 서버가 분리된 환경에서 구현되어야 했습니다.
환전 서비스는 단일 서버에서 동일한 데이터베이스를 바라보고 있는 경우엔 매우 간단하게 구현이 가능합니다.
트랜잭션을 열고, 각 계좌의 잔액을 업데이트 한 후에 트랜잭션을 커밋하면 해결이 가능합니다.
문제가 생겨 트랜잭션이 롤백되는 경우, 두 계좌의 업데이트 내역도 함께 롤백되기 때문에 트랜잭션의 원자성을 보장해줍니다.
하지만, 원화 계좌 서버와 외화 계좌 서버가 분리되어 있고, 서로 다른 데이터베이스를 바라보고 있는 경우엔 복잡한 과정이 됩니다.
환전 서버가 원화 계좌와 외화 계좌에 입출금 요청을 보낼때, 입금 혹은 출금 중 하나가 실패하는 경우, 은행 또는 고객에게 큰 피해를 줄 수 있습니다. 환전의 원자성을 보장하기 위해서는 분산 트랜잭션이 구현되어야 합니다.
2PC vs SAGA 분산 트랜잭션 비교
분산 트랜잭션을 구현하는 방법에는 2PC와 Saga Pattern이 있습니다.
2PC
Two Phase Commit은 두 단계로 나눠 커밋을 진행하는 방식입니다.
첫번째 단계는 투표 단계로 코디네이터가 각 트랜잭션 참여자들에게 커밋 가능 여부를 질의합니다.
각 트랜잭션 참여자들은 트랜잭션을 열고 커밋 가능 여부를 응답하게 됩니다.
두번째 단계는 커밋 단계입니다. 모든 참여자들이 트랜잭션 커밋 가능이라고 응답하는 경우, 코디네이터가 커밋 요청을 보내 트랜잭션을 성공으로 종료합니다.
단 하나의 참여자가 트랜잭션 커밋 불가능이라고 응답하는 경우, 코디네이터가 트랜잭션 롤백 요청을 보내, 트랜잭션을 실패로 종료하게 됩니다.
SAGA 패턴
사가 패턴은 각 서비스들이 작은 로컬 트랜잭션을 실행하면서 진행하다가 특정 단계에서 실패하는 경우 이전에 커밋된 트랜잭션들에게 보상 트랜잭션을 실행합니다.
두 방식의 차이점을 비교하면 다음과 같습니다.
SAGA 패턴에는 크게 2가지 방식이 존재합니다.
코레오그래피 사가와 오케스트레이션 사가입니다.
코레오그래피 사가는 중앙 제어자 없이 메시지 브로커를 통해 이벤트를 교환하며 진행하는 방식입니다.
오케스트레이션 사가는 오케스트레이터가 각 서비스들에게 트랜잭션과 보상 트랜잭션을 명령하며 진행되는 방식입니다.
코레오그래피 사가는 중앙 제어가 없기 때문에, 단일 장애점이 없고, 각 서비스들이 느슨하게 결합된다는 장점이 있습니다.
하지만, 현재 진행중인 트랜잭션의 상태를 추적하거나 디버깅하기에는 어렵다는 단점이 있습니다.
오케스트레이션 사가는 오케스트레이터가 단일 장애점이 되고, 모든 서비스들이 결합된다는 단점이 있습니다.
하지만, 현재 진행중인 트랜잭션의 상태를 추적하기가 쉽다는 장점이 존재합니다.
채택한 방법 : Orchestration Saga
오케스트레이션 사가를 채택한 이유는 클라이언트 요청을 받아 환전을 해주는 환전 서버가 필요했고, 현재 진행중인 환전들의 상태를 관리해야 했기 때문입니다. 예를 들어, 환전 한도를 구현하기 위해선 현재 진행중인 환전의 금액과 상태도 추적이 필요합니다.
Saga를 이용한 환전 구현
환전 성공 케이스
환전을 위해서 원화 계좌 서버로 1,300원 출금 요청을 보냅니다.
출금이 성공하면, 외화 계좌로 1달러 입금 요청을 보냅니다.
입금까지 성공하는 경우 환전이 성공적으로 종료가 됩니다.
입출금 실패 케이스
입출금 실패는 정상적인 실패, 비정상적인 실패 케이스가 존재합니다.
정상적인 실패 - 출금 실패
이 경우는 간단하게 추가적인 입금 요청 없이 환전을 실패로 마무리할 수 있습니다.
정상적인 실패 - 입금 실패
1,300원 출금은 성공했지만, 외화 계좌에 1달러 입금은 실패합니다.
이러한 경우, 출금 했던 1,300원을 보상 트랜잭션을 통해 사용자에게 다시 돌려주어야 합니다.
따라서, 보상 트랜잭션인 출금 취소(입금)을 원화 계좌 서버로 보내줍니다.
출금부터 진행하는 이유
출금부터 진행하는 이유는 Saga패턴의 특징에 있습니다.
Saga패턴은 일부 트랜잭션만 커밋된 중간 상태가 노출되게 됩니다. 즉, 이 패턴은 트랜잭션을 순차적으로 따로 실행하기 때문에, 전체 과정이 끝나기 전까지 일부 서비스에서는 트랜잭션이 커밋되어 버릴 수 있습니다.
출금부터 먼저 진행하는 이유는, 문제가 생겼을 때 더 안전하게 되돌릴 수 있기 때문입니다.
만약 출금이 먼저 성공하고, 그 다음 입금에서 실패한다면 사용자의 계좌에서 돈은 빠졌지만 상대방에게 돈이 가지 않았습니다.
이럴 경우엔, 은행이 다시 출금한 금액만 사용자에게 돌려주면 되기 때문에 문제 해결이 비교적 단순합니다.
하지만 반대로, 입금을 먼저 하고 출금이 실패했다면 상황이 훨씬 복잡해집니다.
이 경우, 상대방의 계좌에는 이미 돈이 들어온 상태가 됩니다. 그러면 상대방은 돈을 받은 줄 알고 사용할 수도 있고,
은행은 이미 입금된 돈을 다시 회수하기 어려워집니다. 법적 문제나 신뢰 문제도 생길 수 있습니다.
Saga패턴은 이러한 부분 커밋 상태가 외부에 잠깐이라도 보이는 것을 피할수는 없기 때문에, "어떤 작업이 먼저 성공하도록 설계할 것인가"가 중요합니다. 해당 기준은 실했을 때 더 위험한 작업은 나중에 하는 것입니다.
입출금 요청에 사용하는 통신 방식
HTTP vs Messaging
입금과 출금에 사용되는 방식
입금과 출금에는 HTTP 방식을 사용합니다. 그 이유는 아래와 같습니다.
1. 출금 결과를 알고 입금으로 넘어가야 합니다.
2. 유저는 환전이 즉시 완료되기를 기대합니다.
3. 타임 아웃을 구현해야 하기 때문에
출금에 사용되는 방식 Messaging
보상 트랜잭션인 출금 취소는 메시징 방식을 사용합니다. 그 이유는 아래와 같습니다.
- 출금 취소는 마지막 과정이므로, 유저가 기다릴 필요가 없습니다.
- 출금 취소에 에러 핸들링을 하기 싫습니다.
- 결과적 정합성만 보장하면 문제가 없습니다
에러 핸들링
HTTP Error Handling(출금 요청)
환전 서버에서 원화 계좌 서버로 출금 요청을 보내는데, 서버 에러나 타임 아웃이 나게 되면 이는 정상적인 실패와 동일하게 보긴 어렵습니다.
이러한 에러가 발생하는 경우에는 출금 결과를 다시 확인하여 결과에 따른 후속처리를 하게 됩니다.
예시로, 출금이 성공한 경우라면 보상 트랜잭션으로 출금 취소 처리를 하고, 출금이 실패한 경우라면 별다른 조치 없이 환전을 실패처리 할 수 있습니다.
HTTP Error Handling(입금 요청)
출금 성공후, 입금 요청에 에러가 발생한 경우엔 입금 결과를 확인해서 입금이 성공한 경우, 환전을 성공처리하고
입금이 실패한 경우, 앞서 성공한 출금의 출금취소를 해서 환전을 롤백할 수 있습니다.
HTTP 에러 핸들링(입출금 결과 확인 실패)
Kafka Message Scheduler
토스 뱅크에는 메시지를 지연시켜 발행할 수 있는 카프카 메시징 스케줄러가 존재합니다.
일반적인 경우에 Producer가 메시지를 발행하면 그 즉시 Message Broker를 통해 Consumer에게 메시지가 전달됩니다.
그런데 이미지와 같이 지연시간을 넣어 메시지를 발행하는 경우엔 별도의 지연 토픽으로 메시지가 Kafka Message Scheduler로 전달됩니다.
KMS는 지연시간만큼 시간을 보낸 후, 원래 토픽에 메시지를 대신 발행하여 consumer가 지연시간 뒤에 메시지를 가져갈 수 있도록 합니다.
이때, Producer와 Consumer 모두 환전 서버가 된다면, 특정 동작을 지연 시간만큼 뒤로 예약한 효과가 됩니다.
이 기능을 활용해 입출금 계좌 서버에 회복할 시간을 줄 수 있습니다.
예시로, 환전 서버가 원화 계좌 서버의 출금 결과 확인을 실패한 경우 그 즉시 재확인을 시도하는 것이 아니라 Kafka Message Scheduler를 통해 30초만큼 환전을 지연시킨 후에 출금 결과 확인을 다시 시도할 수 있습니다.
또 실패하는 경우, 지연 시간을 1분으로 늘려서 출금 결과 확인을 재시도할 수 있습니다. 이는 원화계좌 서버에게 회복할 시간을 조금 더 주는 효과가 됩니다.
정해진 횟수를 모두 초과해서 실패하는 경우엔 개발자 메시지를 확인한 후, 수동으로 발행해서 출금 결과를 확인할 수 있습니다.
그렇다면, 환전 지연 메시지를 발행하지도 못하고 죽는 경우(장비 결함이나 컨테이너의 OOM으로 환전 서버가 죽는 경우)엔 어떻게 해야할까요?
Batch 재처리
이러한 경우엔 Batch를 사용해서 재처리를 할 수 있습니다.
오케스트라된 환전 서버가 환전 전선의 마지막 상태를 저장하고 있기 때문에 중단된 상태부터 환전 재시작이 가능합니다.
예를 들어 출금이 성공하고 멈춰버린 환전의 경우에는 배치가 출금 취소 메시지를 발행한 후에 환전을 실패 처리할 수 있습니다.
입출금 요청에서 에러가 발생했을 때 기본적으로 입출 경과를 다시 확인하여 처리하고 입출금 결과 확인해도 실패했을 때는 환전 지연을 통해서 계좌 서버에게 회복할 시간을 주게 됩니다.
그런데 이런 환전 지원까지 못했을 경우에는 최종적으로 배치를 통해 환전을 재시작할 수 있습니다.
입금이 실패하면 출금된 돈은 꼭 유저에게 돌려주어야 합니다.
만약, 원화 계좌가 출금 취소 메시지를 처리하다가 에러가 발생하면 어떻게 해야 할까요?
토스 뱅크는 컨슈머 데드 레터를 통해 카프카 메시지의 결과적 정합성을 보장하고 있습니다.
원화계좌 서버가 출금 취소를 처리하다가 에러가 발생하면, 컨슈머 데드 레터 메시지 브로커를 통해 메시지를 DL 서버로 전달합니다.
그리고 이 DL 서버는 정해진 재시도 횟수와 간격으로 서비스 메시지 브로커로 메시지를 다시 전달하여 원화 계좌의 출금 취소 처리를 재시도 합니다.
만약 정해진 횟수를 모두 실패하는 경우엔 이전가 마찬가지로 개발자가 코드 수정 등 원화계좌 서버의 정상화를 확인한 후에 DL 서버를 통해 다시 메시지를 발행할 수 있습니다.
Saga에서는 Local Transaction과 메시지 발행이 원자적으로 이루어져야하며 이것을 트랜잭셔녈 메시징이라고 부릅니다.
만약, 서비스 메시지 브로커 장애 등으로 메시지 발행 자체가 안되는 경우, 어떻게 보장이 가능할까요?
트랜잭셔널 메시지를 보장하는 방법에는 트랜잭셔널 아웃박스 패턴 등 다양한 방법들이 알려져 있지만, 토스 뱅크에서는 프로듀서 데드 레터를 이용하고 있습니다.
환전 서버가 서비스 메시지 브로커로 브로커의 장애로 메시지 발행이 실패했을 경우에는 프로듀서 메시지 브로커로 메시지를 대신 발행하여 DL 서버로 전달합니다.
그리고 이 DL 서버는 일정 시간 수련 후 회복된 서비스 메시지 브로커로 메시지를 다시 전달하여, 원화계좌 컨슈머가 가져갈 수 있도록 합니다.
모니터링
오케스트레이션 사가에서 오케스트레이터는 각 트랜잭션 상태별 명령이 정해져 있기 때문에, 스테이트 머신으로 나타내곤 합니다.
환전 플로우는 위 이미지와 같은 스테이트 머신이 됩니다.
데이터는 위 이미지 처럼 적용이 됩니다.
환율, 환전 금액 등 환전 요청은 exchange_request라는 테이블에 스냅샷 형태로 지정됩니다. 그리고 환전이 거쳐가는 상태들은 exchange_state_log 테이블에 변경이 아닌 추가 삽입으로만 적용됩니다.
이렇게 저장했을때의 장점은 현재 상태 뿐만 아니라, 환전이 거쳐간 모든 상태를 확인 가능하다는 점입니다.
예를 들어, 환전 시작 후 출금 실패로 끝난 환전과 출금 성공 후 입금이 실패하여 출금이 취소된 환전을 구분하여 모니터링이 가능합니다.
중간에 멈춰버린 환전이 없다는 것도 모니터링이 필요한데, 환전 서버는 오케스트레이터이기 때문에 모든 환전들의 상태를 관리하고 있습니다. 따라서 일정 시간이 흐른 후에도 끝나지 않은 환전들을 탐지할 수 있습니다.
이러한 환전을 발견하는 경우엔, 개발자들에게 alert을 통해 알려주게 됩니다.
그리고 계좌 서버들은 입출금의 쌍이 맞는지를 비교할 수 있습니다.
계좌 서버들은 각각 입출금 하나씩만 알 수 있기 때문에, 이 두개를 같이 보는 것이 필요합니다.
원화계좌 입출금 내역과 외화계좌 입출금 내역은 해당 틀을 통해 분석 목적으로 정보계 데이터베이스에 주기적으로 적재됩니다.
해당 정보계 데이터베이스를 바라보는 배치가 입출금을 묶어주는 키로 쌍이 맞는지를 지속적으로 체크할 수 있습니다.
이것을 통해 입금만 되었거나 출금만된 환전을 탐지할 수 있습니다.
결론 및 성과
토스 뱅크에서는 매일 발생하는 입금과 출금을 기록하여 회계 처리를 하게 됩니다.
회계 서버가 환전 트랜잭션의 참여자로 추가된다고 볼 수 있습니다.
만약, 이것을 2PC으로 구현을 하게 되면, 원화 계좌 서버와 외화 계좌 서버가 트랜잭션을 열고, 회계 서버의 투표까지 기다리는 등 가용성 성능 문제가 있습니다.
회계 서버에 장애가 났을 때 유저가 환전을 못하는 등에 서비스간 간결한 문제도 존재합니다.
사가 패턴을 사용했기 때문에, 원화 계좌 서버와 외화 계좌 서버가 메시지를 발행하는 방식으로 쉽게 확장할 수 있습니다.
돈 자동환전 결제 시점에 외화가 부족한 경우, 실시간 환율로 환전하여 결제하는 서비스입니다.
카드 서버에서 환전 서버를 이용하여 환전을 한 후에 충전된 외화로 출금하여 결제를 승인하게 됩니다.
이 경우에도 카드가 환전 시스템을 환전 saga를 이용하여 쉽게 확장했습니다.
사가를 활용하여 추가적인 확장에 유리한 환전 시스템을 갖게 됩니다.
출처
'빅테크 글 읽기' 카테고리의 다른 글
[Toss Tech] Java Native Memory Leak 원인을 찾아서 (0) | 2025.04.25 |
---|---|
[Toss Tech] 캐시를 적용하기 까지의 험난한 길 (TPS 1만 안정적으로 서비스하기) (0) | 2025.04.11 |
[Toss Tech] 토스증권 실시간 시세 적용기 (0) | 2025.04.04 |
[Toss Tech] 토스 서비스를 구성하는 서버 기술 소개 (0) | 2025.03.27 |
[Toss Tech] 서버 증설 없이 처리하는 대규모 트랜잭션 (0) | 2025.03.26 |