Skip to content

Latest commit

 

History

History
84 lines (45 loc) · 9.18 KB

toss-bank-system-architecture.md

File metadata and controls

84 lines (45 loc) · 9.18 KB

토스뱅크 대출 시스템에 도입한 유량 제어 시스템

토스뱅크의 완전히 새로운 대출 시스템 by 토스뱅크 민재슬

대출 시스템을 생각해보면, 대출이 실행되기 위해서는 고객의 신용정보가 필요하며 신용정보는 허가받은 기관과의 통신을 통해서 사용할 수 있습니다. 또한 현재 고객이 가지고 있는 대출 정보와 안전한 대출 실행을 위해 필요한 부가 정보 등을 대외기관과의 연동을 통해서 가져올 수 있습니다. 이때 대외기관의 응답 속도 문제가 발생하는데, 기존 금융권에서는 여러 기관에 동시에 호출하는 비동기 처리로 이 문제를 해결했습니다. 하지만 사용자 수가 갑자기 증가하게 되면 대외기관이 처리할 수 있는 트래픽의 양보다 많은 요청을 동시에 보내게 되어 결국 상대 대외기관의 시스템이 마비되는 현상이 자주 발생했습니다. 토스뱅크는 대외 기관이 견딜 수 있을 만큼의 트래픽을 처리하고 유입되는 사용자 수에 맞게 동적으로 요청을 처리할 수 있는 '유량 제어 시스템'을 도입했습니다.

  • 비즈니스 로직을 처리하기 위한 관계형 데이터베이스: MySQL
  • 이벤트 처리를 위한 메시지 서비스: Kafka
  • 유량 제어와 캐시 처리: Redis

MySQL과 DB Migration 전략

비즈니스 로직을 독립된 마이크로 서비스 내에서 직접 구현하고 있기 때문에 애자일한 조직에서의 비즈니스 변화에 맞춰 데이터베이스 스키마는 끊임없이 변화해야 합니다. 하지만 데이터베이스의 잦은 스키마 변화는 협업을 어렵게 하고 오류가 발생할 수 있는 작업입니다. 안정성을 가져가면서 제품 개발의 속도를 버릴 수는 없기 때문에 토스뱅크는 시스템 구축 초기부터 Flyway라는 데이터베이스 마이그레이션 툴과 Hibernate의 DDL Validation을 적용하기로 결정했습니다.

  • Flyway를 사용하면 버전별 SQL 스크립트를 VCS로 관리할 수 있기 떄문에 비즈니스 소스 코드와 함께 코드리뷰를 할 수 있다.
  • 서버 어플리케이션을 실행하기만 하면 DB 세팅이 완료된다. (모든 환경에서 같은 DB 환경을 공유할 수 있음!)
  • 실수로 데이터베이스 형상을 반영하지 않고 서비스 로직이 배포되는 등의 휴먼 에러를 방지할 수 있다.

메세지 서비스 Kafka

많은 트래픽이 한꺼번에 들어온다고 할지라도 파이프라인과 정해진 스레드 풀을 통해서 요청이 나가게 된다면 대외기관이 견딜 수 있을 정도의 TPS만 전달하게 됩니다. 컨슈머와 연동된 스레드 풀은 서버의 상태에 맞게 동적으로 사이즈를 조절할 수 있는 풀입니다. 대외기관의 TPS는 한정적이고 Kafka로 구성된 파이프라인과 동적으로 구성 할 수 있는 스레드 풀로도 충분히 컨트롤 할 수 있는 IO 구간입니다.

유량 제어에서의 지표 수집

대외기관의 처리시간을 측정하기 위해 서버는 대외기관의 파이프라인 전재 직전 이벤트의 시작 시각을 Redis에 기록하고 대외기관과 연동이 끝난 뒤 응답완료 시각과 성공 실패 여부를 Redis에 기록합니다.

통계 분석 결과 대외기관이 약속된 TPS보다 성능이 떨어지게 되고 실패율이 점차 증가하게 된다면 대외기관 파이프라인에 있는 dynamic 풀의 사이즈를 조정하여 대외기관이 견딜 수 있는 수준의 요청 수로 조정합니다.

만약 이렇게 스레드 풀 사이즈를 조정했는데도 실패 응답이 계속된다면 상호 보완이 가능한 대체 기관에서 정보를 가져올 수 있도록 마이데이터의 서킷을 half_open 처리해서 앱 스크래핑으로 전환합니다. 마이데이터가 완전히 제 기능을 하지 못하는 상태라면 서킷을 open하고 모든 고객이 인증서를 통한 앱 스크래핑으로 강제 전환하게 됩니다.

하지만 KCB와 NICE 처럼 신용평가에 반드시 필요한 기관에서 문제가 발생한다면 어떻게 해야할까요? 해당 정보 없이는 대출을 진행할 수 없고, 보완 가능한 대외기관이 존재하지 않기 때문에 대외기관의 서킷을 open하고 고객의 대출 상품 전체의 진입 서킷도 연동해서 같이 open해 고객의 대출 상품 진입을 차단해야 합니다.

