토스 SLASH "왜 은행은 무한스크롤이 안되나요" 정리

hongo·2023년 12월 1일
0

토스 SLASH "왜 은행은 무한스크롤이 안되나요"를 보고 적는 글

모바일 은행에서 계좌 내역을 확인하려면, 무한스크롤 대신 1개월이내 내역 확인, 3개월이내 내역 확인 과 같이 기간별로 조회가 가능한 것을 볼 수 있다. 유저 입장에서 스크롤을 내리면 바로 볼 수 있는 무한스크롤이 편함에도, 기간을 지정해서 확인할 수 있게 한 이유는 무한스크롤을 적용하는 데 있어 문제가 있기 때문일 것이다.

은행이 무한 스크롤을 수행할 때 발생하는 문제가 무엇인지, 이 문제를 어떻게 개선할 수 있는지 알아보자.

은행시스템의 구조

은행은 왜 무한스크롤을 제공하지 않는지 생각해보기 이전에 은행시스템에 대해 알 필요가 있다.

은행시스템은 크게 채널계와 계정계로 나뉜다.

계정계

계정계는 돈을 다루는 영역이다! 돈과 관련된 원본 데이터를 저장하고 로직을 수행한다.

돈을 다루는 환경에서는 신뢰성이 매우 중요하다. 로직을 처리할 때 오류가 나지 않는 것이 최우선사항이다! 따라서 데이터의 안전성과 신뢰성을 최대한 보장하기 위한 구조로 설계해야한다.

신뢰성을 고려할 때, 단일 서버 및 단일 DB를 사용하는 것이 유리하다. 네트워크 복잡도도 줄고, 데이터의 일관성과 트랜잭션 처리를 단일 서버에서 수행함으로써 로직을 처리하는 데 오류가 날 확률이 낮아진다. 때문에 계정계에서는 단일 서버 & DB를 선호하는듯하다. (물론 단일 서버이므로 대규모의 트래픽을 처리하는데는 불리하다.)

채널계

채널계는 유저의 요청을 직접 받아서 처리하는 영역이다! 돈을 직접 다루지는 않는다. 유저에게 받은 요청에서 돈을 다루는 로직이 필요하다면 계정계에 넘겨버리는 것 같다.

채널계는 돈을 다루지 않기 때문에, 계정계와는 달리 다중 서버 및 DB를 적용하는데 더 열려있는 것 같다. 때문에 서버 스케일 아웃이 가능하므로, 트래픽이 몰리는 서버를 확장하는등 대규모 트래픽을 처리하기 유리한 구조이다.

근데 이거 왜 알아야함?

계정계에서 대규모 트래픽을 처리하는데 불리하다는 것이 핵심이다. 계좌 내역을 조회하려면 돈에 대한 원본 데이터를 가지고 있는 계정계에 접근해야할 것이다. 때문에 큰 규모의 조회를 부하없이 처리하기엔 무리가 있어 무한스크롤을 적용하기 어렵다고 한다.

하지만 토스는 무한스크롤이다! 어떻게?!

채널계에서 계좌 내역을 DB에 저장하게 하면 된다.

계정계까지 가지 않고, 대규모 트래픽을 처리하기에 유리한 채널계에서 조회를 해온다면 해결된다. 그렇지만, 이렇게 되면 계정계와 채널계의 DB 동기화 이슈가 발생한다. 이를 어떻게 해결하고 있을까?

카프카를 사용해 계정계의 변화를 채널계에 전파한다.

가장 쉽게 생각할 수 있는 방법은 채널계에 DB를 두고, 송금 요청이 올 때마다 해당 DB에 송금 정보를 저장하면 된다. 하지만 이 경우, 1. 유저가 토스의 송금 서버가 아니라 다른 은행 어플을 사용해서 돈을 송금할 경우, 2. 계정계가 송금을 잘 처리했지만 채널계에게 송금이 완료되었다는 응답을 보낼 때 에러가 발생한 경우 동기화가 되지 않는다.

그래서 토스는 카프카를 사용해 계정계의 변화를 채널계에 전파했다.

계정계에서 송금 내역이 발생하면, 카프카 토픽에 메세지를 프로듀스하고, 송금서버가 컨슘해서 동기화하게 한다.

즉, 채널계에서 송금 요청이 날아오면, 계정계가 받아 이를 외부 카드사등과 소통하여 송금을 수행한다.

