Search
📚

[SLASH 24] 보상 트랜잭션으로 분산 환경에서도 안전하게 환전하기

Tags
Architcture
Meet Up / Conference
Study
Last edited time
2024/11/14 08:39
2 more properties

1. 분산 환경이 만들어진 이유

1.1. 모놀리틱 시스템의 한계

시스템과 개발 규모가 커질수록 배포 및 개발 경험, 확장성, 단일 장애점 문제 발생
MSA로의 전환 시도

1.2. 환전 시스템

MSA 전환으로 외화 계좌와 원화 계좌 서버 및 DB가 분리되어있음
만약 모놀리틱 시스템이었다면? 단순하게 구현 가능
원화 계좌 서버, 원화 계좌 서버 분리되어있어서 트랜잭션 보장 과정이 복잡
입금 / 출금 중 하나 실패 가능
환전 원자성 보장 → 분산 트랜잭션 보장 필요

2. 2PC vs SAGA 분산 트랜잭션 비교

2.1. 2PC: Two Phase Commit

2단계로 나뉘어 커밋이 진행됨
1단계: 투표
커밋 가능 여부 질의
각 트랜잭션 참여자가 트랜잭션 열고 커밋 가능 여부 응답
2단계: 커밋
모든 참여자가 커밋 가능 하다고 응답하면 → 트랜잭션 성공
단 하나의 서비스라도 불가능 응답 → 커밋 롤백 요청 & 트랜잭션 실패

2.2. SAGA

각 서비스들이 작은 로컬 트랜잭션에서 진행
특정 단계에서 실패시 보상 트랜잭션 처리
2PC와의 비교
2PC
Saga
장점
강한 일관성
높은 가용성 높은 확장성
단점
낮은 가용성 낮은 확장성
중간 상태 노출 보상 트랜잭션 구현 필요
토스에서는 Saga 선택
환전 서비스가 높은 트래픽을 견뎌야함
카드나 회계든 다양한 트랜잭션 참여자가 추가될 수 있음

2.3. SAGA 패턴 구현 방식 선정

비교
Choreography Saga
Orchestration Saga
주요 특징
중앙제어자 없이 메세지 브로커 이용
오케스트레이터가 각 서비스들에게 트랜잭션과 보상 트랜잭션을 명령하면서 진행
장점
중앙제어가 없으므로 단일 장애점이 없고 각 서비스들이 느슨하게 결합
현재 진행중인 상태 추적 쉬움
단점
현재 진행중인 트랜잭션 상태 추적 및 디버깅 어려움
오케스트레이터가 단일 장애점이 됨 모든 서비스에게 결합됨
도식화
토스에서는 Orchestration Saga 선택
클라이언트 요청을 받아 환전을 시작하는 환전 서버가 필요했음
현재 진행중인 환전 상태 관리 필요
예) 환전 한도를 고려한 환전 구현 → 현재 진행중인 환전 금액 & 상태 추적 필요
환전 서버가 오케스트레이터가 됨

3. SAGA를 이용한 환전 구현

3.1. 환전 성공

환전 서버가 원화계좌로 부터 출금 요청 → 출금 성공
외화계좌로 1달러 입금 요청 → 입금 성공
입금 성공 → 환전 최종 성공 후 종료

3.2. 환전 실패

정상적인 실패
잔액 부족 / 계좌 해지 → 입출금 실패 → 정상 동작
비정상적인 실패
서버 에러, 네트워크, 타임아웃
정상적인 환전 실패
출금 실패
문제 없음
추가적인 입금 요청 없이 환전 실패 처리 가능
입금 실패
보상 트랜잭션 필요
예) 출금 성공 후 입금 실패 한 경우, 출금 취소 보상 트랜잭션 필요
왜 항상 출금부터 처리할까? - Saga Pattern의 특징: 중간 상태가 노출됨 - 입금 부터 시작하면 환전이 끝나기 전에 다른 트랜잭션에서 입금된 금액을 출금해 갈 수 있음

3.3. 환전에 사용된 2가지 통신 방식

