일반 은행과 토스뱅크의 거래 내역 조회 방식 비교 및 시스템 아키텍처
일반 은행 앱의 거래 내역 조회 방식의 불편함
일반 은행 앱은 거래 내역 조회 시 상단에 기간 설정을 요구한다. 지정된 기간만큼만 스크롤하여 조회할 수 있으며, 그 이상을 조회하려면 유저가 직접 조회 기간을 변경해야 한다. 반면, 트위터, 인스타그램, 카카오톡 등 많은 인터넷 서비스들은 기간 설정을 요구하지 않고, 스크롤만 계속하면 십수 년 전의 글까지 나열되는 우아한 스크롤 UI를 제공한다. 일반 은행이 이러한 불편한 인터페이스를 제공하는 이유는 시스템 아키텍처 때문이다.
은행 시스템의 일반적인 분리 구조 (채널계와 계정계)

계정계 (Core Banking System)
실제로 유저의 돈을 다루며 원본 데이터가 저장되는 영역이다. 장애나 오류 발생 시 치명적이므로 아주 높은 신뢰도가 요구된다.
채널계 (Channel System)
유저 요청을 직접 받아 처리하는 영역이다. 돈을 직접 다루지는 않으며, 그런 요청은 계정계로 전달하여 처리한다.
토스뱅크의 채널계와 계정계 아키텍처 특징

토스뱅크 채널계 아키텍처
쿠버네티스 클러스터 위에 구축된 도메인별로 분리된 복수 개의 서버 애플리케이션으로 이루어져 있다. 데이터베이스 서버 역시 여러 개로 구성되어 있다.
- 장점: 특정 서버에 부하가 몰리면 스케일 아웃(확장)이 가능하고, DB 부하가 커지면 DB를 분리할 수 있어 큰 트래픽을 다루는 데 유리하다.
- 단점: 네트워크 구조가 복잡하고 DB가 여러 개로 나뉘어 있어 경우에 따라 트랜잭션 처리가 어렵다.
토스뱅크 계정계 아키텍처
하나의 서버 애플리케이션과 하나의 데이터베이스 서버로 구성되어 있다.
- 장점: 서버가 하나이므로 네트워크 구조가 단순하고, DB가 하나이므로 트랜잭션 처리가 유리하다.
- 단점: 특정 서버만 스케일 아웃하거나 DB를 분리할 수 없어 급증하는 트래픽을 소화하기에 다소 불리하다.
- 선택 이유: 오류 없이 동작하는 것이 매우 중요하기 때문에 성능을 희생하더라도 높은 신뢰도를 위한 아키텍처를 택하게 된다.
두 아키텍처의 장단점을 잘 활용하여 서비스를 설계하는 것이 중요하다.
일반 은행의 기간 설정 이유와 토스뱅크의 무한 스크롤 구현 원리
일반 은행이 기간 설정을 하는 이유
거래 내역은 은행의 핵심 기록이므로 당연히 채널계가 아닌 계정계에서 관리한다. 채널계는 고객의 거래 내역 요청을 받아 계정계에 전달하고, 그 결과를 다시 앱으로 전달한다. 계정계는 성능보다 신뢰도가 우선이므로, 광범위한 거래 내역 조회를 빠른 응답 시간과 적은 부하로 제공하기 어렵다. 따라서 계정계에 대한 요청을 최소화하기 위해 UI에서 디폴트 조회 범위를 수개월 정도로 한정하는 것이 현명한 선택이다.
토스뱅크의 무한 스크롤 구현 방식
토스뱅크 역시 은행이므로 높은 신뢰성을 위해 기간 설정을 하는 것이 현명할 수 있으나, 토스뱅크 앱에는 기간 설정이 없다. 토스뱅크는 트위터, 페이스북처럼 무한 스크롤이 가능하다. 토스뱅크는 거래 내역 조회를 할 때마다 코어 뱅킹 서버(계정계)에 요청을 보내지 않고, 채널계에 있는 송금 서버가 거래 내역을 직접 반환한다.
거래 내역 동기화의 난제
채널계의 송금 서버가 거래 내역을 직접 반환하려면, 항상 코어 뱅킹 서버와 거래 내역을 정확하게 동기화하는 문제를 해결해야 한다. 동기화가 조금만 어긋나면 거래 내역이 누락되어 은행의 신뢰성에 큰 타격을 입을 수 있다. 너무 자주 동기화를 시도하면 코어 뱅킹 서버에 부하가 커져 장애가 발생할 수도 있다.
결국 거래 내역 무한 스크롤을 위해선 거래 내역의 동기화가 필요한데, 토스의 해당 발표에서는 너무 중요한 정보인 거래 내역을 오차없이 동기화하는 방법과 그 과정에서 생길 수 있는 여러 케이스들을 (주로 카프카를 사용하여) 해결한 방법들을 소개하고 있다.
카프카를 활용한 거래 내역 동기화
카프카를 이용한 기본 동기화 구조 확립
기본 컨셉 & 한계

