LabHub

블로그

증권사 MTS/HTS 개발자: 거래소 프로토콜부터 마이크로초 latency까지

한국어English日本語

글 대상

증권사 IT, MTS(Mobile Trading System), HTS(Home Trading System), OMS(Order Management System), 거래소 게이트웨이 개발 직무에 진입하려는 신입/주니어 그리고 fintech/거래소 인접 도메인에서 증권사로 옮기려는 5-10년차 개발자를 대상으로 한다. JD에 적힌 "대용량 트래픽 처리 경험" "실시간 시세 처리"라는 추상적 표현이 실제로는 어떤 프로토콜, 어떤 latency 숫자, 어떤 장애 패턴을 의미하는지 풀어서 설명한다. 회사 이름과 직무명을 아는 것에서 멈추지 말고, 거래소 spec 한 페이지를 펼쳤을 때 어떤 단어를 검색해야 하는지 감각을 기르자.

왜 MTS/HTS 개발이 "다루는 일"인가

증권사 IT 개발자는 단순히 화면을 만드는 사람이 아니다. 실제로 다루는 대상은 세 층이다.

첫째, 시장이다. 매일 09:00 KRX 정규장이 열리는 순간 수만 건의 주문이 동시에 쏟아진다. 09:00:00.000부터 09:00:00.500까지 500ms 사이에 OMS는 호가 잔량, 가격 우선순위, 동시호가 매칭 로직을 거쳐 거래소로 주문을 전송해야 한다.

둘째, 프로토콜이다. KRX EXTURE+ 시스템은 자체 binary 프로토콜인 KRX UMS를 쓰고, 글로벌 시장은 FIX 4.4/5.0이 표준이다. Nasdaq은 ITCH(시세)/OUCH(주문), TSE는 Arrowhead의 자체 binary frame을 쓴다. 이 모든 프로토콜의 message frame layout, sequence number 처리, gap fill 로직을 다룬다.

셋째, 시스템이다. Front(MTS/HTS 앱) → API Gateway → Order Router → OMS → Exchange Gateway까지 6-7개 hop을 지나는 동안 latency를 50ms 이내로 유지해야 retail 고객 체감 속도가 나온다. HFT라면 tick-to-trade <100μs까지 내려가야 한다.

"다룬다"는 말은 곧 이 세 층 사이의 trade-off를 매일 결정한다는 뜻이다.

시스템 architecture 개요

전형적인 한국 증권사 트레이딩 시스템 architecture는 5-tier 구조다.

  1. Client tier: MTS(iOS/Android, React Native or 네이티브), HTS(Windows C++/.NET native), Web Trading(React/Vue)
  2. Gateway tier: API Gateway(Nginx/Envoy + 자체 인증), WebSocket Gateway(시세 push), MQTT broker(모바일 push)
  3. Middleware tier: Order Router, Risk Engine, Position Manager, Account Service - Java Spring Boot 또는 C++ low-latency
  4. Backend tier: OMS(Order Management System), EMS(Execution Management System), 시세 분배 엔진(Market Data Distribution), 청산/정산 시스템(Settlement)
  5. Exchange Gateway tier: KRX 게이트웨이, KOSCOM 백본, ATS 연결, 해외 거래소 FIX 게이트웨이

각 tier 사이에는 Kafka, Solace, Tibco RV, Aeron 같은 메시징 미들웨어가 들어간다. 한국 증권사는 Kafka + Redis 조합이 많고, 글로벌 IB는 Solace 또는 Tibco를 쓴다.

핵심 프로토콜: FIX 4.4/5.0, FAST, ITCH/OUCH, Arrowhead, KRX UMS, ISO 20022

증권 IT 면접에서 "FIX 프로토콜 다뤄봤어요?"는 진심이다. 표준 프로토콜을 정리하면 이렇다.

FIX는 사람이 읽을 수 있다는 게 강점이고 디버깅이 쉽다. ITCH/OUCH는 binary라 wire format 명세를 들고 와이어샤크 dump를 까봐야 한다.

실제 하루

증권사 IT 운영팀의 하루는 시장 시간에 맞춰 흐른다.

장 중에는 release를 절대 안 한다. 변경 동결(Change Freeze) 정책이 엄격하다.

Smart Order Router 구조

Smart Order Router(SOR)는 하나의 주문을 여러 시장으로 쪼개 보내는 엔진이다. 한국은 KRX 단독 시장이라 SOR 필요성이 낮았지만, 2025년 ATS(대체거래소) 도입 이후 본격화됐다.

SOR가 판단하는 기준:

SOR는 보통 C++ 또는 Java Aeron 기반으로 만든다. Decision logic은 마이크로초 단위로 동작하므로 GC pause를 허용하지 않는다.

한국 거래소 연결: KRX KOSPI/KOSDAQ, ATS, 대체매매시설

