devFancy BE Developer

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

2025-09-24
devFancy

토스뱅크에서 Server Developer 이응준님이 발표한 “왜 은행은 무한스크롤이 안되나요” 영상을 듣고 정리한 내용입니다.

개인적으로 문제를 해결해 나가는 과정이 인상 깊었고, 내용 면에서도 유익한 부분들이 많아 3년 전 영상임에도 불구하고 깊이 있게 정리하게 되었습니다.


은행 시스템 구조

은행에는 기본적으로 채널계와 계정계가 존재한다.

  • 채널계: 유저의 요청을 직접 받아 처리하는 영역이다. 돈을 직접 다루지 않으며 그런 요청은 계정계로 전달하여 처리한다.

  • 계정계: 실제로 유저의 돈을 다루며 원본 데이터가 저장되는 영역이다. 장애나 오류가 발생하면 치명적이기 때문에 아주 높은 신뢰도가 요구된다.

토스뱅크에서는 아래와 같은 시스템 구조를 가지고 있다.

  • 채널계: 쿠버네티스 클러스터 위에 구축된 도메인 별로 분리된 복수개의 서버 애플리케이션으로 이루어져 있다. DB 역시 여러개로 구성되어 있다. 네트워크 구조가 복잡하고 DB가 여러개로 나누어 있기 때문에 트랜잭션 처리가 어려운 경우가 있다.

    • 하지만 특정 서버에 부하가 몰리면 그것만 스케일 아웃할 수 있고 DB 부하가 커지면 DB를 분리하는 선택을 할 수 있기 때문에 큰 트래픽을 다루는데 유리하다.

  • 거래내역은 은행에서 핵심적으로 다루는 중요한 데이터이기 떄문에, 계정계에서 관리한다.

  • 계정계는 성능보다 신뢰가 더 우선적이다.

  • 거래내역을 조회할 때 기간설정을 하는 것이 현명한데, 토스뱅크에는 기간 설정이 없다. 다른 SNS 서비스처럼 무한스크롤이 된다.

  • 이렇게 가능한 이유는 거래 내역을 조회할 때마다 코어뱅킹 서버가 아닌 채널계에 있는 송금 서버가 거래내역을 직접 반환한다.

  • 이렇게 동작하기 위해서는 송금 서버가 항상 코어뱅킹 서버와 거래내역을 정확하게 동기화하는 문제를 해결해야 한다.

송금 문제 해결 방안

문제1. 타행 입금 누락문제 해결

  • 코어뱅킹 서버가 카프카 토픽에 메시지를 프로듀싱하고 송금 서버가 컨슘하여 송금 DB에 저장한다.

  • 하지만 이것은 모든 것이 이상적으로 잘 돌아가는 상황에서만 정상 동작한다.

  • 현실은 그렇지 못하기 때문에 예외상황에 대한 대처를 해야한다.

문제2. 송금 이력 누락 문제

  • 송금 API 실행 중 타임아웃이 발생하면, 송금 서버는 송금이 성공했는지 알 수 없어 거래 이력을 즉시 저장하지 못하는 문제가 있다.

  • 이 문제를 해결하기 위해, 송금이 실제로 완료되었을 때 코어뱅킹 서버가 Kafka 토픽으로 이체 완료 메시지를 송금 서버에 전달한다.

  • 메시지를 받은 송금 서버는 이를 바탕으로 거래내역을 자신의 송금 DB에 저장하고 유저에게 제공해줄 수 있다.

  • 유저는 송금 직후에 타임아웃으로 인한 에러를 만나겠지만, 결국 최종적으로 송금이 완료되고 나면 거래내역에 송금건이 조회될 것이다.

문제3. 중복 송금 문제

  • 타임아웃으로 인해 에러를 만난 유저가 송금이 실패한 줄 알고 다시 중복해서 송금을 할 가능성이 있다.

  • 이 문제를 막기 위해 송금 요청이 들어오면 송금 서버는 코어뱅킹 서버에 송금 요청을 보내기 전에 우선 송금 요청을 DB에 저장한다.

  • 그리고 완료되지 않은 송금 요청이 있는 유저가 다시 송금을 요청하면 거절한다.

  • 나중에 송금이 완료되어 송금 서버가 송금 이력 저장을 끝내고 나면 그때부터 새로운 송금 요청을 수락하게 된다.