HTTP
Messaging (Kafka)
동기
비동기
직관적, 비교적 구현이 간단
서비스 간 느슨한 결합
추가적인 에러 핸들링 필요 (5XX, timeout)
메시지 브로커 레벨에서 에러 핸들링
Client에서 read timeout 두기 편함
응답/timeout 구현 복잡

3.3.1. 입금, 출금: HTTP

1.
입출금 결과를 알고 넘어가야함 (동기)
출금 결과를 알고 입금으로 넘어가야함
2.
유저는 환전이 즉시 완료되기를 기대함
환전 결과가 너무 지연된 경우 타임아웃 구현 필요
타임아웃을 비동기 메세징을 구현하려면, 추가 폴링 구현 등 구현 복잡도가 높아짐

3.3.2. 출금 취소: Messaging

보상 트랜잭션인 출금 취소는 메세징 방식 사용
출금 취소는 환전의 마지막 과정
유저가 기다릴 필요가 없음 (비동기)
출금 취소에 에러 핸들링 하기 싫음
메세지 브로커와 원화계좌 컨슈머에게 책임을 위임하여 결과적 정합성 보장

4. 에러 핸들링: 비정상적인 환전 실패 처리

4.1. 입출금 경과 다시 확인 (HTTP Error Handling)

환전 서버 → 원화 계좌 서버 출금 요청 시 서버에러(5xx) /타임아웃
정상적인 실패라고 동일하게 보기 어려움
입출금은 성공했는데, 그 이후에 에러가 발생했거나, 지연되서 처리되었을 수도 있음
출금
출금 결과를 다시 확인하여 그 결과에 따라 후속 처리 진행
출금 성공 확인 → 보상 트랜잭션인 출금 취소 처리
출금 실패 확인 → 추가적인 처리 없이 환전 실패 처리
입금
출금이 처리와 마찬가지로 입금 결과를 다시 확인하여 후속 처리 진행
입금 성공 확인 → 환전 성공 처리
입금 실패 확인 → 보상트랜잭션인 출금 취소 처리

4.2. 입출금 결과 확인 실패 처리 (Kafka Message Scheduler)

서버 이슈나 네트워크 문제인 경우에 결과 확인 요청도 못하는 경우는 Kafka Message Scheduler 활용
카프카 메시지를 지연시켜 발행할 수 있는 서버
지연시간을 넣어 메세지 발행
별도의 지연 토픽으로 카프카 메세지 스케쥴러로 전달
지연 시간만큼 지난 후, 원래 토픽 메세지를 대신 발행하여 컨슈머가 지연 시간 뒤에 메세지를 가져갈 수 있도록 구현
프로듀서/컨슈머 둘다 환전서버가 된다면 특정 동작을 지연시간만큼 뒤로 예약하는 효과가 됨
상대 입출금 계좌서버에 회복할 시간을 줄 수 있음
예) 환전 서버에서 원화 계좌서버로 출금 결과 확인 실패한 경우
즉시 재확인 시도 X
카프카 메세지 스케쥴러를 통해 30초 만큼 환전을 지연시킨 후에 출금 결과 확인 다시 시도
재실패한 경우 지연시간 늘려서(1분) 다시 체크
원화 계좌 서버에게 회복할 시간을 조금 더 주는 효과 발생
정해진 횟수를 모두 초과한 경우, 개발자가 문제 해결을 확인 후 직접 수동을 메세지를 발행하여 출금 결과 확인 가능

4.3. 환전 지연 실패 (배치 재처리)

환전 서버의 문제로 환전 지연 이벤트를 발행하지 못하고 서버가 죽는 경우 예) 장비결함, Container OOM → 배치 재처리
오케스트레이터에 저장된 중간 상태를 통한 복구
오케스트레이션하는 환전 서버가 환전 트랜잭션의 마지막 상태를 가지고 있음 → 중단된 상태부터 환전 재시작 가능
예) 출금이 성공한 후 멈춰버린 환전의 경우 배치가 출금 취소 메세지 발행한 후 환전 실패 처리

4.4. 요약

