Skip to content

우리 프로젝트는 왜 이렇게 설계되었나요?

nawhes edited this page Dec 5, 2021 · 3 revisions
tags: techtalk

우리 프로젝트는 왜 이렇게 설계되었나요?

서론

Boostock은 주식 거래 시스템을 개발하는 프로젝트입니다.

서비스 관점에서는 서비스를 원활하게 이용하는 것이 우선 목표이고

프로젝트 관점에서는 제한된 자원에서 많은 트래픽을 처리해내는 것이 우선 목표입니다.

서비스를 원활하게 이용하기 위한 요구사항은 아래와 같습니다.

  1. 모든 사용자가 동일한 정보를 얻을 수 있어야 한다.
  2. 모든 사용자의 요청은 정해진 규칙에 따라서 처리되어야 한다.
  3. 사용자의 요청은 다른 이유로 인해 누락되어선 안된다.
  4. 사용자의 요청은 예상할 수 없는 결과를 만들어선 안된다.

이런 요구사항을 만족하기 위해서 가장 먼저 안정적인 데이터베이스 시스템을 필요로 했습니다.

우리의 기술역량을 바탕으로

ACID 원칙을 준수할 수 있는 데이터베이스로 MySQL을

사용자의 요청을 수신하고 피드백을 제공할 웹 애플리케이션 서버는 nodejs을 채택해서

첫번째 프로젝트 아키텍처가 제시되었습니다.

제한된 성능 아래에서 목표를 만족할 수 있을까?

업비트 통계자료와 빗썸 통계자료에 따르면 업비트는 월 최대 거래량이 월 16억건, 빗썸은 월 최대 거래량이 2억건임 알 수 있었고

우리의 프로젝트의 목표는 이렇게 설정했습니다.

  • 최대 거래량 : 빗썸의 50% 수준의 월 1억건 (초당 약 38.6회)
  • 최소 거래량 : 최대 거래량의 10% 수준의 월 1,000만건 (초당 약 3.8회)

한 트랜잭션당 생성되는 로그 데이터의 크기를 36Byte로 계산했을 때 1억건의 트랜잭션은 3.6GB의 크기가 생성됩니다.

이처럼 끈임없이 늘어나는 거래 체결로그와 매 분당 생성될 그래프 데이터를 하나의 MySQL에서 다루면 MySQL의 성능 한계에 빠르게 도달할 것이었고 거래 체결이 원활히 이뤄지지 않을 것이라는 예상을 할 수 있었습니다.

이대로 가다간 답이없다

이후 인메모리DB로의 마이그레이션을 염두하고 있던 저희는

로그의 성격을 띄는 데이터를 따로 분리해서 저장함으로써 MySQL의 부하를 낮춰서 거래 체결에만 집중하기 위한 조치를 취하기로 결정했고

프로젝트 아키텍처는 아래처럼 변경되었습니다.

제한된 자원을 효율적으로 활용하기 위해 수평 확장이 가능한 MongoDB를 채택하여

주문체결내역과 그래프 데이터를 추가하고 조회하는 역할을 부여했고

MySQL는 주문요청과 주문체결과 관련된 핵심 데이터에 집중할 수 있게 되었습니다.

주문이 접수가 안되는 상황만은 피해야 한다.

이번에는 웹 애플리케이션 서버가 혼잡한 상황을 고려해보았습니다.

웹 애플리케이션 서버가 맡은 동작을 정리했을 때

  1. 사용자로부터 주문접수
  2. 접수받은 주문을 처리(가능하면 체결)
  3. 주문처리된 내용을 모든 사용자에게 전송

거래체결 작업은 데이터베이스 접근과 수정이 빈번하기 때문에 많은 리소스를 요구할 것이라고 판단했습니다.

싱글쓰레드로 동작하는 nodejs에서 리소스가 큰 작업때문에 task가 쌓이는 경우에는 전반적인 성능저하가 올 것이라는 생각과

API 서버가 요청에 빠른 처리를 하지 못해서 사용자에게서 들어오는 요청을 접수하지 못하는 최악의 상황은 면해야 했습니다.

그래서 거래를 체결하는 서비스를 따로 분리해야 할 필요를 느꼈고

프로젝트 아키텍처는 주문접수엔티티와 주문체결엔티티가 분리된 구조를 가지게 되었습니다.

