백엔드 공부

SLASH22의 왜 은행은 무한스크롤이 안되나요 영상을 보고..

철매존 2024. 4. 20. 02:48
728x90

카카오 브런치에서 백엔드를 담당하고 있는데, 거기서 연재 작품 전체 라는 슬롯에서 무한스크롤을 적용해 보았다.
그리고 우연한 계기로 토스뱅크에서 무한스크롤을 도입한 계기가 있어서 이를 살펴 보았는데, 좀 신기하고 재밌어 보였다.

SLASH22의 왜 은행은 무한스크롤이 안되나요 를 보고 정리해 보았다.

상황

  • 보통 은행앱이 조회기간 설정을 한다(1주일, 1개월, 6개월 등등...)
  • 그리고 지정된 기간만큼이 쫘라락 조회되는(그만큼만) 방식이다. -> 즉 스크롤은 있는데 무한스크롤로 페이지 조회 후 넘어가는 이런게 없다.

상황 이해

  • 이 이유를 알려면 은행 시스템에 대한 이해가 필요한데
    • 은행 앱 - 채널계 - 계정계
      • 은행 시스템은 이렇게 분리되어 있다.
      • 계정계는 실제로 유저의 돈을 다루며 원본 데이터가 저장되는 영역이다.
        • 이거는 실제 장애나 오류가 발생하면 치명적이라 아주 높은 신뢰도가 요구된다.
      • 채널계는 유저의 요청을 직접 받아 처리하는 영역이다.
        • 돈을 다루는건 여기서 하지 않고 그런 요청은 계정계로 보내준다.
      • 토스의 경우에도 이와 비슷하게 되어있고 채널계는 k8s로 클러스터링 되어있다.
        • 네트워크 구조가 복잡하고 DB가 여러개로 나뉘어 있어서 경우에 따라 트랜잭션 처리가 어려운데, 스케일 아웃이 편하고 DB분리도 가능해서 큰 트래픽을 다루는데 유리하다.
      • 계정계는 하나의 서버와 하나의 DB로만 되어있다.
        • 성능을 희생해서라도 이렇게 하는거임

스크롤에 대해서

  • 거래 내역같은거는 중요 정보이기 때문에 계정계에서 관리된다.
  • 즉 무한스크롤 요청이 온다면
    • 채널계가 고객의 거래내역 확인 요청 받음.
    • 계정계에 그 요청 전달
    • 계정계에서 나온 결과를 앱으로 전달
  • 위의 방식에서 문제는 계정계는 속도가 느리기 때문에 광범위한 거래내역을 빠른 응답시간에 제공해주기가 어렵다는 것이다.
    • 그래서 보통 은행들이 디폴트 기간 설정을 짧게 했던것!
  • 근데 토스는 이게 무한스크롤이 된다.
    • 그 이유는 그냥 송금 서버에서 거래내역을 반환하기 때문
      • 이게 가능하기 위해서는 채널계랑 계정계가 완벽히 동기화 되어있어야 한다.

처리 방법

  • 채널계의 거래내역이 계정계에 있는 거래내역과 같으면 채널계에서 바로 반환해서 빠르게 처리 가능
    • 다만, 데이터는 완전히 동일해야 한다.(신뢰성을 위해)
    • 근데 너무 자주 동기화하면 안된다.(부하가 커지니까)

