Architecture
가격이 아니라 이벤트를 팔자 — monticker 설계 철학
프로젝트를 시작한 동기
주식 앱을 처음 열었을 때 보이는 화면은 대부분 동일하다.
1
삼성전자 73,200 +1.4% [캔들 차트] [뉴스 목록]
숫자가 움직이는 건 알겠는데, 왜 움직이는지 한눈에 들어오지 않는다. 뉴스 탭을 눌러야 하고, 거래량 탭을 따로 눌러야 하고, 공시 탭도 따로 있다. 정보가 파편화되어 있다.
실제로 트레이더들이 “이 차트 어떻게 봐야 해?”라는 질문에 시간 축 위에서 사건을 짚어가며 설명한다는 사실에서 monticker의 핵심 아이디어가 나왔다.
가격 차트 위에 이벤트를 직접 올리면 어떨까?
이벤트 중심(Event-Centric) UI란
일반적인 주식 앱:
1
2
3
4
10:00 | 70,000 ─────────────────────────
10:05 | 70,200
10:10 | 72,500 ← 왜 올랐지?
10:15 | 72,300
monticker:
1
2
3
4
10:10 ▲ PRICE_SPIKE +3.2%
├── 거래량: 평균의 4.8×
├── 뉴스: "HBM 공급 확대 기대감"
└── 공시: 없음
가격 변동의 원인이 될 만한 사건을 타임라인에 직접 표시한다. 그래서 사용자가 차트와 뉴스를 오가며 둘을 손으로 맞춰 볼 필요가 없다.
이 아이디어를 기술로 구현하면 다음과 같은 파이프라인이 된다.
1
2
3
4
5
6
7
External data sources
→ workers (price + news + disclosure collector)
→ Redis (latest price) + TimescaleDB (candles)
→ Event Detector (EMA 기반 이상 탐지)
→ stock_events (이벤트 테이블)
→ REST / WebSocket API
→ 차트 타임라인 오버레이 (web / mobile)
“이벤트”를 어떻게 정의했는가
monticker에서 이벤트는 5가지 유형으로 분류된다.
| 유형 | 예시 | 탐지 방법 |
|---|---|---|
PRICE_SPIKE | 5분 내 3% 이상 급등 | EMA 기반 변화율 |
VOLUME_SURGE | 평균 거래량의 3× 이상 | EMA 기반 비율 |
NEWS_POSITIVE | 긍정 뉴스 발행 | 네이버 뉴스 + 감성 분석 |
DISCLOSURE | 공급 계약, 실적 발표 | DART API |
PATTERN_DETECTED | 이중 바닥, 헤드앤숄더 | ZigZag 패턴 매칭 |
이 이벤트들이 하나의 stock_events 테이블에 모이고, 타임라인 API는 주어진 기간의 이벤트 목록을 차트 라이브러리가 소비할 수 있는 형식으로 반환한다. EMA(지수이동평균, 최근 값에 더 큰 가중치를 두는 평균)로 급등과 거래량 서지를 잡는 방식은 EMA 기반 이상 탐지에서 다룬다.
제품이 확장된 방향
처음에는 순수하게 “왜 가격이 움직였는지 보여주는 앱”을 목표로 했다. 그런데 개발 과정에서 이어진 질문들이 있었다.
- “이 패턴이 반복되는지 확인하고 싶다” → Quant Lab (전략 백테스트)
- “주문을 넣으면 어떻게 체결되는지 배우고 싶다” → 모의투자 + CLOB(중앙 지정가 주문장) 체결 엔진
- “내가 충동적으로 주문을 많이 넣는 것 같다” → Investment Wallet + 감정 태그
- “내 전략이 어떤 시장에서 약한지 알고 싶다” → Quant Analytics (국면 감지 + 포트폴리오 최적화)
네 확장 모두 “가격이 왜 움직이는가”라는 원래 질문에서 갈라져 나왔다. 기능을 따로 덧붙였다기보다, 투자자가 시장을 이해하려 할 때 이어지는 질문을 차례로 따라간 결과다.
이 프로젝트에서 다루는 기술 스택
이후 시리즈에서 상세히 다룰 내용의 전체 그림이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
┌─── 수집 ──────────────────────────────┐
│ Go Market Gateway → Kafka │
│ (goroutine-per-stock) market.ticks │
└────────────────────────────────────────┘
↓
┌─── 처리 ──────────────────────────────┐
│ Kotlin Worker (Spring Boot) │
│ ├── CandleAggregator → TimescaleDB │
│ ├── EventDetector → stock_events │
│ └── RedisTickWriter → Redis │
└────────────────────────────────────────┘
↓
┌─── 서빙 ──────────────────────────────┐
│ Spring Boot API → REST / WS │
│ Netty Broadcast → WebSocket 클라이언트 │
└────────────────────────────────────────┘
↓
┌─── 클라이언트 ─────────────────────────┐
│ Next.js 15 (App Router) │
│ Expo React Native (mobile) │
└────────────────────────────────────────┘
| 레이어 | 기술 | 이유 |
|---|---|---|
| 수집 | Go | goroutine의 가벼운 동시성 |
| 메시지 버스 | Kafka | 수집-처리 분리, 순서 보장 |
| 시계열 저장 | TimescaleDB | PostgreSQL 호환 + Hypertable |
| 캐시 | Redis | 최신 시세 서빙, 알람 쿨다운 |
| API 서버 | Kotlin/Spring Boot | 도메인 모델링, 생태계 |
| 브로드캐스트 | Netty | 스레드 효율적인 WebSocket |
| 프론트엔드 | Next.js 15 | SSR + App Router |
이 그림은 시리즈를 시작할 때의 설계다. 이후 Netty Broadcast는 제거되고 WebSocket 경로는 api의 Spring STOMP 하나로 합쳐졌다. 그 경위와 측정은 WebSocket push와 REST polling 비교에 있다. 표의 “순서 보장”도 조건이 붙는다. Kafka는 파티션 안에서만 순서를 지키고, 같은 키로 보내도 프로듀서 설정에 따라 순서가 깨질 수 있다는 것을 market.ticks 파티션 실험에서 확인했다. TimescaleDB의 Hypertable은 TimescaleDB를 고른 이유에서 다룬다.
이 시리즈를 읽는 방법
monticker 시리즈는 총 6개 주제로 구성된다.
| 시리즈 | 주제 | 편수 |
|---|---|---|
| 1. 아키텍처 설계 | 제품 철학·기술 선택 | 3 |
| 2. 실시간 파이프라인 | Go·Kafka·Netty | 4 |
| 3. 체결 엔진 | CLOB·리스크 게이트 | 2 |
| 4. 이벤트 소싱 | 원장·감정 태그 | 2 |
| 5. Quant Lab | 룰 엔진·백테스트 | 3 |
| 6. Quant Analytics | 최적화·패턴·국면 | 5 |
| 보너스 | 테스트·관측가능성 | 3 |
다음 편에서는 마이크로서비스가 아닌 모듈식 모놀리스를 선택한 이유를 다룬다.
댓글
아직 댓글이 없습니다