Computer >> 컴퓨터 >  >> 프로그래밍 >> Redis

Redis Enterprise로 실시간 주식 거래 플랫폼 구축하기

포트폴리오는 자산운용·자산관리 산업의 근간을 이루는 개념입니다. 해리 마코위츠(Harry Markowitz)가 현대 포트폴리오 이론을 개척한 이래로 자산운용 전문가들은 주어진 위험 수준에서 포트폴리오 수익률을 극대화하는 데 몰두해 왔습니다. 오늘날 이러한 전문가들에 더해 수백만 명의 개인 투자자가 시장에 합류하면서 투자 지형은 완전히 바뀌었습니다. 이 새로운 참여자들은 증권사, 거래소, 청산기관의 거래 인프라를 떠받치는 기술 전반에 커다란 변화를 요구하고 있습니다.

대표적인 사례가 2021년 1월의 게임스톱(GameStop) 주식 열풍입니다. 개인 투자자들은 기록적인 물량으로 게임스톱 주식을 거래했고, AMC 엔터테인먼트 등 다른 '밈스톱(meme stock)'에도 대거 유입되면서 VIX 기준 시장 전체 변동성이 단 며칠 만에 76% 이상 급등했습니다. 이 변동성으로 수천 종목의 가격이 출렁였고, 수백만 명의 투자자가 동시에 자신의 포트폴리오에 접속하려 했지만 앱들은 폭주하는 수요를 감당하지 못했습니다. 투자자들은 정말 필요한 순간에 제 성능을 내지 못하는 앱의 운영사에게 결코 관대하지 않습니다.

Redis Enterprise로 실시간 주식 거래 플랫폼 구축하기

이런 혼란스러운 순간에 투자자들이 언제든 확인하려는 포트폴리오 정보는 두 가지입니다.

  1. 그 시점의 포트폴리오 총 평가액은 얼마인가?
  2. 보유 중인 특정 종목의 손익은 얼마인가?

이 질문에 대한 답은 투자자가 해당 종목을 매수할지, 매도할지, 보유할지 판단하는 근거가 됩니다. 빠르게 움직이는 오늘날 시장에서 사소한 지연조차 기회와 수익의 상실로 이어질 수 있습니다. 따라서 이 질문에 답하려면 가격 정보에 실시간으로 접근할 수 있어야 하는데, 여기에는 두 가지 큰 과제가 있습니다.

  • 수천 개 종목의 가격을 동시에 갱신하는 것
  • 수백만 건의 고객 요청을 한꺼번에 처리하는 것

종목 가격은 거래량, 특정 종목의 변동성, 시장 전체 변동성에 따라 순식간에 변합니다. 한편 증권사에는 수백만 명의 고객이 있고, 각 고객은 수십 개 종목을 포트폴리오에 담고 있습니다. 고객이 로그인하는 순간 최신 가격으로 포트폴리오가 갱신되어야 하며, 거래소에서 가격을 수신하는 동안에도 계속 업데이트를 유지해야 합니다.

본질적으로 이것은 실시간 주식 차트를 만드는 일입니다. 많은 증권사 앱은 이를 대규모로 구현하려 하지 않고, 수백만 클라이언트에 가격을 밀어주는(push) 대신 클라이언트가 최신 가격을 당겨오는(pull) 방식을 씁니다. 포트폴리오 화면에 새로고침 버튼이 있는 경우가 그 예입니다.

이런 차세대 과제는 결코 만만치 않으며, 초당 수백만 건의 연산을 처리하도록 설계되지 않은 디스크 기반 데이터베이스로는 쉽게 해결할 수 없습니다. 금융 산업의 요구사항은 쉽게 확장되면서 초당 수억 건의 연산을 처리할 수 있는 데이터베이스를 필요로 합니다. 이때 인메모리 데이터베이스 플랫폼인 Redis Enterprise가 이러한 복합적 과제들을 해결할 잠재력을 갖추고 있습니다.

Redis Enterprise로 실시간 주식 거래 플랫폼 구축하기

이 글은 금융 분야의 다양한 실시간 사용 사례를 다루는 시리즈의 첫 번째 글입니다. 각 사용 사례의 세부 내용과 비즈니스 과제, 그리고 Redis Enterprise가 이를 해결하는 데 어떤 역할을 할 수 있는지 살펴봅니다. 아울러 샘플 설계, 데이터 모델, 코드 예제를 제공하고 각 접근 방식의 장단점도 논의합니다.