문제4. 송금이 계속 지연되는 문제

  • 만약 네트워크 문제로 인해 송금 서버가 코어뱅킹 서버에 보낸 송금 요청이 코어뱅킹 서버에 도달하지 못한다면 송금은 실행되지 않을 것이고,

  • 그렇게 되면 송금 요청은 여전히 진행중인 상태로 송금 DB에 남을 것이다.

  • 그러면 유저는 영원히 송금을 할 수 없게 된다.

  • 이 문제를 피하기 위해 송금 서버는 주기적으로 코어뱅킹 서버에게 송금 요청의 상태를 확인한다.

  • 이 케이스에서 송금 요청이 코어뱅킹 서버에 도달하지 못했으므로 코어뱅킹 서버는 해당 송금 건에 전혀 모를 것이다.

  • 따라서 송금 서버가 코어뱅킹 서버에 송금 상태를 조회하면 코어뱅킹 서버는 송금 요청이 없었다고(없음) 응답할 것이다.

  • 그러면 송금 서버는 송금 요청을 코어뱅킹 서버에게 전달하는데 실패하는 것으로 보고 송금 실패로 처리한다.

  • 그럼 이제 지연된 송금이 없으므로 유저는 계속해서 송금을 할 수 있다.

문제5. 성공했는데 실패로 처리되는 문제

  • (문제 상황) 아주 희박할 가능성이지만 이 실패 처리가 잘못되어 성공을 실패로 처리해버릴 가능성이 있다.

    • 송금 서버가 지연된 송금의 상태를 코어뱅킹 서버에 물어보고 코어뱅킹 서버가 그런 송금은 없다고 응답해서 실패한 것으로 처리해버렸는데

    • 뒤늦게 송금 요청이 코어뱅킹 서버에 도달해서 성공적으로 송금 요청이 완료된 경우이다.

  • 예를 들어 네트워크에 무언가 이상이 생겨서 송금 요청이 코어뱅킹 서버에 도달하는게 너무 늦어지고 있는데,

    • 그 사이에 송금 서버의 송금 상태 조회 요청이 먼저 코어뱅킹 서버에 도착하게 되면

    • 코어뱅킹 서버는 송금 요청이 없었다고 응답할 것이고 송금 서버는 송금 실패로 처리할 것이다.

    • 그런데 뒤늦게 송금 요청이 코어뱅킹 서버에 도달한다면 그대로 송금 요청이 실행될 것이다.

  • (해결 방안) 이 문제는 타임아웃 시간을 약속하고 그보다 오래된 요청은 거절하는 방법으로 해결한다.

    • 송금 서버와 코어뱅킹 서버가 서로 타임아웃 시간을 1분으로 약속하고 송금 서버는 요청을 보낼 때 요청 시각을 포함시켜서 보낸다.

    • 타임아웃 시간이 지났는데 코어뱅킹 서버로부터 송금 완료 메시지가 오지 않으면 송금 서버는 코어뱅킹 서버에게 송금 상태를 물어본 뒤 ‘없음’ 응답이 오면 실패로 처리한다.

    • 송금 요청이 뒤늦게 코어뱅킹 서버에 도달하면 코어뱅킹 서버는 요청 시각을 보고 타임아웃 시간이 지났다면 처리하지 않고 거절한다.