송금이 완료되면, 1. 계정계는 송금이 완료되었다는 응답을 채널계에게 반환하고, 2. 계정계는 채널계와의 DB 동기화를 위해 카프카에 메시지를 프로듀스한다.

채널계는 1. 송금이 완료되었다는 응답을 받으면 유저에게 송금 완료 응답을 반환하고, 2. 메시지를 컨슘해서 DB를 동기화한다.

물론 이 구조에서 발생하는 문제들이 있을 것이다!!! 어떤 문제가 발생하고, 어떻게 해결하는지 알아보자!

문제1 : 코어뱅킹서버(계정계)가 채널계에 송금 완료 응답을 보내는 중 타임 아웃이 발생

편의상 채널계의 송금 서버와 계정계의 코어뱅킹서버를 가지고 설명해주셨다.

코어뱅킹서버에서 송금 처리는 잘 되었는데, 송금 서버에게 송금 완료 응답을 보내는 중 타임아웃이 발생한 경우이다. 즉, 송금은 잘 처리되었지만 유저는 송금 실패 응답을 받게된다. 실패 응답을 받은 유저는 송금이 잘 되지 않았다고 판단하고 중복으로 송금을 요청할 수 있다.

이를 해결하기 위해 송금 요청 데이터를 DB에 저장하고, 송금 요청 데이터에 Status를 추가했다고 한다.

Status는 미완료, 완료 정도로 나눠볼 수 있을 것 같다. 유저가 송금 요청을 할 때 미완료상태인 송금 요청 데이터가 있다면 중복적인 송금 요청을 막는다.

문제 1-1: 코어뱅킹서버에 송금 요청을 보낼 때 에러가 발생한거라면? 영원히 송금 불가능

내부적으로 송금 완료 응답에 실패했을 경우, 다시 retry를 하게 설계했다면 위 방법으로 해결이 됐을 것이다.

그러나 만약 송금 서버가 코어뱅킹서버에 송금 요청을 보내는 단계에서부터 에러가 발생한 것이라면? 내부적으로 retry를 해도 계속 에러가 발생한다면 유저의 송금 요청 데이터는 계속 미완료 상태이기에 영원히 송금이 불가능한 경우도 생길 수 있다!

이를 위해 송금 서버가 코어뱅킹서버에게 송금이 존재하는지 묻는 요청을 보내게했다. 송금 요청을 보내고, 송금이 존재하는지 조회 요청도 보내는 것이다. 만약 송금 요청이 잘 성공했다면 송금서버가 송금 상태 조회 요청을 보낼 때, 코어뱅킹서버가 해당 송금이 존재한다고 응답할 것이다. 그러나 송금 요청이 실패했다면, 코어뱅킹서버는 해당 송금 요청이 존재하지 않다고 응답할 것이다.

송금 조회 요청시, 코어뱅킹서버에게 송금이 존재하지 않는다는 응답을 받는다면, 송금서버의 송금 요청 데이터의 Status를 미완료에서 실패등으로 바꿀 수 있을 것이다. 때문에 영원히 송금이 불가능한 경우를 막을 수 있다.

문제 1-1-1: 코어뱅킹 서버가 송금 조회 요청때는 존재하지 않는다고 응답했지만, 그 이후 송금 요청을 수행한 경우

앞서, 송금 서버가 코어뱅킹서버에게 1.송금 요청을 보내면 추가적으로 2. 송금이 존재하는지 조회 요청도 보낸다고 했다.

만약 송금 요청이 코어뱅킹서버 부하로 인해 지연되고 있다면, 송금이 존재하는지 조회 요청을 보냈을 때는 존재하지 않는다는 응답을 받았으나, 지연이 해결되어 송금 요청이 성공적으로 수행된다면, 실제로는 송금이 성공되었지만 송금 서버에서는 송금을 실패한 것으로 처리할 것이다.

이를 막기위해 송금 요청의 타임 아웃 시간을 지정하고, 송금 요청 시 데이터에 요청받은 시간을 함께 보낸다.

그렇다면 송금이 존재하는지 조회 요청은 지정한 타임 아웃 시간이 지났을 때 요청하고, 만약 이후에 송금 요청 의 지연이 해결되어 코어뱅킹서버로 간다해도, 코어뱅킹서버에서 송금 요청이 요청받은 시간을 보고 타임 아웃 시간이 지났다면 무조건 실패 처리를 하면 된다.