한국 시장은 KRX(한국거래소)가 KOSPI, KOSDAQ, KONEX를 운영하는 단일 거래소 체제였다. 2025년 넥스트레이드(Nextrade) ATS가 출범하면서 변화가 시작됐다.

증권사 게이트웨이는 KOSCOM 회선을 통해 KRX에 접속한다. 회선은 보통 이중화하고, KOSCOM 부산 백업센터까지 회선이 빠져 있다.

일본 거래소: TSE Arrowhead, JPX, PTS

일본 시장은 도쿄증권거래소(TSE, Tokyo Stock Exchange)와 오사카증권거래소(OSE)를 통합한 JPX(Japan Exchange Group) 산하 체제다.

일본 증권사는 TSE FLEX 프로토콜(자체 binary)을 다루고, 글로벌 IB는 FIX 4.4 + FIX FAST 조합을 쓴다.

Latency 요구사항: tick-to-trade <100μs HFT vs <10ms retail

증권 IT의 latency는 서비스 segment마다 다르다.

HFT는 GC pause(Java) 5ms도 용납 안 한다. Zing JVM, Azul Platform Prime 같은 pauseless GC 또는 C++/Rust로 작성한다.

Tech stack 한국: Java/Spring Boot, Kafka, Redis, Cassandra, kdb+, C++

한국 증권사 IT의 tech stack은 다음 조합이 흔하다.

미래에셋, 한투, 삼성, KB는 Java/Spring 중심. C++ low-latency 코어는 일부 OMS/EMS에서만 사용한다.

Tech stack 일본/글로벌: C++/Java, Aeron, Chronicle, Solace, Tibco RV

글로벌 IB(Goldman, JPM, MS, Citi)와 일본 대형 증권사(Nomura, Daiwa, SBI)의 tech stack은 한국과 차이가 있다.

일본 대형 증권사도 글로벌 IB와 유사한 stack을 쓴다. 다만 사내 표준이 보수적이라 Java가 주력이고, C++는 일부 트레이딩 데스크에서만 사용한다.

보안: 공인인증서/금융인증서, PKI, 생체인증, OTP, 2FA, FIDO2, 망분리, ISMS-P, FISC

증권 IT 보안은 사용자 인증과 망 보안 두 축이다.

망분리 때문에 한국 증권사 개발자는 GitHub.com 직접 접근이 어렵다. 내부 GitLab/Bitbucket을 쓰고, 외부 OSS 다운로드는 nexus 같은 프록시를 거친다.

한국 증권사 IT: 미래에셋, 한투, 삼성, KB, 신한투자

한국 증권사 IT 조직은 in-house vs SI 외주 모델로 나뉜다.

신입 채용은 미래에셋, 한투, 삼성, KB가 매년 두 자릿수 규모로 뽑는다. 외주 SI는 LG CNS, 삼성SDS, 코스콤, 한투ICT, 신한DS가 주력.

일본 증권사 IT: SBI, Rakuten, Matsui, Nomura, Daiwa + 토스증권 STARTRADE, 카카오페이증권 KakaoStock

일본 증권사 IT는 노포(老舗)와 신흥의 이원화 구조다.

한국 신흥은 토스증권(STARTRADE 시스템), 카카오페이증권(KakaoStock 앱)이 대표적이다. 토스증권은 자체 개발 OMS를 운영하고, 카카오페이증권은 한투ICT 백엔드 위에 자체 프론트를 올렸다.

AI/ML 적용: 부정거래 탐지, KYC OCR, 챗봇, 추천 종목

증권사 AI/ML 활용 영역은 크게 5가지다.

  1. 부정거래 탐지(FDS, Fraud Detection System): 차명계좌, 자전거래, 시세조종, pump-and-dump 탐지. Graph Neural Network, anomaly detection
  2. KYC(Know Your Customer): 신분증 OCR, 안면인식, 라이브니스 체크. AWS Textract, GCP Vision API
  3. 챗봇: 잔고 조회, 주문 내역, 시세 안내. LLM 기반(GPT-4o, Claude, HyperCLOVA X)
  4. 추천 종목: 고객 포트폴리오 분석, 유사 고객 매수 종목 추천. Collaborative Filtering, Matrix Factorization
  5. 알고리즘 트레이딩 신호: 시세 패턴, 거래량, 펀더멘털 결합. XGBoost, LSTM, Transformer

AI 영역은 별도 팀으로 분리된 곳이 많다(미래에셋 디지털혁신본부 산하 AI Lab, 한투 AI 추진 TF). 트레이딩 시스템 개발자가 직접 모델을 만들기보다는 AI 팀 결과물을 시스템에 통합하는 일이 더 많다.

실패 사례: 삼성증권 2018 우리사주 사고, Knight Capital 2012, KOSCOM 2008