입출금 요청에서 에러 발생 → 입출금 경과를 다시 확인하여 처리
입출금 결과 확인 실패 → 환전 지연(Kafka Message Scheduler)을 통해 계좌 서버에게 회복할 시간을 줌
환전 지연 실패 → 최종적으로 배치 통해 환전 재시작 처리

5. 기타 에러 핸들링

5.1. Eventually Consistency: CDL

입금이 실패하면 출금된 유저에게 돌려줘야함
원화계좌가 출금 취소를 처리하다가 에러가 발생한 경우 → CDL(Consumer Dead Letter) 를 통해 카프카 메세지 결과적 정합성 보장
예) 원화 계좌 서버가 출금 취소를 처리하다가 에러 발생한 경우
1.
CDL 메세지 브로커를 통해 DL 서버로 전달
2.
DL 서버는 정해진 재시도 횟수와 간격으로 서비스 메세지 브로커로 메세지를 다시 전달
3.
원화계좌의 출금취소 재시도를 실행
4.
만약 정해진 횟수를 모두 실패한 경우 개발자가 DL서버를 통해 다시 메세지 발행 가능

5.2. Transactional Messaging: PDL

Saga에서는 Local Transaction commit과 message 발행이 원자적으로 이루어져야함
즉 입금 실패로 인한 환전 실패 처리와 출금 취소 메세지 발행은 항상 같이 이루어져야함
서비스 메세지 브로커 장애로 메세지 발행 자체가 안되는 경우 어떻게 보장할 수 있을까?
트랜잭셔널 메세징 보장 방법
Transactional Outbox Pattern
Producer Dead Letter (PDL) ← 토스에서 사용한 방식
예) 환전서버가 서비스 브로커 장애로 메세지 발행을 실패한 경우
1.
PDL 메세지 브로커로 메세지 발행 후 DL 서버로 전달
2.
DL 서버는 일정 시간이 지난 후, 회복된 서비스 메세지 브로커로 메세지를 다시 전달
3.
원화 계좌 컨슈머가 가져 갈 수 있게 함 → 출금 취소 처리 가능

6. 모니터링

6.1. 상태머신 (State Machine)

Orchestration Saga 에서는 각 트랜잭션 상태별 명령이 정해져있음 → 상태머신으로 나타낼 수 있음

6.2. 데이터베이스: State Path

exchange_request
환율, 환전 금액 등 환전 요청이 스냅샷 형태로 저장됨
exchange_state_log
환전이 거쳐가는 상태. insert의 형태로 저장
현재 상태 뿐만 아니라 환전이 거처간 모든 상태 확인 가능
예를 들어 아래 2가지 케이스도 구분 가능
환전 시작 후 츨금 실패로 끝난 환전 (스크린샷 좌측 예시)
출금 성공 후 입금 실패 후 출금이 취소된 환전(스크린샷 우측 예시)

6.3. 모니터링

6.3.1. 중간에 멈춰버린 환전

환전서버는 오케스트레이터이므로, 모든 환전 서버의 상태를 관리함
일정 시간이 지난 후에도 나지 않은 환전을 탐지 → 개발자들에게 Alert

6.3.2. 계좌 서버 입출금 최종 정합성 확인

계좌서버들에 입출금의 쌍이 맞는지 비교 가능
계좌서버는 각각 입/출금 하나씩만 알 수 있음. 두개를 같이 보는것이 필요
원화 / 외화 계좌 입출금 내역은 ETL 을 통해 분석 목적의 정보계 DB에 주기적으로 적재됨
정보계 DB를 바라보는 배치가 입출금을 묶어주는 키로 쌍이 맞는지 지속적으로 확인 → 입금만 되었거나, 출금만 된 환전 탐지 가능

7. 결론 및 성과

7.1. 또다른 Saga: 회계