문제 2 송금 서버 DB에 저장하는 것이 실패한다면?

송금 서버는 카프카의 메시지를 컨슘해서 송금 서버 DB에 정보를 저장해 동기화한다. 하지만, DB에 저장하는 도중 실패한다면 카프카에서 송금 완료 재확인 메시지를 보내 한 번 더 retry하게 한다고 한다.

하지만 두 번의 시도에도 DB 저장에 실패했다면, 더이상 재시도하지 않고 컨슈머 데드 레터라는 카프카 토픽에 실패한 메시지를 저장하고, 개발자가 직접 처리를 할 수 있게 했다고 한다.

문제 3 송금 과거 거래 내력 누락

송금 서버가 데이터 동기화에 실패한 경우이다. 기본적으로 카프카 메시지를 컨슘해서 실패하면 재시도를 한다. 그런데 유저가 두 번의 송금 요청을 보냈을 때, 첫 번째 요청이 실패되어 두 번째 요청보다 늦게 동기화가 되었다면 어떻게 해야할까?

예를 들어 유저가 계좌에 0원이 있는 상태에서 1. 500원을 입금하고, 2. 100원을 출금했다고 해보자. 이 때 500원 입금 동기화만 실패해서 다시 요청을 수행하는 사이에 유저가 계좌 내역을 조회하게 되면 100원만 출금된 결과를 보게 된다.

이를 막기 위해 송금 내역을 동기화할 때, 이전에 동기화 되지 않은 요청이 있다면 함께 동기화 요청을 보낸다.

모든 송금 내역에는 순차적으로 증가하는 일련 번호가 있다. 송금 서버에서 가장 마지막에 저장된 송금 내역의 일련번호와 카프카에서 받은 송금 내역의 일련번호의 차이가 1보다 크다면, 이전에 동기화 되지 않은 요청이 있다는 의미이다. 이 경우 전부 동기화 요청을 보낸다. (만약 이 때도 이전 송금 내역을 동기화하는데 실패한다면, 전부 동기화하지않는다.)

문제 3-1 동기화 되기 전에 유저가 거래내역을 조회한다면?

송금 서버는 진행중인 거래내역을 저장하고 있다. 유저가 거래내역을 조회할 때 진행중인 거래내역이 있다면 그 즉시 동기화 요청을 보낸다. 동기화 요청이 수행되면 유저에게 거래내역을 응답한다.

  • 근데 여기서 또 동기화 오류나면 어떡함??? 그냥 거래내역 조회 실패 응답 보내는 거임?

문제4 동기화 지연

너무 많은 동기화 요청이 들어와서 지연된다면?

카프카는 메세지들을 여러 파티션으로 나눠서 저장할 수 있다. 이 때 계좌번호를 키로 두어 특정 파티션과 1대1매핑을 시킨다. (여러 개의 컨슈머가 같은 계좌의 송금을 처리해 동시성 문제가 생기는 상황을 막기 위함)

  • 파티션만 주구장창 늘리는 게 답인가요?

그건 아니다! 만약 특정 시간에만 많은 동기화 요청이 들어오는 것이라면, 나머지 시간에는 많은 양의 파티션이 필요하지 않을 수도 있다. 늘어난 파티션은 시스템 자원을 차지하고, 한 번 늘린 파티션은 다시 없앨 수가 없기 때문에 평균적인 요청을 고려하여 파티션 개수를 지정하는 게 중요하다. 피크시간에만 많은 요청을 감당하려면 컨슈머의 워커 스레드 개수를 늘리는 게 좋다. (워커 스레드 또한 특정 계좌번호와 1대1로 매핑되게 할 수 있다.)

문제5 다 제치고 동기화안됨!!!

위와 같은 처리를 했음에도 동기화가 안되었다면 유저가 직접 동기화를 요청할 수 있게 버튼을 생성해 놓았다고 한다.

  • 근데 여기서도 또 동기화 오류나면?! 카프카 데드레터에 메세지 보내고 개발자가 빨리 처리하길 기다리는 수밖에 없는건가?!
profile
https://github.com/hgo641

2개의 댓글

comment-user-thumbnail
2023년 12월 2일

어제 이거 보고 내용 정리한 블로그 없나 검색하니까 따끈따끈한 홍고의 글이..

1개의 답글