은행 최초 코어뱅킹 MSA 전환기 (feat. 지금 이자 받기)
토스뱅크는 기존의 공급자 중심의 뱅킹 서비스를 고객 중심으로 변화시키기 위해 많은 노력을 기울이고 있어요.
그러나 기존의 전통적인 뱅킹 시스템을 구현하는 방식으로는 안정적인 고객 중심 뱅킹 서비스 제공에 여러 기술적 한계가 있었죠.
이번 아티클에서는 토스뱅크가 어떤 방식으로 기술적 한계를 극복했고, 어떤 기술로 고객 중심의 뱅킹 서비스를 제공해 드리고 있는지에 대해 소개해 드릴게요.
현재 은행 시스템에 대한 소개
채널계와 코어뱅킹(계정계)
먼저, 일반적인 은행 시스템의 아키텍처에 대해 알아볼게요.
은행에는 크게 고객의 요청을 코어뱅킹 서버로 전달하는 채널계와 금원과 관련된 메인 비즈니스 로직을 처리하는 코어뱅킹(계정계)라고 하는 두 개의 서버를 중심으로 하는 아키텍처로 구성되어 있어요.
여기에 코어뱅킹 서버는 대부분의 은행에서 거대한 모놀리식 아키텍처로 구성되어 있죠.
코어뱅킹 시스템 아키텍처 히스토리
코어뱅킹 시스템이 모놀리식 아키텍처를 유지해온 이유는 은행 시스템의 변천사를 알면 그 힌트를 얻을 수 있는데요.
1970년대부터 은행의 계좌 데이터를 적절하게 가공하고 처리해야 하는 니즈가 생기면서, 1세대와 2세대 코어뱅킹 아키텍처가 생겨났고, 2000년대에 디지털 붐이 일면서 모바일 뱅킹, 웹 뱅킹, 텔레뱅킹 등 다양한 거래 요청을 한 곳에서 적절하게 처리해줄 수 있도록 현재의 모놀리식 코어뱅킹 아키텍처가 생겨나게 되었어요.
지난 20여 년간 코어뱅킹 아키텍처는 운영체제와 개발언어의 크고 작은 변화는 있었지만, 현재의 모바일 트렌드와는 맞지 않는 20년 전의 모놀리식 아키텍처를 대부분의 은행에서 사용하며, 현재의 거대한 모놀리식 형태로 몸집을 불려가고 있었죠.
현재 토스뱅크의 채널계는 기존 토스의 DNA를 이어받아 모두 MSA 환경으로 구성되어 있어요. 반면에, 기존의 코어뱅킹 시스템은 Redis, Kafka 등의 모던한 기술을 사용하고는 있었지만, 여타 은행과 다름없이 채널계와의 통신을 위한 MCI, 대외연계를 위한 FEP, 대내 단위 시스템과의 연계를 위한 EAI가 코어뱅킹 서버에 강결합되어 있는 구조로 여타 은행과 다른 없는 거대한 모놀리식 시스템으로 구성되어 있었죠.
그렇다면, 모놀리식 코어뱅킹 아키텍처가 어떤 한계가 있었기에 MSA로 전환했어야 했을까요? 모놀리식 코어뱅킹 시스템의 장점과 단점을 곱씹어보며, 그 이유를 살펴볼게요.
물론 모놀리식 코어뱅킹 시스템도 장점이 있습니다.
그렇지만, 모놀리식으로 구성된 시스템은 트래픽이 갑자기 몰렸을 때, 특정 코어뱅킹 서비스만 스케일 아웃을 하는 전략을 가져갈 수 없어요.
또한, 1개의 서버이기 때문에 장애가 발생한 서비스 외에 다른 서비스들의 영향도를 제한할 수 없어, 안정성이 부족하다는 단점도 있죠. 즉, 한 개의 컴포넌트에서 장애가 발생하면, 전 업무가 마비되는 구조로 이어질 수 있다는 건데요.
예컨대, 토스뱅크가 카드 결제 시 결제 금액의 30%를 환급해주는 파격적인 이벤트를 모놀리식 시스템 구조에서 진행한다고 해볼게요.
카드 서비스는 평소보다 훨씬 많은 트래픽이 들어올 것이고, 이 트래픽이 수용할 수 있는 임계점을 넘어서면, 이벤트를 진행하는 카드 서비스 뿐만 아니라 전혀 상관 없는 계좌 개설이나, 대출 약정 서비스들까지도 마비 될 거에요.
미리 이벤트를 알고 있다고 하더라도, 카드 서비스만 스케일 아웃을 할 수 없기 때문에 전체 시스템의 가용성을 확보해두어야 하는 비효율도 발생할 것이고요.
모놀리식 아키텍처의 서비스 영향도 제한이 어려운 이유에 대해 조금은 이해가 되셨나요?
토스뱅크는 고객분들에게 가치를 제공해드리기 위해 하루에도 수차례씩 혁신적인 실험과 기능 추가를 위한 배포를 하고 있어요. 그러면서 Market Fit에 맞는 제품과 서비스들을 빠른 속도로 찾아가고 있고, 그만큼 토스뱅크를 애용해주시는 고객분들도 많이 늘어나고 있죠.
하지만 토스뱅크의 서비스가 고객분들의 사랑을 받아 나날이 성장하는 만큼 기존의 모놀리식 아키텍처를 유지하면서 토스뱅크의 혁신적인 서비스들을 안정적으로 제공해드리기는 점점 어려워졌어요.
그래서 저희는 현재의 차세대 코어뱅킹 아키텍처를 대량 트래픽에 특화되어 있고, 각 업무별 서비스 영향도를 분리할 수 있는 MSA로 전환하기로 결정했습니다.
그중에서도 저희는 토스뱅크 서비스 중에서 가장 트래픽이 많으면서, 토스뱅크의 대표 서비스 중 하나인 지금 이자 받기 서비스를 모놀리식 코어뱅킹 시스템에서 분리하여 MSA로 전환하기로 했답니다.
개발 방법
기술 스택 선정
먼저 기술 스택은 현재 토스뱅크 채널 서버에서 사용하고 있는 기술들을 대부분 채택했어요. Kubernetes위에 Spring boot, Kotlin, Jpa 등을 기반으로 개발했고, 비동기 메시지 처리와 캐싱은 Kafka, Redis를 사용하기로 결정했어요.
그런데 개발하자마자 첫 번째 고민에 봉착했는데요. 현재 모놀리식으로 강결합되어있는 업무별 비즈니스 의존성을 어느 정도까지 느슨하게 가져갈 것이냐였어요.
지금 이자 받기를 위해 필요한 도메인은 고객 정보 조회를 위한 고객, 금리조회를 위한 상품 그리고 이자의 회계 처리를 위한 회계 정보가 필요했어요. 이 모든 것을 하나의 마이크로 서버에서 처리하는 것은 MSA의 장점을 활용하지 못할 것이라 판단하여, 도메인 단위로 서비스를 나누기로 결정했어요.
고객의 지금 이자 받기 요청은 고객 정보 조회를 거쳐, 금리 조회와 이자계산, 이자 송금, 회계처리를 한개의 트랜잭션으로 처리하고 있었는데요.
새로운 코어뱅킹 아키텍처에서는 트랜잭션으로 엮이지 않아도 되는 도메인은 별도의 마이크로 서버로 구성했고, 각 서버의 API 호출을 통해 비즈니스 의존성을 느슨하게 가져가도록 구성했어요.
그러면 이제 실제 이자지급 서버를 어떻게 개발했는지 알아볼게요.
동시성 제어
먼저 은행 시스템의 안정성과 직결되는 부분인 동시성 제어입니다.
일단 적절하게 동시성 제어가 안되었을 때, 어떤 문제가 있을지 살펴볼까요?
0.01초 사이에 Transaction1을 통해 이자를 받았고, Transaction2를 통해 입금을 받았다고 가정해보면, Transaction1에서는 현재 잔액 기준인 100원에 지금 이자 받기를 한 100원을 더해 200원으로 갱신을 할 거예요.
그리고 Transaction2에서는 Transaction1의 요청이 있었는지를 알 수 있는 방법이 없으므로, 처음에 조회한 100원의 잔액에 타행으로부터 입금받은 300원의 잔액을 더해 400원이라는 엉뚱한 금액으로 잔액을 갱신할 거예요.
이렇게 되면, 어떤 고객도 토스뱅크의 시스템을 신뢰하지 않겠죠.
이렇듯 은행에서 고객 잔액의 갱신은 앱을 통한 거래는 물론이고, 타행을 통한 입금, ATM을 통한 이체, 자동이체 등으로 잔액를 갱신하는 트랜잭션의 채널이 매우 많아요.
그렇기 때문에 일반적으로 사용되는 Redis Global Lock 만으로는 은행 시스템 환경에서 동시성 제어 이슈는 해결하기가 어렵죠.
그래서 동시성 이슈를 해결하는 것이 코어뱅킹 개발에 있어서 필수 조건이라고 할 수 있습니다.
저희는 이 문제를 Redis Global Lock과 더불어 DB Layer에서 동시성을 제어하기 위한 JPA의 @Lock 어노테이션을 통해 해결했어요.
앞에 예시로 다시 돌아가 볼게요.
Transaction2는 DB Layer에서 Lock으로 동시성을 제어하고 있기 때문에 Transaciton1이 끝날 때까지 대기합니다.
그리고, Transaction1의 commit이 끝난 이후의 변경된 잔액을 참조하겠죠. 그러면 잔액은 최초에 예상했던 500원으로 commit이 되고 트랜잭션의 동시성은 안전하게 보장됩니다.
그런데 이 때, DB Lock을 사용할 때는 주의해야 하는 점이 있어요.
Lock을 잡아야 하는 데이터를 명확히 식별하고, 갱신하는 데이터에 대해서만 Lock을 획득해야 데드락과 시스템 성능 저하를 예방할 수 있다는 점인데요.
지금 이자 받기API의 경우 잔액을 갱신하는 이벤트가 메인 비즈니스 로직이기 때문에, 계좌 단위 현재 잔액 데이터에 대해서만 고유하게 Row Locking이 걸리도록 개발하여, 동시성을 보장하도록 구현했어요.
또한, Transaction2의 동시성이 발생하였을 때, Transaction1을 끝날 때까지 기다릴 수 있도록 재시도할 수 있는 로직과 적절한 타임아웃을 적용해주어서 고객 관점에서 Lock이 걸렸는지도 모르게 안정적으로 이자를 받을 수 있게 구현했죠.
성능 개선을 위한 비동기 처리
두번째는 카프카를 활용한 비동기 트랜잭션 구현입니다. 기존 코어뱅킹 시스템에서는 1번의 이자를 지급받기 위해 20개의 테이블에 80번의 UPDATE, INSERT가 이루어지는 복잡한 구조였어요.
그렇기 때문에 지금 이자 받기 서비스의 속도도 평균 300ms로 전체 코어뱅킹 서비스 중에서 느린 편에 속했죠. 이 정도면 정규화가 잘 되어 있는 데이터 모델과 정교하게 잘 설계된 인덱스 구조로도 빠른 응답 속도를 기대하기는 어려운 구조였어요. 그래서 기존 지금 이자 받기 트랜잭션에서 분리가 가능한 테이블은 카프카를 이용해 트랜잭션에서 분리했어요.
트랜잭션 분리에 대한 기준은 고객의 잔액과 통장 데이터 관점에서 DB 쓰기 지연이 발생하였을 때, 실시간으로 문제가 발생하느냐? 로 접근하였고, 반드시 트랜잭션이 보장되어야 하는 데이터 모델과 즉시성을 요하지 않는 즉, 세금 처리와 같이 지금 이자 받기 트랜잭션과 묶이지 않아도 되는 데이터 모델의 DML은 트랜잭션을 분리했죠.
구체적으로 살펴보면, 지금 이자 받기 서버에서 지금 이자 받기의 트랜잭션 종료와 동시에 세금 카프카 토픽에 메시지를 Produce하고, 비동기 처리 서버가 Consume해서 세금 DB에 저장하도록 구현했어요. 정상적인 상황이라면, 이자 DB와 세금 DB에도 준실시간으로 업데이트가 되었을 것이기 때문에 지금 이자 받기의 거래는 정상적으로 종료될 거에요.
그렇지만, 카프카 메시지가 정상적으로 처리되지 않는 경우도 있기 때문에, dead letter queue를 이용해서 세금DB에 대한 트랜잭션을 안정적으로 보장할 수 있도록 했어요. 또, 재처리시 중복으로 세금이 업데이트 안되도록 API도 멱등하게 설계했죠.
그 결과 세금 DML을 지금 이자 받기 트랜잭션에서 분리함으로써, 기존 80회의 DML이 이루어지던 지금 이자 받기 트랜잭션을 50회의 DML로 줄이는 개선 효과를 얻을 수 있었습니다.
Redis를 활용한 캐싱 전략
마지막으로는 Redis를 활용한 캐싱 전략입니다.
기존 코어뱅킹 시스템에서의 이자 계산은 RDB 기반의 일자별 거래내역DB를 조회해서 연산하는 방식으로 구현되어 있었어요.
고객이 지금 이자 받기를 할 때마다, 계좌의 매일 매일 거래내역을 참조해서 이자 계산과 세금을 계산하는 구조이므로 성능적으로 오래 걸릴 수 밖에 없는 구조였죠. 그러나 고객은 하루에 1번 밖에 이자를 못받기 때문에 Redis를 활용하면, 하루에 1번만 DBIO를 발생시킬 수 있을 것이라 판단해서 Redis를 이용해 캐시를 활용하기로 했어요.
기존의 이자금액은 고객이 계좌 상세탭에 접근할 때마다, 이자계산을 위한 DB I/O가 발생하고 있었는데요. 이를 고객이 하루 중 처음으로 계좌 상세탭에 접근할 때에만 DB에 접근하도록 구현했고, 이자예상조회의 결과를 Redis에 캐싱해 두도록 구현했어요.
그래서 고객이 하루에 2번 이상 계좌 상세탭에 접근할 경우에는 Redis에 미리 저장되어 있던 이자계산 결과를 리턴하도록 했죠. 그래서 불필요하게 DB 리소스가 낭비되는 것을 예방했습니다.
또한, Redis에 캐싱 된 이자 데이터의 만료일자도 하루로 두어서, 이자금액이 잘못 계산 되는 케이스도 원천적으로 방지했어요. 그래서 매일 자정 이후 고객이 계좌 상세탭에 처음 접근할 때만, 이자예상조회의 결과를 캐싱해서 이자 데이터의 정합성도 안정적으로 보장할 수 있었죠.
기존 시스템을 안정적으로 전환하는 방법
이자 지급 마이크로 서버에 이자 조회 거래, 지금 이자 받기 거래를 개발 완료했어요. 이제 기존 코어뱅킹(계정계)를 참조하던 서비스를 이자 지급 마이크로 서버를 바라보도록 전환해야 하죠.
시스템을 전환하기에 앞서, 이자 지급 마이크로 서버 API에 대한 검증이 필요했는데요. 어떤 검증 방식을 활용할 수 있을까요?
첫 번째 방법: 실시간 검증을 통한 건별 검증 방식
첫 번째 방법인 온라인 검증 방식을 도식화한 그림입니다.
먼저, 앱에서 고객이 이자 조회 거래를 일으키면 채널계에서 MCI를 통한 기존 코어뱅킹 서버에 이자 조회 서비스를 호출하고, 동시에 이자 지급 마이크로 서버의 API를 호출했어요.
코어뱅킹 서버에서 리턴된 이자 값과 이자 지급 마이크로 서버에서 리턴된 이자 값을 각각 리턴 받아, 두 이자 값이 불일치할 경우 토스뱅크 내부 모니터링 채널에 해당 내용을 알림으로 받도록 했어요. 채널에 알림이 오면 대상 및 로그를 확인하고 원인을 확인하여 이자 계산 로직을 수정해주는 과정을 거쳤어요.
두 번째 방법: 배치를 활용한 대량 검증 방식
다음은 배치를 활용한 대량 검증 방식입니다.
- Staging 환경이란? 실제 운영환경과 동일하게 구성된 내부 API 테스트용 환경.
Staging 환경에서 채널계 배치를 활용해 매일 대량의 검증 대상 목록을 추출했고, 온라인 검증 방식과 동일하게 코어뱅킹 서버와 이자 지급 마이크로 서버를 각각 호출해주었어요. 대상 목록에 대한 검증이 모두 끝나면, 이자 리턴 값이 불일치했던 건들에 대한 내용을 담아 내부 모니터링 채널에 알림으로 받았고, 로직 수정을 반복해주었습니다.
이렇게 저희는 두 가지 방식을 활용해서 이자 조회 거래에 대한 검증을 완료했습니다.
그런데 실제 이자를 지급받는 지금 이자 받기 거래의 경우 코어뱅킹 DB 원장에 잔액을 갱신하고 거래내역을 쌓고, 회계 처리를 해주는 등의 작업이 필요했기 때문에, 거래가 발생했을 때 실제 데이터가 정확하게 쌓이고 갱신되었는지 추가로 검증해야 했어요.
그래서 지금 이자 받기 거래의 데이터 정합성 검증을 위해, 상세한 도메인 기반의 테스트 시나리오를 작성했어요.
테스트 시나리오 작성을 통한 E2E 통합 테스트 수행하기
토스뱅크 통장은 잔액을 구간별로 나누어 이자를 차등 지급하고 있는데요. 잔액 구간별로 나누어 차등 계산되어 이자가 지급되었는지 검증이 필요했어요.
그리고 명의도용, 해킹 피해, 사망 등 토스뱅크 고객의 상태에 따른 검증이 필요했고, 계좌의 상태 및 출금/입금 정지 상태에 따른 검증이 필요했죠.
해당 검증 케이스들을 고려해서 테스트 시나리오를 수립했고, 케이스 별로 테스트를 진행하여 이자 계산 및 실제 DB에 데이터가 정확하게 갱신되었는지를 확인하며 로직을 수정해주는 과정을 거쳤습니다. 이 과정을 통해, 이자 받기 거래에 대한 정합성 검증을 완료할 수 있었어요.
순차 배포를 통한 안정적인 마이그레이션하기
이제 API에 대한 검증은 완료되었으니, 코어뱅킹을 바라보던 서비스를 fade out 시키고 이자 지급 마이크로 서버 API만을 바라보도록 전환해줘야 했어요.
API를 전환할 때 대상 모수를 점차 늘려가며, 순차적으로 오픈했는데요. 먼저 토스뱅크 수신개발팀에 오픈하여 직접 이자 받기 거래를 일으키며 데이터 결과값을 검증했어요. 특이사항이 없는 것을 확인하여 토스뱅크 내부 팀원에게 오픈하였고, 모니터링을 진행했어요.
다음으로는 일부 고객을 대상으로 오픈하고 점차 모수를 늘려가며 순차 오픈하여 전체 고객을 대상으로 전환을 완료하는 방식을 선택했습니다.
순차 배포 과정을 살펴보면, 코어뱅킹 서버를 바라보던 API 호출량과 이자 지급 마이크로 서버를 바라보던 API 호출량을 조절하여 이자 지급 마이크로 서버의 트래픽을 점차 늘려가는 형태로 진행했습니다.
그렇게 저희는 순차 배포 방식을 채택 함으로써 기존에 운영하고 있던 시스템을 중단하지 않고도 안정적으로 시스템을 전환 할 수 있었어요.
마지막으로 코어뱅킹 MSA 전환의 성과에 대해 공유 드리며, 이번 아티클을 마무리 해볼게요.