유저가 이체를 실행할 때마다 이체 내역을 송금 서버 DB에 저장하는 방식은, 다른 은행 앱을 이용해 토스뱅크 계좌로 입금하는 경우를 처리할 수 없다. 따라서 코어 뱅킹 서버가 송금 서버에게 입금 사실을 알려주어야 한다.
카프카(Kafka) 도입

토스뱅크는 카프카를 이용하여 이 문제를 해결한다. 코어 뱅킹 서버가 토픽에 메시지를 프로듀싱(Producing)하고, 송금 서버가 이를 컨슈밍(Consuming)하여 송금 DB에 저장한다. 이로써 토스 앱을 이용하든 타행 앱을 이용하든, 코어 뱅킹 서버에 요청하지 않고도 토스뱅크의 모든 거래를 조회할 수 있게 된다.
하지만 이러한 플로우는 아름다운(?) 상황에서만 정상적으로 동작한다. 이 중 어느 부분이라도 오류가 발생하거나 지연이 발생하면 데이터가 제대로 조회되지 않기 때문에, 은행의 신뢰도에 큰 타격을 줄 수 있다. 이를 어떻게 해결할 수 있을까?
기본적인 예외 처리 방안들
송금 실행 중 타임아웃 및 중복 송금 방지 처리
송금 실행 중 타임아웃 발생 시 처리

문제: 송금 실행 중 코어 뱅킹 서버의 응답이 늦어져 타임아웃이 발생하면, 송금 서버는 응답을 받지 못해 거래 내역을 DB에 저장하지 못한다.

해결: 타임아웃 발생 후 코어 뱅킹 서버가 카프카 토픽을 통해 송금 서버에게 이체 실행 결과를 알려주면, 송금 서버는 거래 내역을 DB에 저장하고 유저에게 조회해 줄 수 있다. 이 경우 송금 직후에는 난감할 수 있으나, 최종적으로 송금이 완료되면 거래 내역에 성공 거래로 남게 된다.
중복 송금 방지 처리