거래내역 동기화 문제 해결 방안

  • 송금 서버가 코어뱅킹 서버로부터 송금 완료 메시지를 받아 이력을 저장하는 거래내역 동기화와 관련하여 몇 가지 문제가 있다.

  • (문제1) 앞서 카프카를 통해 송금 완료 여부를 확인하여 송금 이력을 저장한다고 말씀드렸는데, 만약 저장하는 도중 어떤 에러가 발생해서 실패하면 어떻게 될까?

    • 예를 들어 순간적인 DB 장애로 저장이 실패될 수 있다. 그렇게 되면 해당 송금 건은 유저에게 보이지 않게 된다.

    • (문제1-해결방안) 이런 경우 송금 서버는 잠시 후에 송금 완료 메시지를 다시 컨슘(4번)하고 이력을 저장한다.(5번)

  • (문제2) 이렇게 재시도를 했을 때도 여전히 실패할 수 있다. 이런 경우에는 몇 번을 재시도든 실패하는 상황인 것으로 간주하고 더 재시도하지 않는다.

    • (문제2-해결방안) 대신, 컨슈머 데드 레터(Consumer DeadLetter) 라는 카프카 토픽에 실패한 메시지를 저장하고 개발자가 실패하는 원인을 확인하여 문제를 해소한 뒤 해당 토픽을 다시 컨슘해서 송금 이력을 저장하게 된다.

  • (문제3) 또 다른 무제는 송금 이력이 누락되는 가능성이 있다.

    • 토스뱅크 통장에 500원이 입금된 후 100원이 출금되었고, 이것이 카프카를 통해 송금 서버에 동기화되는 과정을 생각해보자.

    • 각각 동기화된 뒤 토스앱에 거래내역 조회를 요청하면 정상적으로 거래내역이 조회될 것이다.

  • 문제는 500원 입금 동기화가 실패하고 100원 출금 동기화가 성공한 뒤 500원 입금 재동기화가 성공하기 전에 거래내역을 조회한 경우이다.

    • 유저는 100원 출금만 동기화가 완료된 상태이므로 유저는 존재하지도 않은 100원이 출금되어 총 잔액이 -100원 되어있는 이상한 상황이 만나게 된다.

  • (문제3-해결방안) 이 문제를 해결하기 위해 ‘송금이 완료되었다’는 카프카 메시지를 받았을 때, 그것만 동기화하는 것이 아니라 그 이전에 다른 거래가 있는지, 코어뱅킹 서버에 조회해서 동기화하는 것이다.

  • 모든 거래내역에는 거래 순서대로 증가하는 일련번호가 붙어있기 때문에,

    • 송금 서버에 저장된 가장 최근 거래 건과 카프카 메시지로 받은 거래 건의 일련번호를 비교하면

    • 송금 서버는 누락이 있는지의 여부를 판단할 수 있다.

  • (정리) 따라서 이 케이스에서 송금 서버는 100원 출금 건을 동기화할 때 500원 입금이 누락되었음을 발견하고, 이것을 먼저 동기화하고 그 후 100원 출금 건을 동기화한다.

  • 이를 통해 언제나 과거 거래내역이 누락되지 않음을 보장한다.

  • (문제4) 그런데 만약 500원 입금 동기화가 또다시 실패한다면 어떻게 될까?

  • (문제4-해결방안) 이 경우 100원 출금건도 동기화하지 않는다.

    • 그래야 순간적으로 잔액이 -100원이 되는 문제를 피할 수 있기 때문이다.

    • 또한 이 카프카 메시지 컨슘은 실패로 처리되므로 송금 서버는 다시 컨슘하여 동기화를 시도할 것이다.

  • (문제5) 그런데 만약 이 동기화가 완료되기 전에 유저가 거래내역 조회를 하게 된다면 어떻게 될까?

  • 유저는 입금된 500원과 출금된 100원이 모두 보이지 않는 문제를 겪게 될 것이다.

    • 입금된 거래가 다소 늦게 보이는 것은 입금이 오래된 것으로 이해해줄 수 있지만

    • 방금 100원 출금이 성공했다는 메시지를 보았는데 거래내역에 100원 출금된 것이 없다면 유저는 굉장히 당황할 것이다.

  • (문제5-해결방안) 이 문제를 해결하기 위해 송금 서버는 진행중인 거래내역을 DB에 저장하는데 유저가 계좌의 거래내역을 조회할 때 아직 진행중인 거래가 있다면 그 즉시 계정계와 거래내역을 동기화한다.

    • 따라서 유저가 거래내역을 조회하면 진행중인 100원 출금이 있음을 발견하고 동기화한다.

    • 이때 500원 입금도 아직 동기화되지 않는 상태이므로 그것을 100원 출금보다 먼저 동기화한다.