이번 포스트에서 다룰 내용은 다음과 같습니다.

  • Redis Enterprise 기반 고성능·확장 가능한 증권 포트폴리오 데이터 모델의 샘플 구현
  • 증권사가 거래소에서 최신 가격을 수신하는 대로 포트폴리오 내 종목 가격을 실시간으로 갱신

클라이언트 앱이 포트폴리오를 조회하고 최신 가격 스트림을 받기 시작하면 다음 작업을 수행할 수 있습니다.

  • 포트폴리오 총 평가액 계산
  • 개별 보유 종목별 손익 계산

증권 포트폴리오 데이터 모델

먼저 포트폴리오 내 하나의 보유 물량(lot)을 모델링해 보겠습니다. 아래 예시에서 CVS 헬스(CVS Health Corp., NYSE: CVS)가 예시 보유 종목 중 하나입니다. CVS는 두 개의 별도 물량으로 보유되어 있으며, 첫 번째는 2021년 1월 4일, 두 번째는 2021년 3월 1일에 매수했습니다. 두 매수 거래 모두 같은 수량, 즉 각각 10주씩 매수했지만 주당 매수 가격은 달랐습니다. 첫 번째 물량은 $68.3378, 두 번째는 $68.82입니다. 따라서 포트폴리오 내 CVS 보유 총 수량은 20주이며 평균 매입 단가는 (($68.3378 × 10) + ($68.82 × 10)) / 20 = $68.5789로 계산됩니다.

Redis Enterprise로 실시간 주식 거래 플랫폼 구축하기

요구사항 구현

Redis Enterprise로 실시간 주식 거래 플랫폼 구축하기

Redis의 데이터 표현 방식은 평탄(flat)합니다. 예컨대 하나의 Set 안에 또 다른 Set을 내장할 수 없습니다. 따라서 ER 다이어그램으로 기술된 데이터 모델을 반드시 그대로 직접 구현할 수 있는 것은 아니며, 직접 구현한다고 해도 원하는 성능 특성을 얻지 못할 수 있습니다. 그래서 실제 구현 단계에서는 조금 다른 방식으로 사고해야 합니다. 이 절에서는 Redis로 고성능·확장 가능한 구현을 설계할 때 필요한 기본 설계 원칙을 다룹니다.

이 데이터 모델에는 다음 엔티티들이 등장합니다.

Redis Enterprise로 실시간 주식 거래 플랫폼 구축하기

ER 다이어그램은 시각적 표현을 제공하여 전체 구조를 파악하는 데 도움이 됩니다.

다만 위 다이어그램에는 유입되는 가격 스트림(종목별 가격 이력에는 기록됨)과 가격 변동에 따른 순간 평가액 및 손익 계산이 드러나 있지 않습니다. 즉 ER 다이어그램은 포트폴리오 평가가 수행되는 맥락에 해당하는 비교적 정적인 데이터를 나타냅니다.

전체 아키텍처

이 시스템 설계 시 고려해야 할 핵심 사항은 다음과 같습니다.

  • 타임리니스(즉, 지연 시간)가 핵심입니다. 이것이 시스템 전체를 관통하는 설계 목표입니다.
  • 포트폴리오 평가액 계산은 계정 고유 데이터(물량 정보)와 계정 간 공유 데이터(종목 가격)를 결합합니다. 계정 고유 데이터는 공유 데이터가 활용되는 '맥락'입니다.
  • 포트폴리오 평가액은 온라인 상태인 투자자의 계정(전체 계정 중 일부)에 대해서만 계산하면 됩니다.
  • 데이터는 가격이 생성되는 거래소에서 시작해, 온라인 투자자에게 포트폴리오 가치를 보여주는 클라이언트(브라우저, 모바일 앱)까지 흐릅니다.
  • 특히 동적으로 변하는 영역은 입력 데이터 유입 속도와 동시 접속 투자자 수입니다.

