목차
1. 개요
2. 서버가 다운되면 무슨 일이 일어날까?
- 기존 타행 이체 순차 다이어그램
- 서버 다운 가능 지점
3. Transactional Outbox Pattern
- 이벤트 발행 패턴 선정
- 아웃박스 패턴을 적용한 순차 다이어그램
- 서버 다운 check, 아웃박스 패턴을 적용한 순차 다이어그램
- 이체 입금 요청 API의 멱등성
4. 구현
✍️ 개요
이체 기능을 설계하면 가장 신경 쓴 것은 데이터 정합성이였습니다. 키워드의 검색 횟수 같은 데이터는 정합성이 완전하게 맞춰질 필요가 없을 수 있습니다. 하지만 돈과 같은 경우는 1원이 없어지더라도 이는 결국 회사 혹은 고객의 돈이 없어지는 것 이기 때문에 큰 문제로 이어질 수 있습니다. 그렇다면 돈은 언제 없어질까요?
돈은 이체 하는 과정에서 해피케이스가 아닌 언해피 케이스에 돈이 없어집니다. 예를 들면, 출금이 됐는데 입금이 안된 상황 혹은 입금은 됐는데 출금이 안된 상황이 있겠죠. 혹은 출금이 두번되는 상황도 존재합니다. 이러한 상황은 서버가 다운되는 상황에서 대표적으로 일어난다고 판단했습니다.
물론 서버가 다운되더라도 트랜잭션에 의존한다면 ACID로 인해 출금과 입금 둘다 되거나 둘다 안되거나 하는 상황을 만들 수 있습니다. 하지만 타행 이체에서는 서로 다른 서비스의 트랜잭션을 관리해야 합니다. 두 서비스는 서로가 어떤 DB를 쓰는지도 모르기 때문에 API 규약 만으로 서로 다른 서버에 있는 트랜잭션을 관리해야 하죠. 자 이제 서버가 다운되면 어떻게될지 알아보죠.
✍️ 사전 지식
용어
- 당행 : 사용자로부터 이체 요청을 받고, 이체 입금을 진행하는 은행
- 타행 : 이체 과정에서 출금을 진행하는 은행
- 당행 이체 : 입금 은행과 출금 은행이 동일한 이체
- 타행 이체 : 입금 은행과 출금 은행이 다른 이체
- OutBox 데이터 : 이벤트나 메시지를 DB에 있는 아웃박스에 저장해서 DB 트랜잭션의 일부로 발행하는 패턴을 Transactinal OutBOx라고 합니다. 서로 다른 서버의 트랜잭션을 관리하고 할 때, eventual consistency를 보장하기 위해 DB 테이블을 메세지 큐로 사용하는 방식을 사용합니다. 이때 DB 테이블에 있는 이벤트 데이터를 OutBox 데이터라고 본 글에선 칭했습니다.
- OutBox polling : DB에 있는 아웃박스를 폴링해서 메시지를 발행하는 방식입니다. Transactional OutBox 패턴에 사용되는 Polling 발행기 패턴입니다.
Transactional OutBox 패턴 Ref : https://thebook.io/007035/0185/
Polling 발행기 패턴 Ref : https://thebook.io/007035/0186/
테스트 환경
- 구체적인 타행 이체 상황 : 타행 이체 중에서도 계좌 A에서 계좌 B로 연속 이체하는 상황을 테스트 했습니다. 주식 트레이드 시스템 처럼 단위 시간 동안 수많은 타행 이체가 일어나는 상황을 가정했습니다.
✍️ 서버가 다운되면 무슨 일이 일어날까?
기존 타행 이체 순차 다이어그램
기존에 설계했던 타행 이체 다이어그램(그림1 참조)입니다. 위 다이어그램에서 주목해야할 점은 2개입니다. 첫째는 두개의 API로 타행 이체를 설계한 것과 두번째는 이체 입금 요청이 왔을 때 '아무 로직도 처리하지 않고 200을 먼저 응답'하고 이체 입금을 처리한다는 것입니다.
두개의 API로 타행 이체를 설계한 이유는 이체 출금 하는 은행이 이체 입금 은행의 입금 로직을 기다리지 않게 하기 위해섭니다. 출금 처리 스레드가 입금 처리 스레드를 기다리지 않기 위해서 입금 스레드가 자신의 로직을 완전히 처리하면 '이체 입금 완료 응답 API'를 호출하는 형태가 되는거죠. 이체 완료 응답 호출은 이체 요청에 담김 callback URL을 이용합니다.
이체 입금 요청이 왔을 때 '아무 로직도 처리하지 않고 200을 먼저 응답'하는 이유는 이체 출금 은행의 스레드가 타행의 db에 묶이지 않기 위해서였습니다. 이에 200을 먼저 응답하고 입금을 처리하도록 설계했죠(물론 이상하다고 생각하실 수 있습니다. 현 설계의 문제점은 밑에서 설명 드릴겁니다.) 또한 이체 출금 은행과 입금 은행과의 규약과도 관련 있습니다. 두 은행은 이체 입금 요청에 대한 응답으로 200이 온다면 '이체 입금 은행이 이체 입금 요청을 제대로 받았으니 책임지고 입금을 수행한 뒤 이체 입금 완료 응답 API를 호출하겠구나'라고 생각하기로 정했습니다. 이에 이체 입금 은행은 '요청을 잘 받았다는 의미'로 200을 바로 돌려주고 자신의 로직(입금)을 수행하게 되죠.
서버 다운 가능 지점
위 설계는 많은 데이터 정합성 관련하여 많은 문제가 있었습니다. 실제로 1,2,3,4,5번에서 서버가 다운될 시 출금만 되고 입금은 안되는 상황, 입금만 되고 출금은 안되는 상황, 출금과 입금 모두 됐는데 이체 내역이 아직 시작인 상황 등 여러 언해피 케이스가 존재했습니다. 결국 사용자 입장에선 돈을 잃어버리거나 제대로 이체(입금, 출금)가 됐음에도 계좌 거래 내역에는 '이체 시작'만 보이는 상황에 놓이게 되죠.
*측정: 인텔리제이 디버깅 모드에서 특정 코드 위치에서 stop 시킨 후, process kill로 검증헀습니다.
본 설계의 문제점은 결국 서로 다른 서버(서비스의 서버)에 있는 트랜잭션을 코드로만 관리하려고 했던 것에 있습니다. 코드의 실행 상태(TCB)는 메모리에 저장할 수가 없습니다. 서버가 다운되면 다 날아가죠. 결국 변하지 않는 것에 의존해야 합니다. DB 그중에서도 RDB는 ACID를 보장합니다. 이에 중간에 서버가 다운되더라도 all or nothing을 보장하죠. 그렇습니다. RDB를 사용하면, 서로 다른 서버에 있는 트랜잭션을 관리할 수 있습니다. 지금부터 이에 대해 알아보겠습니다.
✍️ Transactional Outbox Pattern
트랜잭셔널 아웃박스 패턴은 DB를 큐 처럼 사용하여 데이터의 Eventual Consistency를 보장하는 방식입니다. OUTBOX 테이블에는 이체 입금 요청을 보내야 한다는 정보를 담은 이벤트가 저장됩니다. 이체 입금 요청을 보내야한다는 정보와 이체 출금 로직을 한 트랜잭션으로 묶어서 데이터 일관성을 보장하는 것이죠. 이후 다른 트랜잭션으로 이벤트를 읽어온 후 이체 입금 요청(NIO)를 하게 되면 출금 은행 계좌 lock의 수명이 입금 은행 계좌 lock과의 결합도가 없어집니다. 서로 다른 트랜잭션에서 출금 은행과 입금 은행 계좌의 lock을 요청하기에 결합도가 존재하지 않는거죠.
출처: https://thebook.io/007035/0185/
이벤트 발행 패턴 선정
OUTBOX 테이블에서 이벤트를 발행하는 패턴에는 폴링 발행기 패턴과 트랜잭션 로그 테일링 패턴이 있었습니다. 폴링 발행기 패턴은 성능이 낮지만 구현 난이도가 낮다는 장점이 있었습니다. 그에 반해 트랜잭션 로그 테일링 패턴은 빠르지만 구현 난이도가 비교적 높았습니다. 두 방식으로 모두 구현봐야겠다고 결정했으며, 이에 쉬운 방식인 폴링 발행기 패턴을 먼저 사용했습니다.
폴링 발행기 | 트랜잭션 로그 테일링 | |
구현 난이도 | 낮음 | 비교적 높음 |
성능 | 낮음 | 비교적 높음 |
Note≡ 패턴: 폴링 발행기
DB에 있는 아웃박스를 폴링해서 메시지를 발행한다.43
Note≡ 패턴: 트랜잭션 로그 테일링
트랜잭션 로그를 테일링하여 DB에 반영된 변경분을 발행한다.44
아웃박스 패턴을 적용한 순차 다이어그램
아웃박스 패턴을 바탕하여 새로운 순차 다이어그램을 완성했습니다. 이전 다이어그램과 비교했을 때 달라진 점은 이체 출금하는 스레드와 이체 입금 요청을 보내는 스레드를 분리시킨 것이였습니다. 즉 서로 다른 트랜잭션의 범위에 있는 논리적인 작업을 서로 다른 스레드에서 처리하도록 변경한 것이죠. 그럼에도 두 스레드는 RDB에 저장된 이체 입금 요청 이벤트에 의존하기 때문에 데이터 일관성 문제가 생기지 않습니다.
이체 입금 요청이 일어났을 때 타행(이체 입금 은행)에서 입금을 바로 진행하지 않는 이유는 당행(이체 출금 은행)의 스레드가 타행(이체 입금 은행)의 db 작업(특히 lock)과 강한 결합을 만들지 않기 위해서입니다. 이에 이체 입금 이벤트만을 적재하고 타행(이체 입금 은행)의 스케줄러에서 해당 이벤트를 긁어오는 방식으로 설계했습니다. 이체 완료 응답 호출은 이체 요청에 담김 callback URL을 이용합니다. 자 그렇다면 현 상황에선 서버가 다운되도 문제가 없을까요?
서버 다운 상황 시나리오 재검증
실제로 서버가 다운되도 현 설계에 문제가 없을지 살펴보죠! 문제가 생길 것으로 예상되는 서버 다운 포인트는 <그림5>와 같이 7곳입니다. 각 다운 시나리오을 분석한 결과(아래 목록 참조) '당행은 이벤트를 삭제 하지 못하고 다시 이체 입금 요청 이벤트를 보냄'으로 인한 부작용만 해결해주면 됩니다.
[서버 다운 시나리오]
1번 다운
- 출금, 이체 내역 적재와 이체 입금 요청 이벤트 적재가 한 트랜잭션이기에 전부 rollback 됩니다.(문제 없음)
2번 다운
- 타행은 이체 입금 이벤트를 적재했지만 당행은 다운됐기 때문에 200 응답을 못받음.
- 이에 당행은 이벤트를 삭제 하지 못하고 다시 이체 입금 요청 이벤트를 보냄.
3, 4번 다운
- 2번 다운 상황과 동일함.
5, 6, 7번 다운
- 2,3,4번과 패턴(스케줄링, API 요청, 이벤트 삭제)과 동일
이체 입금 요청 API의 멱등성
위 부작용은 이체 입금 요청 이벤트가 멱등성을 보장해주면 해결됩니다. API의 규약을 정하면 되는 것이죠. 즉 같은 이체 입금 요청 이벤트에는 같은 이체 입금 결과가 나와야 합니다. 이는 이체 입금 요청 API의 response code에 대한 이야기가 아닙니다. 서버가 다운될 수 있기 때문에 이체 입금이 제대로 됐더라도 500 response를 돌려줄 수 있기 때문이죠. 저는 두 은행이 이체 입금 요청과 이체 입금 완료 응답 API의 멱등성을 보장한다는 공통 API 규약을 지키도록 설계했고, 그 결과 부작용을 해결했습니다.
✍️ 구현
아웃박스 패턴 구현은 굉장히 간단하게 했습니다. 현 프로젝트는 이체 기능만 구현할 것이기 때문에 아웃박스의 확장성은 고려 사항이 아니였기 때문입니다.
✍️ 추후 고민해볼 만한 것
1. DB polling 부하 고려
2. 이체 출금-입금 시간 간격이 너무 클 수도 있음. 스케줄러를 두개 쓰기 때문.
'TIL > 개발 칼럼' 카테고리의 다른 글
#4 타행 이체 기능 성능 개선기, 속도(인프라 개선) (0) | 2023.04.08 |
---|---|
#3 타행 이체 기능 성능 개선기, 속도(아웃박스 스케줄러 전략 선정 및 구현) (0) | 2023.04.08 |
#1 타행 이체 기능 성능 개선기, 프로젝트 소개 (0) | 2023.04.07 |
야크 털은 어디까지 깎아야 할까? JWT, 비대칭키, RSA, 유클리드 호제법, 정수론 (0) | 2022.11.10 |
사용자의 UX를 고려한 로그인(JWT, 세션, 쿠키) 보안 전략 수립 및 구현(2) (0) | 2022.07.02 |