문제: 타임아웃으로 에러를 만난 유저가 송금에 실패한 줄 알고 중복해서 송금을 시도할 가능성이 있다.
해결: 송금 요청이 들어오면, 송금 서버는 코어 뱅킹 서버에 요청을 보내기 전에 우선 송금 요청을 DB에 저장한다. 완료되지 않은 송금 요청이 있는 유저가 다시 송금을 요청하면 거절한다. 나중에 송금이 완료되어 송금 서버가 거래 내역 저장을 끝내고 나면, 그때부터 새로운 송금 요청을 수락한다.
송금 요청 영구 지연 문제 해결
문제: 네트워크 문제 등으로 송금 서버가 보낸 요청이 코어 뱅킹 서버에 도달하지 못하면, 송금 요청이 영원히 진행 중인 상태로 송금 DB에 남아 유저가 영원히 송금을 할 수 없게 된다.
해결: 송금 서버는 주기적으로 코어 뱅킹 서버에게 송금 요청의 상태를 확인한다. 코어 뱅킹 서버는 해당 송금 건에 대해 전혀 모르므로, 송금 요청이 없었다고 응답할 것이다. 송금 서버는 요청 전달에 실패한 것으로 보고 송금 실패로 처리하며, 유저는 계속해서 송금을 할 수 있게 된다.
성공 거래의 실패 처리 가능성 방지 (타임아웃 약속)
희박한 실패 처리 오류 가능성
문제: 송금 서버가 코어 뱅킹 서버에 송금 상태를 물어보고 '없음' 응답을 받아 실패 처리했는데, 뒤늦게 송금 요청이 코어 뱅킹 서버에서 성공적으로 실행되는 경우가 발생할 수 있다.
이는 네트워크 이상으로 송금 요청이 코어 뱅킹 서버에 전달되는 도중, 송금 서버의 상태 확인 요청이 먼저 도착하여 '없음' 응답을 받고 실패 처리했으나, 뒤늦게 송금 요청이 도착하여 실행되는 경우이다.
타임아웃 약속을 통한 해결
해결: 송금 서버와 코어 뱅킹 서버가 타임아웃 시간을 약속하고, 그보다 오래된 요청을 거절하는 방법으로 해결한다.
송금 서버와 코어 뱅킹 서버는 타임아웃 시간을 1분으로 약속하고, 송금 서버는 요청 시각을 포함하여 보낸다. 타임아웃 시간이 지났는데 코어 뱅킹 서버로부터 완료 알림이 오지 않으면, 송금 서버는 상태를 물어본 뒤 '없음' 응답이 오면 실패로 처리한다.
송금 요청이 늦게 코어 뱅킹 서버에 도달하더라도, 코어 뱅킹 서버는 요청 시각을 보고 타임아웃 시간이 지났다면 처리하지 않고 거절한다.
거래 내역 동기화 중 발생하는 데이터 누락 및 지연 문제 해결
거래 내역 저장 실패 시 재시도 및 Dead Letter 처리
문제: 카프카 메시지를 받아 송금 이력을 저장하는 도중 순간적인 통신 장애 등으로 에러가 발생하면 해당 거래가 유저에게 보이지 않게 된다.
해결: 송금 서버는 재시도 후에 송금 완료 메시지를 다시 컨슈머하고 이력을 저장한다. 재시도 후에도 계속 실패하는 경우, 더 이상 재시도하지 않고 컨슈머 데드 레터(Consumer Dead Letter) 카프카 토픽에 실패한 메시지를 저장한다. 개발자가 실패 원인을 확인하고 문제를 해결한 뒤, 해당 토픽을 다시 컨슈머하여 송금 이력을 저장하게 된다.
과거 거래 내역 누락 가능성 방지

문제: 500원 입금 동기화가 실패하고 100원 출금 동기화가 성공한 뒤, 500원 입금 재동기화가 성공하기 전에 유저가 거래 내역을 조회하면, 잔액이 -100원이 되는 이상한 상황을 겪게 된다.

해결: 송금이 완료되었다는 카프카 메시지를 받았을 때, 해당 거래만 동기화하는 것이 아니라 그 이전에 다른 거래가 있는지 코어 뱅킹 서버를 조회하여 동기화한다. 모든 거래 내역에는 거래 순서대로 증가하는 일련번호가 붙어 있어, 송금 서버는 저장된 가장 최근 거래 건과 카프카 메시지로 받은 거래 건의 일련번호를 비교하여 누락 여부를 판단할 수 있다.
따라서 100원 출금 건을 동기화할 때 500원 입금이 누락되었음을 발견하고, 500원 입금을 먼저 동기화한 후 100원 출금 건을 동기화하여 과거 거래 내역 누락을 방지한다.
누락된 거래 동기화 실패 시 처리
만약 누락된 500원 입금 동기화가 또다시 실패한다면, 100원 출금 건도 동기화하지 않는다. 이는 순간적으로 잔액이 -100원이 되는 문제를 피하기 위함이다. 이 카프카 메시지 컨슘은 실패로 처리되므로, 송금 서버는 다시 컨슈머하여 동기화를 시도할 것이다.
동기화 완료 전 거래 내역 조회 문제 해결
문제: 동기화가 완료되기 전에 유저가 거래 내역 조회를 하면, 입금된 500원과 출금된 100원이 모두 보이지 않는 문제가 발생한다. 유저는 방금 성공한 출금 내역이 보이지 않으면 당황할 수 있다.