데이터베이스 트랜잭션은 이렇게 구성했다.

유저의 자산을 다루는 시스템이기에 모든 트랜잭션에서 Exclusive Lock을 적용한 상태에서 동작시킨 시스템은

많은 대드락으로 인해 정상적으로 동작하지 못했습니다.

쿼리 실행결과 분석을 통해 데드락이 발생하는 지점을 파악했습니다.

user 테이블과 user_stock 테이블에서 데드락이 발생하는 것을 파악하고

서비스에서 사용되는 트랜잭션의 순서를 user 테이블을 통해 제어할 수 있도록 수정했습니다.

단, 트랜잭션의 주체가 유저가 아닌 주문체결 프로세스는 order테이블을 먼저 조회한 뒤 user 테이블에 접근할 수 있었는데

order테이블은 INSERT, SELECT, UPDATE, DELETE 모두 자주 수행되는 테이블이었기 때문에 인덱스를 통한 row lock도 불가능했고

lock이 가능하더라도 유저의 주문취소가 동시에 동작할 경우 user테이블과 order테이블의 데드락이 발생할 여지가 있었습니다.

이런 상황에서 order테이블은 낙관적 잠금기법을 사용해서 동시성을 제어하도록 했습니다.

낙관적 잠금은 데이터에 잠금을 걸지 않고 애플리케이션 레벨에서 수정시점에 참조한 데이터가 변경되었는지 확인하는 기법을 말합니다.

이를 통해서 모든 트랜잭션이 user테이블의 row lock에 의해 순서가 제어될 수 있었습니다.

사용자 인증방식에 대한 고민

우리의 프로젝트가 MSA를 지향한 아키텍처를 갖추어갈 때 사용자 인증과정에서 가장 먼저 떠오르는 것이 JWT였습니다.

JWT를 이용한다면 서버가 별도의 상태를 가질 필요가 없고 MSA로 구성된 여러 서비스가 하나의 세션DB를 조회해야 하는 역설적인 상황을 피할 수 있기 때문입니다.

다만, JWT를 사용하는 것이 과연 옳은가에 대한 고민을 계속 하게되었습니다.

서버에서 접속한 사용자에 대한 상태가 없다면 포기해야 되는 기능들이 많이 존재합니다.

  1. 모든 디바이스에서 로그아웃
  2. 동시 접속 제한하기
  3. 한 사용자에게서 발생하는 비정상적인 트래픽 감지하기

1번과 3번은 우리의 서비스에 반드시 필요한 기능이라고 여겨졌습니다.

그렇기에 사용자 인증방식은 JWT가 아닌 세션 방식을 취하도록 했습니다.

이후의 과제

여기까지가 현재 boostock의 서비스 개발입니다.

하나의 거래가 체결되는 것에 약 50ms의 시간이 소요됩니다.

파악한 정보에 의하면 금융 거래 시스템은 수 마이크로초 안에 거래를 체결할 수 있도록 많은 노력을 기한다고 합니다.

50ms의 시간을 1/10000으로 감축시키기 위해서는 가장 먼저 disk IO에서 걸리는 시간을 줄여야 할 것 같습니다.

프로젝트의 초기 기획에서 인메모리DB가 아닌 MySQL을 채택한 이유는 두가지 입니다.

  1. 인메모리DB가 ACID원칙을 준수할 수 있는지 확신이 필요했다.
  2. 필요한 자원에 대한 예측이 어려웠고 가용 가능한 자원이 부족해지는 상황을 우려했다.

위 두가지 이유를 설득할 수 있는 DB 시스템을 파악하고 구성하는 시도가 당면한 첫번째 과제로 여겨집니다.

추가로

시스템의 이상 유무를 확인하기 위해서 통합로그 시스템이 필요해보입니다.

또, 아키텍처를 구성하고 있는 엔티티간 메세지큐를 적용해서 더 안정적인 서비스가 되도록 하는 것도 필요합니다.

우리의 API 서버는 몇명의 동시접속자까지 수용이 가능할 지 파악한 뒤

동시접속자가 많아지는 때에도 API 서버를 수평확장하면서 메세지큐의 도움을 받을 수 있을 것 같습니다.

Clone this wiki locally