높은 정합성과 신뢰성

데이터 정합성: 서로 다른 위치에 보관된 동일한 데이터가 일치하는지 여부를 나타낸다. by WikipediA

모놀리틱 기반에 비해 MSA 기반이 가지는 장점
각 도메인 별 모듈들이 독립적으로 구성되어 있기 때문에 1. 효율적인 자원관리가 가능하고, 2. 모듈의 장애가 전체 장애로 전파되지 않도록 한다.

MSA 구조에서는 어떻게 동시성을 제어할까?

Lock을 사용할 수 있어요.

테이블에 락을 걸어 동시성을 제어할 수 있습니다. 하지만 여러 테이블의 데이터를 변경하는 로직일 경우 여러 테이블에 모두 락을 걸어야하기 때문에 성능 저하와 데드락이 발생할 수 있습니다.

Lock을 위한 테이블을 만들어볼까요?

이런 경우 별도의 락을 위한 테이블을 만들어 트랜잭션이 시작할 때 해당 테이블에 락을 획득하도록 해 문제를 해결할 수 있습니다.

위와 같이 특정 계정에 대한 락 데이터를 가지는 락 테이블을 만들고, 트랜잭션 시작 시 락 테이블의 특정 계정에 대한 배타 락을 잡게 해 실제 테이블에는 락을 걸지 않게 설계할 수 있습니다.

하지만 MSA 구조에서는 각 작은 모듈들이 독립적인 데이터베이스를 사용하고 있습니다. 또한 하나의 서버가 여러 인스턴스를 사용하고 있을 수도 있습니다. 따라서 락 테이블을 사용하는 방식은 MSA 구조에서는 서비스 간의 높은 결합도를 만들고 비효율적인 자원 사용을 야기할 수 있습니다.

MSA 기반에서는 분산락을 사용해볼 수 있겠네요.

Redis 기반의 분산락을 이용해 해결할 수 있습니다. Redis 기반 분산락을 사용함으로써 모듈의 데이터베이스를 강제하지 않고 서비스 간의 결합도도 느슨하게 유지할 수 있습니다.

왜 Redis인가요?

이러한 공유되는 데이터를 관리하는 DB에는 여러 서버에서 요청을 보내기 때문에 높은 처리량이 보장되어야 합니다. Redis는 메모리 기반 저장소이기 때문에 RDBMS를 사용하는 락 테이블 방식에 비해 보다 높은 처리량을 제공할 수 있습니다.

분산락이 만능일까요?

분산락은 모드 서버가 공통으로 사용하기 때문에 하나의 트랜잭션이 무한정 락을 소유하고 있을 경우 다른 서버들의 요청이 무한정 대기할 수 있는 데드락에 빠질 수 있습니다. 때문에 분산락을 사용할 경우 적절한 타임아웃 설정이 필요합니다. 하지만 타임아웃을 설정하게 되면 타임아웃 이후 커밋이 일어나는 등의 상황에서 갱신 유실이 발생할 수 있는 문제가 있습니다.

갱실 유실을 방지하기 위한 방법으로는 원자적 연산 사용, 명시적 잠금, 갱신 손실 자동 감지, CAS(compate-and-set) 연산 등이 있습니다.

JPA를 사용하는 경우 @OptimisticLocking을 이용해 CAS 연산을 간단하게 구현할 수 있습니다. 낙관적 락은 version을 통해 갱신 유실을 방지합니다. update가 일어날 때마다 이전에 select 시점의 version과 현재 version이 일치하는지 확인하고 일치하는 경우에만 update를 수행하면서 version을 갱신하기 때문에 갱신 유실을 방지할 수 있습니다.

그러면 분산락 없이 낙관적 락만 사용해도 되는거 아닌가?

분산락이 없다면 즉 낙관적 락만 사용하는 경우 version이 불일치하면 예외를 던지면서 롤벡을 수행하기 때문에, 동시에 발생하는 트랜잭션들은 대기 없이 실패하게 되거나 별도의 재시도 구현이 필요합니다. 하지만 트랜잭션 재시도 구현은 재시도 자체의 실패 등 여러 케이스들을 고려해야하며 이는 곧 코드의 복잡도 상승으로 이어지게 됩니다.

대부분의 상황에서 분산락은 정상적으로 동작하기에 분산락으로 동시성을 제어하면서 갱신 유실과 같은 만약의 상황의 경우 낙관적 락을 통해 데이터 정합성이 틀어지지 않도록 하는 방법을 사용하는 것입니다. (물론 비즈니스 상황에 따라 여러가지 동시성 제어 방법들이 있겠죠?)

해외구간 네트워크 지연으로부터 안전하게 서비스하기