동기화가 지연되는 문제 해결 방안

  • 만약 한 번에 백만 건의 거래내역을 동기화해야 하는 일이 생기면 어떻게 될까

  • 예를 들어 어떤 돈 많은 회사가 전국민에게 100원씩 그냥 주는 이벤트를 한다고 가정해본다.

  • 100만명의 토스 고객에게 1시간에 걸쳐서 입금이 완료된다면 1초에 300건 정도의 입금이 실행될 것이다.

  • 코어뱅킹 서버가 카프카를 통해 송금 서버에게 입금을 알리면 송금 서버는 순서대로 하나씩 동기화한다.

  • 만약 한 건 처리에 100ms 가 걸린다면 1초에 10건씩만 동기화가 될 것이므로 1초에 300건씩 들어오는 입금을 다 처리하지 못하고 점점 밀리게 된다.

  • 1시간이 지나면 거의 백만 건이 밀려있을 것인데 이것을 모두 처리하려면 28시간이 걸리므로 이 시간동안 누군가 토스뱅크로 입금을 하면 밀린 입금 거래의 동기화가 다 끝나기 전까지는 해당 입금 거래가 안보이는 문제 가 생길 것이다.

  • 하지만 다행히도 모든 카프카 메시지를 하나의 스레드에서 하나씩 처리하지 않는다.

  • 카프카는 메시지를 여러 파티션으로 나눠서 여러 개의 컨슈머가 처리할 수 있게 해준다.

  • 또한 이때 유저가 정한 키를 기준으로 파티션을 나눠준다.

  • 예를 들어, 거래내역을 동기화하는 경우에 계좌번호를 키로 삼으면

    • 같은 계좌의 거래내역은 반드시 같은 파티션에 들어가게 되고

    • 같은 파티션은 같은 컨슈머가 처리하기 때문에

    • 같은 계좌를 여러 컨슈머가 동기화하면서 발생할 수 있는 동시성 문제를 방지할 수 있다.

  • 이 동기화의 경우 초당 300건의 입금 요청을 처리해야 하므로 파티션이 30개라면 충분히 처리할 수 있을 것이다.

  • 하지만 유저가 계속 늘어나면서 더 높은 처리량을 요구하게 되면 어떻게 될까?

  • 앞으로 계속 파티션을 무한정 늘리면서 대응하면 충분할까? 그러기엔 어렵다.

    • 파티션을 늘리기에는 시간이 걸리기 때문에, 그 사이에 유저가 동기화 지연 문제를 겪게 될 수 있다.

    • 아예 지연 문제가 안생기도록 피크시를 기준으로 파티션을 아주 넉넉하게 잡을 수도 있겠지만, 늘어난 파티션들은 시스템 자원을 차지하게 되며 한번 늘린 파티션은 다시 줄일 수 없기 때문에 지속적인 자원 낭비가 될 수 있다.

  • 따라서 파티션 갯수는 적당한 수준으로 유지하고 대신 컨슈머별로 워커 스레드를 충분히 할당한다.

  • 파티션을 10개로 하고 컨슈머당 워커 스레드를 100개로 한다면, 총 1천개의 스레드로 건당 100ms 속도로 동기화를 하게 되므로 초당 1만건의 동기화가 가능해지게 된다.

  • 단, 이때 각 워커 스레드가 같은 계좌에 대한 동기화를 동시에 실행하게 된다면, 이미 동기화가 완료된 거래를 재동기화하려고 시도하는 등의 시도로 코어뱅킹 서버에 불필요한 트래픽을 유발할 수 있다.

  • 이를 위해 카프카 파티션이 그랬던 것과 같은 요령으로 동기화 작업을 실행할 워커스레드를 선택할 때 계좌번호 기준으로 선택하게 하여 같은 계좌는 항상 같은 스레드가 동기화하도록 한다.


  • 그런데 이 모든 방어책에도 불구하고 어떤 예상치 못한 문제로 인해 동기화 안되고 있는 상황을 유저가 마주칠 수 있다.

  • 그런 경우를 대비한 최후의 수단으로 유저가 직접 동기화를 실행할 수 있는 버튼을 만들어 두었다.

  • 유저가 거래내역에 최신화되지 않은 것 같다고 느낀 경우 불러오기 버튼을 눌러서 즉시 거래내역을 최신화할 수 있다.

Review

  • 사실, 이 영상을 2023년에 봤던 기억이 있다. 그 당시에는 Java, Spring 기반의 개발 경험이 있었지만, Kafka에 대한 지식은 부족해 전체 내용의 50~60% 정도밖에 이해하지 못했다.

  • 하지만 올해 Kafka를 학습하고 다시 영상을 보니, 이전에 보이지 않았던 기술적 깊이의 의사결정 과정이 눈에 더 잘 들어오면서 이해도가 80-90% 로 올라왔다.

  • 영상을 통해 여러 실용적인 아키텍처 전략을 배울 수 있게 되었다.

    • 첫 번째는 타임아웃을 1분으로 제한하여 무한 대기를 방지하고, 재처리가 어려운 메시지는 DLQ(Dead Letter Queue)로 보내 안정성을 확보하는 방법에 알게 되었고,

    • 두 번째는 동기화 지연 문제를 해결하기 위해 계좌번호를 키(Key)로 사용해 파티션을 분배하고, 컨슈머와 워커 스레드 수를 늘려 병렬 처리로 처리량을 극대화하는 방법을 알게 되었다.

  • 영상에서 다룬 내용에 더해, 브로커와의 관계까지 고려하면 다음과 같은 전략을 고려해볼 수 있을 것 같다. ‘카프카 핵심 가이드’라는 책에서 본 내용인데, 실무에 적용하기 전 좋은 참고 자료가 될 것 같다.

    • 일반적으로 파티션 수에 대한 명확한 기준이 없다면, 클러스터의 브로커 수와 맞추거나 그 배수로 설정하여 부하를 고르게 분산시키는 방법을 사용한다.

    • 예를 들어 브로커가 10대일 때 파티션을 10개로 생성하면, 각 브로커가 파티션 리더를 하나씩 담당하게 되어 처리량을 최적화할 수 있다.

  • 이번 영상을 보면서 느낀점은 당장의 비즈니스 문제 해결도 중요하지만, 그에 필요한 기술도 미리 학습하고 개인 프로젝트로라도 경험해보는 것이 얼마나 중요한지 다시 한번 깨닫게 해주었다.

  • 그런 의미에서, 토스가 문제를 깊이 있게 정의하고 기술적으로 해결해 나가는 과정이 인상 깊었고 배울점이 많았다.

Reference


Recommend

Index