증권 IT의 실패 사례는 시스템 개발자가 반드시 외워둬야 한다.

이 사례들의 공통점은 입력 검증, 한도 체크, 배포 절차, 백업 절차의 어느 한 곳이 무너졌다는 것이다.

인터뷰 질문 20개

증권 IT 면접에서 자주 나오는 질문 20개를 영역별로 정리했다.

  1. FIX 4.4의 MsgType=D(NewOrderSingle)와 MsgType=8(ExecutionReport) 차이는?
  2. OMS와 EMS의 책임 분리를 설명하라
  3. Sequence number gap이 발생했을 때 복구 절차는?
  4. 호가 잔량(Order Book)을 메모리에 어떻게 표현할 것인가? 자료구조와 시간복잡도
  5. 주문 중복 방지를 위한 idempotency key 설계
  6. Java GC pause <5ms을 보장하려면 어떤 GC와 설정을 쓸 것인가?
  7. Kafka 파티셔닝 키를 종목 코드로 했을 때 hot partition 문제 해결 방법
  8. Redis로 호가 캐시 만들 때 메모리 사용량 추정과 eviction 정책
  9. Low-latency 환경에서 lock-free 큐 구현 방법(SPSC, MPMC)
  10. 망분리 환경에서 외부 OSS 라이브러리는 어떻게 도입하는가?
  11. 장 시작 09:00:00 동시호가 매칭의 가격 우선/시간 우선 원칙
  12. 신용거래 융자/대주 차이와 IT 시스템에서 추적해야 할 데이터
  13. 미수금 발생 시 반대매매 알고리즘
  14. 미국 주식 24시간 거래(pre-market, after-market) 환율 처리
  15. WebSocket으로 시세 push할 때 backpressure 처리 방법
  16. 권리락 처리: 액면분할, 무상증자, 배당 발생 시 잔고 자동 조정
  17. 시장 가격(Market Order) vs 지정가(Limit Order) 주문의 거래소 message 차이
  18. 옵션 만기일 주가지수 SQ(Special Quotation) 산출 방식
  19. ETF 시장조성자(LP) 호가 의무 시스템 구현
  20. 알고리즘 트레이딩의 TWAP, VWAP, IS, POV 전략 차이

면접관은 답이 막혔을 때 어떻게 사고를 전개하는지를 본다. 모른다고 끝내지 말고 "이 부분은 잘 모르지만 X라는 원칙으로 추론하면..." 식으로 풀자.

연봉

대략적인 연봉 수준(2025년 기준, 베이스 + 인센티브)이다.

HFT/알고 트레이딩 quant dev는 별도 시장이다. 미국 Citadel/Two Sigma/Jane Street 신입 패키지는 $400K+, 시니어는 $1M+도 나온다.

커리어 패스: Junior Dev→Senior→Tech Lead→Architect→CTO

전형적인 증권사 IT 커리어 패스다.

수평 이동도 흔하다. 증권사→fintech(토스, 카카오페이), 증권사→거래소(KRX/KOSCOM), 증권사→글로벌 IB(GS, JPM, MS 한국지점)로 이직.

References

다음은 본문에 인용된 자료들이다. 실제 학습은 이 URL들을 책갈피해 두고 매주 새 발표/스펙을 따라가는 식이 좋다.

# FIX 4.4 ExecutionReport handler 예제
# 실제 production에서는 QuickFIX/J 또는 onixs 같은 검증된 라이브러리 사용

import quickfix as fix
import quickfix44 as fix44

class TradingApp(fix.Application):
    def onMessage(self, message, sessionID):
        msg_type = fix.MsgType()
        message.getHeader().getField(msg_type)

        if msg_type.getValue() == fix.MsgType_ExecutionReport:
            self.handle_execution_report(message, sessionID)

    def handle_execution_report(self, message, sessionID):
        exec_type = fix.ExecType()
        ord_status = fix.OrdStatus()
        order_id = fix.OrderID()
        cl_ord_id = fix.ClOrdID()
        symbol = fix.Symbol()
        side = fix.Side()
        last_qty = fix.LastQty()
        last_px = fix.LastPx()
        cum_qty = fix.CumQty()
        avg_px = fix.AvgPx()

        message.getField(exec_type)
        message.getField(ord_status)
        message.getField(order_id)
        message.getField(cl_ord_id)
        message.getField(symbol)
        message.getField(side)

        # ExecType=F(Trade) → 체결
        if exec_type.getValue() == fix.ExecType_TRADE:
            message.getField(last_qty)
            message.getField(last_px)
            message.getField(cum_qty)
            message.getField(avg_px)

            print(f"[FILL] Order={order_id.getValue()} "
                  f"ClOrdID={cl_ord_id.getValue()} "
                  f"Symbol={symbol.getValue()} "
                  f"Side={side.getValue()} "
                  f"LastQty={last_qty.getValue()} "
                  f"LastPx={last_px.getValue()} "
                  f"CumQty={cum_qty.getValue()} "
                  f"AvgPx={avg_px.getValue()}")

            # 체결 데이터를 OMS, Position Manager, Risk Engine으로 전파
            self.publish_to_kafka("fills.equity.kospi", {
                "order_id": order_id.getValue(),
                "symbol": symbol.getValue(),
                "qty": last_qty.getValue(),
                "px": last_px.getValue(),
            })

        # OrdStatus=8(Rejected) → 거래소 거부
        elif ord_status.getValue() == fix.OrdStatus_REJECTED:
            ord_rej_reason = fix.OrdRejReason()
            message.getField(ord_rej_reason)
            print(f"[REJECT] Order={order_id.getValue()} "
                  f"Reason={ord_rej_reason.getValue()}")
            self.alert_oms_reject(order_id.getValue(), ord_rej_reason.getValue())