이러한 사항을 바탕으로 도출할 수 있는 일반적인 접근 방식은 다음과 같습니다.

  • Redis의 인메모리 아키텍처를 활용해 정적·동적 데이터 모두에 낮은 지연 시간으로 접근
  • Redis의 데이터 구조를 활용한 데이터 모델링 최적화로, 느리게 변하는 맥락 데이터에 대한 신속한 접근 확보
  • Redis의 통신 구조(Streams, Consumer Group, Pub/Sub)를 활용해 동적 데이터 요구사항 처리
  • 전체 시스템 성능에 영향을 주지 않는 선에서 저장 데이터를 필요한 최소한으로 축소
  • 클라이언트별 계산은 클라이언트 자체에서 수행 — 온라인 투자자 수에 따라 자연스럽고 자동으로 확장되어 시스템 부하를 크게 덜어줌

아래는 주요 연산 구성 요소와 데이터 흐름입니다.

Redis Enterprise로 실시간 주식 거래 플랫폼 구축하기

Redis Enterprise는 온프레미스, Kubernetes, 하이브리드 클라우드, 관리형 서비스 또는 퍼스트파티 클라우드 서비스 등 어떤 형태로든 여러 머신에 걸친 하나 이상의 노드로 구성되며, 수십만 명의 투자자가 각자 선택한 클라이언트로 동시 접속하게 됩니다.

Redis Enterprise 구성 요소

종목 가격 업데이트는 Redis Streams로 흡수됩니다. 이 스트림에는 여러 종목의 업데이트가 뒤섞여 들어오므로, 데이터를 활용하려면 분해(disaggregation)가 필요합니다. 컨슈머 그룹(consumer group)이 이 분해 작업을 수행하며, 종목별로 두 가지 구조로 데이터를 처리합니다.

  • RedisTimeSeries 데이터베이스: 가격 변동 이력을 추적(방금 접속한 클라이언트에 제공할 최신 가격 기록 포함)
  • Pub/Sub 브로커 채널: 해당 채널을 구독 중인 클라이언트(즉, 해당 종목을 보유한 투자자)에게 가격 변동 알림을 푸시

다음 다이어그램은 아키텍처의 이 부분을 자세히 보여줍니다.

Redis Enterprise로 실시간 주식 거래 플랫폼 구축하기

우리 모델에서 가장 중요한 요소는 물량(lot)과 관련 종목을 나타내는 계정 고유 데이터입니다. Redis에서 데이터를 모델링하는 사고방식을 성능 중심으로 설명하기 위해 두 가지 구현 방식을 비교해 보겠습니다. 물론 다른 구현도 가능하며, 여기서의 목표는 Redis에 데이터를 구현할 때의 전반적인 설계 원칙과 사고 과정을 소개하는 것입니다.

구체적인 예시로 다음 정보를 사용합니다.

Redis Enterprise로 실시간 주식 거래 플랫폼 구축하기

부동소수점을 피하고 모든 값을 정수로 유지하기 위해 가장 작은 통화 단위로 가격을 표현합니다. 달러와 센트 간의 변환은 클라이언트가 처리하도록 할 수 있습니다. 이 예제에서는 소수점 둘째 자리까지의 정밀도를 가진 가격을 사용합니다.

데이터 모델 A

첫 번째 구현은 SET을 사용해 계정 ID로 식별되는 키에 계정의 모든 물량 ID를 기록하고, 물량 ID로 식별되는 Redis HASH 하나를 LOT마다 두어 티커(ticker), 수량(quantity), 매수 가격(price)을 필드로 저장합니다. 다시 말해 HASH로 LOT 엔티티 구조를 모델링하며, LOT 엔티티의 각 속성이 Redis HASH의 필드가 됩니다.

이 데이터 모델에서는 계정마다 키가 하나 있고, 그 값은 해당 계정의 모든 물량 ID를 Redis SET으로 담습니다.

lotids:<ACCOUNT_ID> SET <LOTID>

또한 각 lotid마다 티커, 수량, 매수 가격을 필드로 갖는 HASH가 존재합니다.

lot:<LOTID> HASH <ticker TICKER> <quantity INTEGER> <price INTEGER>

실제로는 다음과 같이 키를 생성합니다.

127.0.0.1:6379> SADD lotids:ACC-1001 LOT-9001 LOT-9002
(integer) 2
127.0.0.1:6379> HMSET lot:LOT-9001 ticker AAPL quantity 200 price 12556
OK
127.0.0.1:6379> HMSET lot:LOT-9002 ticker CAT quantity 1200 price 18063
OK