단계별 처리방법

  1. 유저가 송금 요청을 할 때 채널계에서 계정계로 요청을 보내기 전에 DB에 저장해준다.
    • 내역이 잘 저장될테니 이대로 활용?
      • 이렇게 하면 다른 은행에서 토스뱅크로 입금한것은 조회가 안될것이다.
  2. 위의 문제를 해결하기 위해 타행에서 입금되는 경우 계정계에서 채널계로 입금 사실을 알려주도록 한다.
    • 토스뱅크에서는 kafka를 이용한다.
      • 계정계 서버에서 kafka에 produce -> 채널계에서 consume해서 저장
        • 이렇게 하면 일단 된다고 하는데, 이상적으로 동작을 안한다면? (예외상항 발생)
          • 예를 들어 송금 실행중 계정계 응답이 늦어져서 채널계가 응답을 받지 못한다면?
            • 채널계가 거래내역 저장을 안하고, 실제로는 거래 성공인데 못보게 된다(채널계와 계정계의 불일치 발생)
  3. 위의 예외 문제는 kafka로 처리 가능
    • api실행 중 타임아웃 발생해도 위에서와 마찬가지로 계정계에서 처리 완료되면 kafka를 통해 채널게에 알려주면 DB에 저장할 것이다.
      • 즉 어쨌든 거래가 완료된다면 그거를 조회 가능하다는 것이다!
        • 그러면 만약에 api실행중(거래 도중에) 타임아웃이 발생해서 실패했다고 유저가 파악하고 한번 더 거래를 시도하게 된다면??
  4. 최초 거래 요청시 채널계에서 DB에 거래 요청 저장(성공이 아니라)하고, 완료되지 않은 거래가 있는 유저가 다시 거래요청하면 거절해버린다.
    • 나중에 이력 저장까지 완료되면 새로운 거래를 수락하면 된다.
      • 근데 그러면 영원히 거래가 안된다면?
      • 예를 들어 네트워크 문제 등으로 송금 요청이 손실되는 등의 경우 계속 진행중으로 남을텐데?
  5. 위의 문제 해결을 위해 주기적으로 채널계는 계정계에 거래 요청 상태를 확인한다.
    • 아예 네트워크 문제로 도달도 안했으면 계정계가 거래 요청이 없었다고 할테니, 이 경우는 그냥 실패처리한다.
      • 근데 아주 희박하게.. 성공을 실패로 처리할수도 있다.
        • 예를 들어 (1)거래요청했는데 네트워크 이슈로 전달 지연 (2)그 사이에 상태 조회했는데, 아직 전달 안돼서 그런거 없다함 (3)실패처리 (4)아까 1번에서 보낸 거래요청이 도달 (5)실제로는 성공했는데 실패라고 채널계에 저장
  6. 이 문제는 타임아웃 시간을 약속하고 그보다 오래된 요청은 거절하는 방법으로 해결
    • 예를 들어 서로 타임아웃 시간을 1분으로 약속하고 채널계는 요청을 보낼 때 요청시각 포함시켜서 보낸다.
      • 그 이후에 연락이 없으면 채널계가 계정계에 물어보고 없음처리 한다.
        • 그리고 채널계의 요청이 뒤늦게 온다면 요청 시각을 보고 타임아웃 시간 이외면 거절해버림
          • 근데 만약에 순간적인 DB장애로 저장이 안된다면?
            • 그러면 이 거래내역은 보이지 않는다.
  7. 그러면 채널계에서 다시 한번 컴슘해서 이력을 저장
    • 근데 그렇게 재시도 해도 저장이 안될수도 있는데?
      • 그러면 그냥 아예 실패라고 가정하고 더이상 시도하지 않는다.
        • 그 대신 컨슈머 데드레터라는 카프카 토픽에 실패한 메시지를 저장하고 개발자가 문제 해결한 뒤에 이 토픽을 다시 컨슘해서 거래 이력을 저장한다.
          • 만약에 거래 이력이 누락되게 된다면??
            • 예를 들어 (1)500원 입금 동기화 (2)100원 출금 동기화 되는 과정을 생각해보면
              • 각각 kafka를 통해 동기화
              • 앱이 거래내역 조회를 요청하면
              • 정상적으로 조회될것이다.
                • 자 근데 만약에
                  • 500원 입금 저장 실패 (그러면 다시 컨슘 기다림)
                  • 100원 출금 저장 성공
                  • 거래내역 조회
                    • 이렇게 하면 100원 출금에 대해서만 동기화가 완료된거인데 유저는 존재하지도 않는 100원이 출금된걸 보게된다.
  8. 이걸 해결하기 위해 채널계에서 송금이 완료되었다는 Kafka 메시지를 받았을 때 그것만 동기화하는게 아니라 다른 거래가 있었는데 계정계에 조회해서 동기화한다.
    • 모든 거래내역에는 거래 순서대로인 일련번호가 있기 때문에 이번에 받은게 잘 들어온 것인지 확인한다.
      • 그렇게 하면 과거 거래내역이 누락되지 않았음을 보장할 수 있다.
        • 근데 만약에 500원 입금이 계속 안된다면?
          • 유저는 거래내역을 확인해도 아무것도 안보이는 문제를 겪게 될 것이다.
            • 아니 근데 100원 출금은 성공했다고 했는데 거래내역에 출금된게 없으면 안되지
  9. 아까 말했듯 채널계는 진행중 거래내역을 DB에 저장하는데, 유저가 계좌의 거래내역을 조회할 떄 아직 진행중인 거래가 있다면 그 즉시 계정계와 거래내역을 동기화한다.
    • 그래서 유저가 거래내역을 조회하면 진행중인 100원 출금이 있음을 알게될테고 동기화를 하면 500원 100원 모두를 동기화한다(이거는 계정계에 잘 저장된 후 Kafka에서 온거를 동기화 실패한 경우에)
      • 근데 동기화가 지연되는 경우가 있다면?
        • 진짜 엄청많이 들어가게 된다면
          • 전국민한테 100원씩 준다 뭐 이런거 하면 계정계는 속도가 느리니까 점점 동기화가 밀리게 될것이다.
            • 그거 다 처리하려면 엄청나게 시간이 미뤄져서 그보다 늦게 보낸거는 며칠 뒤에 올수도
  10. 근데 사실 카프카가 한개는 아님
    • 당연한게 채널계가 여러개가 있다고 했는데 카프카 파티셔닝 하면 되는거임
      • 계좌번호같은걸로 파티셔닝 하면 동시성 문제도 방지 가능(같은 계좌를 여러 서버에서 처리하지 않으므로)
        • 근데 유저가 계속 늘어나면서 처리량이 계속 늘어난다면? 그러면 파티션을 무지성으로 늘리기는 너무 힘들거다.
          • 그리고 파티션을 한번 늘리면 줄일수도 없으니까 막 늘리면 안될것
  11. 파티션은 적당히 만들고 내부의 워커스레드를 많이 할당하면 된다.
    • 참고로 이것도 하나의 계좌를 여러 워커스레드가 처리하면 이미 동기화가 완료된걸 다시 동기화 하려는 시도로 불필요한 트래픽을 유발할 수 있어서 같은 계좌는 항상 같은 워커 스레드가 동기화하도록 해야한다.
      • 근데 만약에 이런 경우에도 불구하고 동기화가 안된다면...?!
  12. 유저가 직접 동기화하는 버튼 만듬
    • 유저가 뭐가 요상하다 하면 불러오기로 즉시 동기화 가능

