HTS(홈트레이딩시스템)와 같은 초고속 금융 소프트웨어에서 장 시작 직후나 급격한 시세 변동이 일어나는 '장 폭발' 순간, 초당 수천 건 이상의 실시간 패킷이 쏟아져 들어옵니다. 이때 수신부에서 매 패킷마다 화면 UI에 PostMessage를 날리면 윈도우 메시지 큐가 포화 상태에 빠지며 화면이 프리징되거나 앱이 뻗어버리는 심각한 문제가 발생합니다.
이를 해결하기 위해 중앙에서 시세를 관리하는 DataManager를 두고, 통신 모듈(MngSB) 단에서 유입량에 따라 동적으로 쿨타임을 조절하는 가변 스로틀링(Adaptive Throttling)과 마지막 데이터 유실을 방지하는 Trailing Edge Flush 기법을 적용한 아키텍처 개선 과정을 정리하여 기록합니다.
1. 배경 및 문제 상황: 장 폭발 시의 UI 프리징
주식 시장이 개장하거나 대형 호재/악재가 터지는 이른바 '장 폭발' 상황에서는 초당 수많은 실시간 체결 및 호가 패킷이 클라이언트로 쏟아집니다.
기존 HTS 아키텍처에서는 통신 수신 모듈(MngSB)이 시세 패킷을 파싱하자마자 해당 화면(View/UI) 윈도우 핸들로 즉시 PostMessage를 전송하여 화면을 갱신하도록 되어 있었습니다.
하지만 초당 수천 건의 패킷마다 PostMessage를 호출하면 다음과 같은 치명적인 병목이 발생했습니다.
- 메시지 큐 오버플로우: 윈도우 메인 UI 스레드가 메시지를 다 소화하지 못해 메시지 큐가 가득 차게 됩니다.
- UI 프리징 및 크래시: 마우스 클릭, 메뉴 이동 등 사용자의 정상적인 인터랙션이 먹통이 되고 화면이 멈추거나 뻗어버립니다.
- 불필요한 과다 렌더링: 인간의 눈으로 초당 수천 번 깜빡이는 숫자를 인지할 수도 없는데 CPU/GPU 자원만 낭비됩니다.
이를 근본적으로 해결하기 위해 데이터를 중앙에서 모아서 관리하는 DataManager를 도입하고, 실시간 UI 갱신 부하를 제어하는 새로운 아키텍처 설계에 착수했습니다.
2. 구조적 고민: 어디서 부하를 제어할 것인가?
처음 구상했던 방식은 중앙 DataManager가 실시간 데이터 구독까지 직접 통신 모듈에 요청하고, 데이터를 취합해 화면들이 폴링(Polling)해 가는 구조였습니다.
하지만 기존 수십 개의 HTS 화면 클래스들이 통신 모듈(MngSB)을 통해 구독을 직접 등록하고 응답을 받는 구조에 너무 깊게 결합(Tightly Coupled)되어 있었습니다. 이를 전부 뜯어고치려면 전체 화면 코드를 대대적으로 수정해야 하는 막대한 리스크가 있었습니다.
따라서 기존 화면 코드 수정을 최소화하면서 관심사를 분리하는 절충 아키텍처를 결정했습니다.
- 화면의 실시간 등록 로직: 기존대로 유지 (
MngSB활용) - 데이터 처리 및 스냅샷 관리:
DataManager가 전담 (UpdateSnapshot호출) - 화면 통지(PostMessage):
MngSB가 담당하되, 과도한 통지를 MngSB 단에서 스로틀링(Throttling)으로 방어
이 구조를 통해 화면(View) 영역을 전혀 건드리지 않고도 통신 모듈 계층에서 안전하게 폭주하는 메시지를 통제할 수 있게 되었습니다.
3. 대안 탐색: UI 더티 플래그 vs 퍼블리셔 스로틀링
과도한 UI 갱신 부하를 막기 위해 실무적으로 두 가지 구현 대안을 두고 비교 검토를 진행했습니다.
대안 1: 화면(UI) 단에서 Dirty Flag와 Invalidate() 활용
DataManager가 데이터만 업데이트하고, 대상 화면 클래스에 Invalidate()를 호출하여 윈도우 OS가 여유가 될 때 WM_PAINT 메시지를 통해 페인팅하도록 맡기는 방식입니다.
- 한계점: HTS의 모든 화면이
OnPaint()에서 직접 그래픽을 그리는(Custom Drawing) 구조가 아니었습니다. 화면 위에 얹혀진 수많은 개별 컨트롤/컴포넌트(Edit, Static, Grid 등)에SetItemText()같은 함수로 직접 값을 세팅해야 하는 경우가 많아Invalidate()만으로는 갱신이 불가능했습니다. - 결합도 문제:
DataManager가 수많은 UI 클래스의 인스턴스 포인터를 일일이 알고 있어야 하므로 클래스 간 결합도가 극도로 높아지는 부작용이 있었습니다.
대안 2: 퍼블리셔(MngSB) 단에서의 스로틀링 (최종 선택)
통신 모듈(MngSB)이 UI로 PostMessage를 쏠 때 쿨타임(Cooldown)을 두어 메시지 발송 빈도를 제어하는 방식입니다.
- 장점: 기존 UI 클래스를 단 한 줄도 수정할 필요가 없고, 통신 모듈 계층에서 깔끔하게 통제가 완료됩니다.
| 비교 항목 | 대안 1: UI Dirty Flag + Invalidate() | 대안 2: MngSB 퍼블리셔 스로틀링 (선택) |
|---|---|---|
| 수정 범위 | 모든 UI 화면 클래스 대폭 수정 필요 | 통신 모듈(MngSB) 단독 수정 (UI 무수정) |
| 컨트롤 지원 | Custom Draw 화면만 가능 (SetItemText 한계) | 모든 표준/커스텀 컨트롤 100% 호환 |
| 결합도 | DataManager ↔ UI 강결합 발생 | 느슨한 결합 유지 (관심사 분리) |
| 제어 위치 | 화면(Consumer) 단 분산 처리 | 통신 모듈(Publisher) 단 집중 제어 |
4. 핵심 챌린지: 가변 쿨타임과 마지막 데이터 누락 방지
퍼블리셔 스로틀링을 도입하기로 결정했으나, 단순한 고정 쿨타임(Fixed Cooldown) 방식에는 두 가지 치명적인 딜레마가 존재했습니다.
- 반응성 딜레마: 쿨타임(예: 200ms)이 너무 길면 장이 한가할 때(평상시)도 데이터가 늦게 떠서 HTS 특유의 즉각적인 체감이 죽어버립니다.
- Trailing Edge 누락 (가장 치명적): 쿨타임 도중에 패킷이 들어와 메시지를 무시(Drop)했는데, 그게 해당 종목의 '마지막 체결 패킷'이라면 화면은 영원히 이전 과거 시세에 머물게 됩니다.
이 두 가지 문제를 완벽하게 해결하기 위해 가변 스로틀링(Adaptive Throttling)과 Trailing Edge Flush 기법을 융합 설계했습니다.
4.1 가변 스로틀링 (Adaptive Throttling) 도입
데이터가 유입되는 시간 간격(dwElapsed)을 실시간으로 계산하여 상황에 맞춰 쿨타임을 동적으로 조절하도록 구현했습니다.
- 장 폭발 시 (패킷 유입 간격 < 5ms): 쿨타임을 점진적으로 늘려 최대 200ms까지 확장합니다. 초당 5회 수준으로 메시지를 압축하여 UI 큐를 완벽하게 보호합니다.
- 평상 시 (패킷 유입 간격 > 100ms): 쿨타임을 즉시 최소치(10ms)로 축소합니다. 데이터가 들어오는 즉시 지연 없이(0ms 체감) 화면에 반영되어 최고의 반응성을 유지합니다.
4.2 Trailing Edge Flush (잔여 패킷 밀어내기)
스로틀링에 의해 스킵된 최신 데이터가 화면에 영원히 반영되지 못하는 현상을 막기 위해, MngSB 내부에 이미 존재하던 백그라운드 스케줄러(AftPBProc 루프)를 활용했습니다.
- 스로틀링에 의해 메시지 전송을 스킵할 때마다
HasPendingData = true로 마킹하고m_setPendingDataMgrSet(큐)에 해당 항목을 등록합니다. - 백그라운드 스케줄러 루프가 돌면서 이 큐를 검사하여, "최신 패킷이 남아있는데 쿨타임이 경과한 경우" 강제로
PostMessage를 밀어내어(Flush) 화면을 최종 갱신합니다. - 또한 화면이 닫히거나 소멸(
RemoveSB)될 때 이 대기 큐에서 해당 항목을 즉각 제거하여 Dangling Pointer로 인한 크래시 발생 가능성을 완벽하게 차단했습니다.
5. 에피소드: 아찔했던 파일 손상과 기적적인 코드 복구
이 정교한 로직을 적용하는 과정에서 식은땀이 흐르는 아찔한 사고도 있었습니다.
MngSB.cpp (약 1,200줄에 달하는 핵심 통신 소스) 파일에 파이썬/스크립트를 활용해 정규식 치환(Replace)을 시도하던 중, 인코딩 문제로 인해 파일의 모든 개행 문자(\r\n)가 증발하면서 1,200줄 전체 코드가 단 한 줄로 뭉개져버린 것입니다.
해당 파일이 IDE 에디터에 열려있지 않은 상태로 외부 스크립트가 실행되어
Ctrl+Z(실행 취소)조차 불가능한 절망적인 상황이었습니다.
눈앞이 캄캄했지만 포기하지 않고 로컬 디스크를 샅샅이 뒤졌습니다. 마침 다른 테스트용 샘플 프로젝트 폴더에 5개월 전 백업되어 있던 동명의 MngSB.cpp 옛날 파일이 기적적으로 남아있음을 발견했습니다. 이를 바탕으로 차이점을 하나하나 대조하며 기존 로직을 완벽히 복원한 뒤, 가변 스로틀링과 Trailing Edge Flush 로직까지 안전하게 재이식할 수 있었습니다.
6. 결론: 반응성과 안정성을 모두 잡은 최종 성과
치열한 고민과 아찔한 우여곡절 끝에 완성된 이번 실시간 처리 아키텍처는 다음과 같은 뛰어난 성과를 입증했습니다.
- 평시 반응성: 패킷 유입 시 0ms의 즉각적인 화면 반응 속도 유지
- 폭발 시 안정성: 초당 수천 건 시세 폭주 시에도 UI 프리징 없는 완벽한 방어력 달성
- 데이터 정합성: Trailing Edge Flush를 통해 마지막 체결 잔여 데이터 100% 반영 보장
- 구조적 유지보수성: 수십 개 View 클래스의 수정 없는 깔끔한 관심사 분리(SoC) 실현
실시간 금융 플랫폼에서 고성능과 사용자 경험(UX)은 양립하기 어려운 과제이지만, 정교한 가변 제어와 백그라운드 플러시 기법을 통해 두 마리 토끼를 모두 잡을 수 있었습니다.
7. 자주 묻는 질문 (FAQ)
SetItemText 등)해야 하는 경우가 많았기 때문입니다. 또한 중앙 DataManager가 수많은 UI 포인터를 직접 관리하면 클래스 간 결합도가 극도로 높아지므로 통신 모듈 단에서 쿨타임으로 메시지를 제어하는 방식이 훨씬 유지보수에 유리했습니다.
RemoveSB 함수에서 m_setPendingDataMgr 대기 큐에 등록된 항목을 즉시 제거하도록 처리하여, 백그라운드 스케줄러가 이미 파괴된 윈도우 핸들에 접근하는 메모리 참조 오류를 원천 차단했습니다.