RedisTimeSeries 모듈은 시간-값 쌍의 저장·조회를 지원하며, 대량 삽입과 저지연 읽기를 제공합니다. 클라이언트가 관심 있는 티커의 최신 가격은 대응되는 타임시리즈 키로 가져옵니다.

price_history:<TICKER> TIMESERIES <price INTEGER>

127.0.0.1:6379> TS.GET price_history:APPL
1) (integer) 1619456853061
2) 12572
127.0.0.1:6379> TS.GET price_history:CAT
1) (integer) 1619456854120
2) 18021

그리고 업데이트를 받기 위해 가격 채널을 구독합니다.

<TICKER> SUBSCRIPTION_CHANNEL

클라이언트가 모든 데이터를 얻으려면 다음 연산을 수행합니다.

  1. lotids 키에 대해 SMEMBERS 1회 — 시간 복잡도 O(N), N은 물량 수
  2. lot 키들에 대해 HGETALL N회 — 시간 복잡도 N × O(1)
  3. price_history 키들에 대해 TS.GET T회 — 시간 복잡도 T × O(1), T는 티커 수
  4. <TICKER> 채널에 대해 SUBSCRIBE 1회 — 시간 복잡도 O(T) (SUBSCRIBE 호출 한 번으로 모든 채널 구독 가능)

전체 시간 복잡도는 O(N + T)입니다.

실제로 연산 1과 2는 다음과 같이 실행됩니다.

127.0.0.1:6379> SMEMBERS lotids:ACC1001
1) "LOT-9001"
2) "LOT-9002"
127.0.0.1:6379> HGETALL lot:LOT-9001
1) "ticker"
2) "AAPL"
3) "quantity"
4) "200"
5) "price"
6) "12556"
127.0.0.1:6379> HGETALL lot:LOT-9002
1) "ticker"
2) "CAT"
3) "quantity"
4) "1200"
5) "price"
6) "18063"

파이프라이닝(클라이언트 측 배칭의 일종)이나 LUA 스크립트 반복 사용(SCRIPT LOAD & EVALSHA)을 활용하면 네트워크 지연을 최소화할 수 있습니다. 참고로 트랜잭션도 파이프라인으로 구현해 네트워크 지연을 줄일 수 있지만, 이는 클라이언트 의존적이며 목적 자체가 서버 측 원자성(atomicity) 보장이므로 네트워크 지연 문제를 실질적으로 해결하지는 못합니다. 파이프라인에 포함되는 명령들의 입력과 출력은 서로 독립적이어야 합니다. LUA 스크립트는 모든 키를 사전에 지정해야 하고, 모든 키가 동일한 슬롯으로 해싱되어야 합니다(자세한 내용은 Redis Enterprise 문서 참조).

이러한 제약을 고려하면 연산을 파이프라인에 배분하는 방식은 다음과 같습니다.

  • 파이프라인 1: 연산 #1의 단일 명령
  • 파이프라인 2: 연산 #2의 전체 N개 명령
  • 파이프라인 3: 연산 #3, #4의 전체 명령

그리고 LUA 스크립트는 사용할 수 없습니다. 각 연산이 서로 다른 키를 사용하며, 그 키들 사이에 동일 슬롯으로 해싱될 공통 부분이 없기 때문입니다.

이 모델을 활용하면 시간 복잡도는 O(N+T)이고 네트워크 왕복은 세 번 발생합니다.

데이터 모델 B

대안이 되는 모델은 LOT 엔티티 구조를 평탄화하고, 각 엔티티 속성을 계정 ID로 식별되는 키로 표현하는 것입니다. 물량의 각 속성(수량, 티커, 가격)마다 키가 하나씩 존재하며, 각 HASH의 필드는 LOT ID가 되고 값은 해당 속성 값이 됩니다. 즉 다음과 같은 키들을 갖습니다.

tickers_by_lot: <ACCOUNT_ID> HASH <LOTID TICKER>

quantities_by_lot:<ACCOUNT_ID> HASH <LOTID INTEGER>

prices_by_lot:<ACCOUNT_ID> HASH <LOTID INTEGER>