# Smart Order Router 의사결정 로직 (간략화)
# 실제 SOR는 마이크로초 단위 latency 요구로 C++ 또는 Java Aeron 사용

from dataclasses import dataclass
from typing import List

@dataclass
class Venue:
    name: str          # KRX, Nextrade, NYSE 등
    best_bid: float
    best_ask: float
    bid_qty: int
    ask_qty: int
    latency_us: int    # 마이크로초
    fee_bps: float     # basis points

@dataclass
class Order:
    symbol: str
    side: str          # BUY or SELL
    qty: int
    limit_price: float

def route_order(order: Order, venues: List[Venue]) -> List[dict]:
    """
    주문을 여러 venue로 분할 라우팅
    우선순위: 가격 > 수수료 포함 net price > latency
    """
    routes = []
    remaining_qty = order.qty

    # BUY는 ask 기준, SELL은 bid 기준
    if order.side == "BUY":
        candidates = [v for v in venues if v.best_ask <= order.limit_price and v.ask_qty > 0]
        # net price = 호가 + 수수료
        candidates.sort(key=lambda v: (
            v.best_ask * (1 + v.fee_bps / 10000),
            v.latency_us,
        ))

        for venue in candidates:
            if remaining_qty <= 0:
                break
            fill_qty = min(remaining_qty, venue.ask_qty)
            routes.append({
                "venue": venue.name,
                "qty": fill_qty,
                "price": venue.best_ask,
            })
            remaining_qty -= fill_qty
    else:  # SELL
        candidates = [v for v in venues if v.best_bid >= order.limit_price and v.bid_qty > 0]
        candidates.sort(key=lambda v: (
            -v.best_bid * (1 - v.fee_bps / 10000),
            v.latency_us,
        ))
        for venue in candidates:
            if remaining_qty <= 0:
                break
            fill_qty = min(remaining_qty, venue.bid_qty)
            routes.append({
                "venue": venue.name,
                "qty": fill_qty,
                "price": venue.best_bid,
            })
            remaining_qty -= fill_qty

    if remaining_qty > 0:
        # 잔량은 limit order로 가장 유리한 venue에 대기
        routes.append({
            "venue": candidates[0].name if candidates else "KRX",
            "qty": remaining_qty,
            "price": order.limit_price,
            "type": "LIMIT_RESTING",
        })

    return routes
# KRX gateway 연결 설정 예제
# 실제 production에서는 비밀번호 vault, 인증서 PKCS#12로 분리

krx_gateway:
  environment: production
  member_code: "001"          # 증권사 회원번호
  participant_id: "PROD001"

  primary:
    host: ums-primary.koscom.co.kr
    port: 30443
    ssl: true
    cert_path: /opt/secrets/krx-prod.p12
  backup:
    host: ums-backup.koscom.co.kr
    port: 30443
    ssl: true
    cert_path: /opt/secrets/krx-prod.p12

  session:
    heartbeat_interval_sec: 30
    logon_timeout_sec: 10
    sequence_reset_on_logon: false
    max_messages_per_sec: 5000
    reconnect_attempts: 5
    reconnect_delay_sec: 2

  order_limits:
    max_order_qty: 1000000
    max_order_value_krw: 10000000000  # 100억원
    max_orders_per_second: 1000

  market:
    kospi_open: "09:00:00"
    kospi_close: "15:30:00"
    pre_market_open: "08:30:00"      # 동시호가
    after_market_close: "16:00:00"   # 시간외 단일가
    timezone: Asia/Seoul

  monitoring:
    metrics_endpoint: http://prometheus.internal:9090
    alert_channel: "#trading-prod-alerts"
    latency_alert_threshold_ms: 50
    rejected_orders_threshold_per_min: 100

댓글

아직 댓글이 없습니다.

로그인하면 댓글을 쓸 수 있습니다