해결: 송금 서버는 진행 중인 거래 내역을 저장하고 있다. 유저가 거래 내역을 조회하면, 송금 서버는 현재 진행 중인 100원 출금 항목을 발견하고 동기화를 시도한다. 이때 500원 입금도 아직 동기화되지 않은 상태이므로, 100원 출금보다 먼저 동기화하여 모든 거래가 잘 동기화된 정상적인 거래 내역을 유저에게 보여준다.
대규모 트래픽 환경에서의 동기화 지연 방지 전략
대규모 동기화 지연 문제가 발생하는 상황 예시

전 고객에게 100원씩 입금하는 이벤트로 100만 건의 거래 내역을 동기화해야 하는 경우를 가정한다. 1시간에 걸쳐 입금이 완료된다면 1초에 약 300건씩 처리될 것이다.
만약 한 건 처리에 100밀리초(ms)가 걸린다면, 1초에 10건, 즉 10만 개의 동기화가 일어나므로 1초에 300건씩 들어오는 처리량을 다 처리하지 못하고 점점 밀리게 된다. 100만 건이 밀리면 이를 모두 처리하는 데 28시간이 걸리므로, 그동안 새로운 입금 거래가 동기화되기 전까지 유저에게 보이지 않는 문제가 생긴다.
카프카 파티셔닝을 통한 동시성 확보
카프카는 메시지들을 여러 파티션으로 나누어 여러 개의 컨슈머가 처리할 수 있게 해준다. 이때 유저가 지정한 키(Key)를 기준으로 파티션을 나누어 준다.
거래 내역 동기화의 경우 계좌 번호를 키로 사용하여 같은 계좌의 거래 내역은 반드시 같은 파티션에 들어가게 된다. 같은 파티션은 같은 컨슈머가 처리하므로, 여러 컨슈머가 같은 계좌를 동기화하면서 발생할 수 있는 동시성 문제를 방지할 수 있다.
초당 300건의 입금 요청을 처리해야 하는 경우, 파티션이 30개라면 충분히 처리할 수 있을 것으로 추정된다.
워커 스레드 활용을 통한 처리량 극대화
파티션 무한 증설의 한계
유저가 계속 늘어나 더 높은 처리량이 요구되더라도 파티션을 무한적으로 늘릴 수 없다. 파티션을 늘리는 데 시간이 걸려 유저가 동기화 지연 문제를 겪게 될 수 있다. 피크치를 기준으로 넉넉하게 파티션을 잡으면 시스템 자원을 차지하고, 한 번 늘린 파티션은 다시 줄일 수 없어 지속적인 자원 낭비가 될 수 있다.
해결 방안

파티션 개수는 적당한 수준을 유지하고, 대신 컨슈머별로 워커 스레드를 충분히 할당한다. 예를 들어, 파티션을 10개로 하고 워커 스레드를 100개로 한다면, 총 1,000개의 스레드로 동기화가 가능해져 초당 1만 건의 동기화가 가능해진다. (건당 100ms 소요 기준)
워커 스레드 동시성 문제 방지
각 워커 스레드가 같은 계좌에 대한 동기화를 동시에 실행하면 불필요한 트래픽을 유발할 수 있다. 따라서 카프카 파티셔닝과 같은 요령으로, 동기화 작업을 실행할 워커 스레드를 선택할 때 계좌 번호 기준으로 선택하게 하여 같은 계좌는 항상 같은 스레드가 동기화하도록 한다.
최후의 수단: 유저 수동 동기화 버튼 제공
최후의 실드 (수동 동기화)
모든 방어책에도 불구하고 예상치 못한 문제로 동기화가 안 되는 상황을 유저가 만날 수 있다. 이러한 경우를 대비하여 유저가 직접 동기화를 수행할 수 있는 버튼을 만들어 두었다. 유저가 거래 내역이 최신화되지 않은 것 같다고 느낀 경우, 불러오기 버튼을 눌러 즉시 거래 내역을 최신화할 수 있다.
자료
'개발 공부 > 기타' 카테고리의 다른 글
| [git] .gitignore 문법 및 규칙 정리 (0) | 2022.05.23 |
|---|