이 해시들은 데이터 모델 A의 LOTID·LOT 키를 대체하며, price_history<TICKER> 키는 그대로 유지됩니다.

키 생성은 다음과 같습니다.

HSET tickers_by_lot:ACC-1001 LOT-9001 AAPL LOT-9002 CAT
HSET quantities_by_lot:ACC-1001 LOT-9001 200 LOT-9002 1200
HSET prices_by_lot:ACC-1001 LOT-9001 125.56 LOT-9002 180.63

값 조회는 다음과 같습니다.

127.0.0.1:6379> HGETALL tickers_by_lot:ACC-1001
1) "LOT-9001"
2) "AAPL"
3) "LOT-9002"
4) "CAT"
127.0.0.1:6379> HGETALL quantities_by_lot:ACC-1001
1) "LOT-9001"
2) "200"
3) "LOT-9002"
4) "1200"
127.0.0.1:6379> HGETALL prices_by_lot:ACC-1001
1) "LOT-9001"
2) "12556"
3) "LOT-9002"
4) "18063"

클라이언트에 필요한 연산은 이제 다음과 같습니다.

  1. lot_quantity 키에 대해 HGETALL 1회 — 시간 복잡도 N × O(1)
  2. lot_ticker 키에 대해 HGETALL 1회 — 시간 복잡도 N × O(1)
  3. lot_price 키에 대해 HGETALL 1회 — 시간 복잡도 N × O(1)
  4. price_history 키들에 대해 TS.GET T회 — 시간 복잡도 T × O(1), T는 티커 수
  5. <TICKER> 채널에 대해 SUBSCRIBE 1회 — 시간 복잡도 1 × O(T)

전체 시간 복잡도는 이전과 동일한 O(N+T)입니다.

파이프라인 관점에서는 다음과 같이 됩니다.

  • 파이프라인 1: 연산 #1, #2, #3의 전체 명령
  • 파이프라인 2: 연산 #4, #5의 전체 T개 명령

네트워크 왕복 횟수가 한 번 줄어들었습니다. 절대적으로는 크지 않지만 상대적으로는 33% 개선입니다.

또한 키를 미리 알 수 있고 특정 계정의 모든 키를 동일 슬롯에 매핑할 수 있으므로 LUA 스크립트도 손쉽게 활용할 수 있습니다. 연산이 단순하므로 여기서 LUA를 더 깊이 다루지는 않겠지만, 이 설계가 LUA 적용을 가능하게 만든다는 점은 주목할 만합니다.

간단한 벤치마크에서 데이터 모델 B가 4.13ms 더 빨랐습니다(수천 회 실행 기준). 다만 이 연산은 계정당 클라이언트 초기화 시 한 번만 수행되므로 전체 성능에는 큰 영향을 주지 않을 가능성이 높습니다.

마무리

이 글에서는 Redis 데이터 타입을 활용해 엔티티 모델을 구현하는 두 가지 방식을 보여드렸습니다. 아울러 Redis 데이터 타입을 선택할 때마다 수행해야 할 시간 복잡도 분석과 함께, 대규모·고성능이 요구되는 환경에서 필수적인 네트워크 성능 개선 방안도 소개했습니다. 후속 글에서는 데이터 모델을 확장하며 이러한 개념을 더 깊이 다룰 예정입니다.

이번 글에서는 대규모 증권 포트폴리오 관리의 비즈니스 과제 일부를 소개하고 다음 내용을 보여드렸습니다.

  • 실시간·확장 가능한 증권 포트폴리오를 구현하기 위한 Redis 데이터 모델
  • 포트폴리오 총 평가액과 개별 보유 종목 손익을 계산하는 데 활용할 수 있는 고성능 실시간 가격 업데이트 시스템

이 두 가지 핵심 기능을 갖추면 증권사 앱 클라이언트는 수백만 계정을 처리할 수준의 성능과 확장성으로 실시간 포트폴리오 업데이트를 제공할 수 있습니다. 이 설계는 포트폴리오 총 평가액과 각 보유 종목의 손익을 실시간으로 표시할 수 있습니다. 또한 이 데이터 모델과 아키텍처는 증권을 넘어 암호화폐, 광고 거래소(ad exchange) 등 다양한 사용 사례에도 적용할 수 있습니다.