Saga 패턴의 장점인 확장성을 통해 회계 관련 기능 쉽게 처리
토스에서는 매일 발생하는 입/출금을 기록하여 회계 처리 필요
회계 서버가 환전 트랜잭션의 참여자로 추가 된다고 볼 수 있음
만약 2PC로 구현할 경우?
가용성 및 성능 문제 발생
원화 / 외화 계좌 서버가 트랜잭션을 열고 회계 서버의 투표까지 기다려야함
서비스간 강결합 문제
회계서버 장애 발생 시 유저가 환전을 못함
Saga 패턴
원화 계좌 서버와 외화 계좌 서버가 메세지 발행하는 방식으로 쉽게 확장 가능

7.2. 부족한 돈 자동환전 결제

부족한 돈 자동환전 결제 서비스
결제시점에 외화가 부족한 경우, 카드로 실시간 환율로 환전하여 결제하는 서비스
카드가 환전 Saga를 이용하여 쉽게 확장 가능

7.3. Trade Off

Pros
MSA로의 전환, 기존 계정계 시스템이 가지는 문제 해결
복잡한 개발 및 배포 경험
낮은 확장성
SPOF(단일장애점)
계정계 시스템과의 느슨한 결합
확장 가능한 구조
Cons
트랜잭션 구현 복잡도 증가

7.4. 결론

요약
외화 예금을 MSA 서버에 구축하면서 분산된 환경 생성
SAGA를 사용하여 높은 가용성을 유지하면서도 분산 트랜잭션을 보장
계좌 입출금 서버에 문제가 생겨도 자동으로 회복
기존 계정계 시스템과 연결 및 확장 가능한 구조
결론
출시 후 3개월간 약 100만 계좌 개설
5조 2천억 거래량 소화

8. Reference

8.1. 번외: PDL에 대한 질의응답

내부적으로 메시징 발행 재처리가 DL 중심으로 잡혀 있는지?
예상하신대로 토스뱅크는 모든 토픽에 대하여 기본적으로 PDL이 적용되어 있음
따라서 추가적인 아웃박스 패턴 등을 적용하지 않았음
DL 서버에서 메시지 발행에 실패하면 어떻게 되는지?
DL 서버의 PDL 메시지 처리도 컨슈머로 동작하므로, 발표에서 설명드린 CDL을 이용하여 재처리할 수 있음
PDL(Producer Dead Letter)이 아웃박스 패턴보다 나은 점은 무엇인지?
질문
트랜잭셔널 아웃박스 패턴 사용하면 출금 취소 메시지 발행 실패를 방지 가능함
환전DB 변경과 메시지 페이로드를 동일 트랜잭션으로 묶어 outbox 테이블에 저장
배치를 통해 메시지를 발행하여 유실 방지 가능
PDL 방식을 사용하는 경우의 개발 복잡도 증가
메시지 브로커 간 발행 실패 또는 DLQ 처리 실패 발생 가능성
복잡성 증가로 인해 별도의 설정이나 해결 방법 필요
답변
PDL/CDL의 장점
플랫폼 팀에서 제공하므로 모든 서비스에 일괄 적용이 용이함
아웃박스 패턴의 단점
서비스 변경과 아웃박스 데이터 저장을 트랜잭션으로 묶어야 하며, 각 서비스 DB에 아웃박스 테이블이 필요함
토스뱅크는 수백 개의 MSA 서버가 독립된 스키마를 사용하며, 다양한 DB(Oracle, MySQL, Mongo 등)와 수십 개의 물리 서버에 그룹화되어 있어 일괄 적용이 어려움
각 DB 종류, 물리 서버, 스키마마다 아웃박스 테이블과 메시지 발행 애플리케이션을 만들어야 함.
반면, PDL은 모든 서버에서 동일한 메시지 브로커를 바라보므로 라이브러리 형태로 제공되어 일괄 적용이 편리함
PDL 메시지 브로커도 고가용성이 보장되는지, 그렇다면 불필요한 인프라 비용이 생기는 것은 아닌지?
PDL 메시지 브로커로 서버에서 로그를 남길때 사용하는 로그 Kafka 클러스터를 사용하고 있음
즉 고가용성을 위한 세팅이 적용되어 있으며 평상시에도 항상 사용하고 있음
따라서, PDL 메시지가 발행되지 않더라도 메시지 브로커에 문제가 생기면 그 즉시 알게 됨