개인적 생각

  • 사실 내가 적용한 것은 딱 토스뱅크에서 한 내용이랑은 좀 다르긴 한데 확실히 배울점이 많다는 생각이 들었다.
    • 스크롤용 집계 데이터를 만들고(여러 테이블에 데이터가 나뉘어져 있어 성능을 위해 이를 하나의 집계테이블로 보여줘야했다.) <- 이게 채널계라는 생각
    • 실제 글이 생성되면 그 글이랑 관련 내용들이 보관되는게 계정계의 역할 <- 뭐가 더 중요하다. 다른 서버에 있다 이런건 아니지만 쿼리를 할 때에 여기서 바로 가져오면 느리고 이를 집계쪽에 보관하는 흐름이 비슷해서
      • 다만 토스뱅크에서 처리한것은 채널계-계정계 간에 데이터의 차이가 있는것은 아니기 때문에 동기화가 상대적으로 수월해 보이기는 했다. 뭔가를 가공해서 만들어낸다면 또 생각을 해야하니까..
  • 나는 요거를 2가지 방법으로 생각해 보았다.
    • 배치를 통해 집계데이터를 만드는 방법.
      • 딱 토스에서 은행은 무한스크롤이 안되는 이유로 말했던 신뢰성과 굉장히 큰 연관을 가진 내용이라는 생각이 들었다. 명확하게 둘간의 비일치가 발생하므로 (그리고 배치가 실패한다면 진짜 큰 차이가 생길 것이다)
    • 여러 상황에 따라 동기화를 진행하는 방법.
      • 실시간성을 보장하기 위해 집계테이블에 변화가 필요한 시점에 동기화를 적용하는 방법이었다.
      • 이 때에도 여러 상황을 고려했고 처리 상황에 발생 가능한 속도 이슈나 문제 상황을 가정해서 처리하였다.
      • 사실 이런 여러 상황에 맞춘 동기화가 매우 좋기는 하지만, 결국 코드가 더러워지고 유지보수하기 매우 까다롭다는 지적을 받았었다.

정리 및 궁금점

  • kafka가 과거 메시지 재생과 장기간 메시지 보관을 할 수 있는데, 그게 이런 식으로 활용되는게 신기했다.
  • 아 진짜 다양한 상황을 고려하는구나
    • 근데 이러면 유지보수는 괜찮을까?
      • 생각해보니 거래내역의 변경이 있어도 둘이 같이 바뀌고 채널계랑 계정계의 불일치가 발생해서 이걸 뭐 어떻게 해야하는 경우가 없을 것 같다. 그러니 지금 만들어진 내용에서 뭔가 차이가 생겨서 공사가 발생할 일은 적을 것 같다.
    • 그러면 코드가 복잡해지고 여기저기 관련 코드가 들어가지 않을까?
      • OOP 쓰면 될것같기도 하고 궁금하다.
  • 그럼 내 코드에 적용할 수 있는거는 뭘까?
    • 당장 적용할 수 있는 내용은 없을 것 같고, kafka를 이용한 방법은 한번 실시간 동기에서 활용해보면 좋겠다는 생각은 들었다.
  • 뭔가 확실히 금융쪽은 중요한 정보를 다루니 DB구축부터 API설계까지 다양한 것들을 고려하는구나(그럴 필요도 있고 그렇게 하는걸 장려하는구나) 싶어서 뭔가 대단해보이고 부럽기도 하고 신기하기도 하고 이런느낌이 들었다.
    • 그리고 기존 금융들이 뭘 안바꾸려고 하는 이유랑 속도가 느린 이유도 좀 많이 이해가 갔다.
      • 이야 이거 잘못고치거나 하면 끝장이라 코드를 잘 안걸드리는거겠구나