- 초당 수천건의 거래에 해당하는 주식거래 데이터를 사용자가 실시간 Tick 으로 볼 수 있음
- 실시간 거래 이상 내역을 확인 할 수 있음
- 지난 거래내역을 분/시간/일별로 조회 할 수 있음
flowchart TD
Source[Data Source] --Incoming--> PostgreSQL[PostgreSQL with TimescaleDB extension]
App[Real-time Stock Application] --Fetch Data--> PostgreSQL
Debezium --CDC--> PostgreSQL
Debezium --producer--> Kafka[Apache Kafka]
App --consumer--> Kafka
Flink[Apache Flink] --consumer--> Kafka
App --Table API--> Flink
User <--Web Socket / SSE / RestAPI--> App
- 거래 데이터 생성, 저장, 분석, 알림이 각각 독립적으로 확장 가능하도록 구성
- 하나의 컴포넌트 장애가 전체 시스템에 미치는 영향을 최소화
- Real-time (WebSocket): 사용자가 즉시 거래 변화를 확인할 수 있도록
- Batch (REST API): 대용량 히스토리 데이터 조회 시 안정적인 성능 보장
- 데이터 정합성 보장: 트랜잭션 커밋과 동시에 이벤트 발행
- 기존 레거시 시스템 변경 최소화
| # | 시나리오 | HW 사양 | 대상 메트릭 | 요구사항 |
|---|---|---|---|---|
| 1 | 실시간 호가 전송 (WebSocket) | 4 CPU / 8GB RAM / 25Gbps NIC | 1,000개 동시 연결, 초당 5,000건 거래 데이터 | End-to-End 지연시간 < 100ms (DB 커밋 → Client 수신) |
| 2 | 거래 데이터 삽입 (Bulk Insert) | 8 CPU / 16GB RAM / SSD | 배치당 10,000건 거래 데이터 | 처리시간 < 500ms (asyncpg COPY 메서드 사용) |
| 3 | 히스토리 조회 (REST API) | 4 CPU / 8GB RAM / TimescaleDB | 30일치 데이터 조회 (약 1,296,000건) | 응답시간 < 2초 (TimescaleDB 파티셔닝 활용) |
측정 일시: 2026-07-10 (KST)
| 구분 | 측정 환경 |
|---|---|
| 호스트 | macOS 26.5.1 (Build 25F80), arm64 |
| 하드웨어 | MacBook Pro (MacBookPro18,3), Apple M1 Pro 10코어(8P+2E), 메모리 16GB |
| Docker | Docker Desktop 29.6.1, aarch64, 6 CPU, 메모리 약 9.7GiB |
| 애플리케이션 런타임 | Python 3.13.14, FastAPI 0.139.0, asyncpg 0.31.0, NumPy 2.5.1 |
| 데이터베이스 | PostgreSQL 16.9, TimescaleDB 2.20.3 |
| 측정 도구 | hyperfine 1.20.0, k6 2.0.0 |
docker/data-pipeline/docker-compose.yaml의 TimescaleDB, Kafka, Debezium, Kafka UI, Flink JobManager/TaskManager를 함께 실행했다.- FastAPI는 호스트의
127.0.0.1:8000에서 실행하고 TimescaleDB는localhost:5432로 연결했다. stock_trades는event_time기준 hypertable이며 청크 간격은 7일이다. 측정 범위에 포함된 청크는 6개였고 compression이 활성화되어 있었다.stock_trades에서 기본 키를 포함한 인덱스 7개를 확인했다.- Debezium의
dbz_publication은stock_trades를 대상으로 하며 connector와 task가 실행 중인 상태에서 측정했다. - continuous aggregate 비교를 위해
stock_trades_1min을 생성하고 측정 구간을 수동 refresh했다.
| 항목 | 값 |
|---|---|
| 시드 삽입 건수 | 1,296,000건 |
| 시드 배치 크기 | 10,000건 |
| 시드 시간 범위 | 2026-06-10 11:34:44.227022 UTC ~ 2026-07-10 11:34:44.227022 UTC |
| 기존 데이터 포함 조회량 | 1,508,344건 |
| 원본 조회 | 위 30일 범위, limit=1000 |
| 분 집계 조회 | 위 30일 범위, granularity=minute, limit=1000 |
현재 REST API의 pagination 상한에 따라 30일 범위의 첫 1,000건 응답을 측정했다. 분 집계 API는 stock_trades를 직접 집계하며 continuous aggregate 조회는 SQL로 별도 측정했다.
POST /api/v1/stock/generate에mode=historical,totalRecords=1296000,days=30,batchSize=10000을 전달하고 완료 상태까지 대기했다.- hypertable, 청크, 인덱스와 조회 범위 건수를 확인하고
ANALYZE stock_trades를 실행했다. - 원본 범위 조회와
stock_trades직접 분 집계에EXPLAIN (ANALYZE, BUFFERS, VERBOSE)를 실행했다. stock_trades_1min을 refresh한 뒤 동일 범위의 continuous aggregate 조회 실행계획을 측정했다.- hyperfine으로 원본 API와 분 집계 API를 각각 3회 워밍업 후 20회 측정했다.
- k6에서 5 VU로 30초 동안 각 iteration마다 원본 API와 분 집계 API를 순차 호출해 응답시간 분포를 측정했다.
| 구분 | 측정 항목 | 결과 |
|---|---|---|
| 데이터 생성 | 1,296,000건 COPY 삽입 | 46.920초 |
| SQL 원본 조회 | 실행시간 | 0.582ms |
| SQL 직접 분 집계 | 실행시간 | 6,587.098ms |
| Continuous aggregate | 30일 범위 refresh | 16.515초 |
| Continuous aggregate | 조회 실행시간 | 18.732ms |
| hyperfine 원본 API | 평균 ± 표준편차 | 34.03 ± 9.47ms |
| hyperfine 원본 API | 최소 ~ 최대 | 26.04 ~ 62.25ms |
| hyperfine 분 집계 API | 평균 ± 표준편차 | 7,279.46 ± 313.02ms |
| hyperfine 분 집계 API | 최소 ~ 최대 | 6,945.30 ~ 8,148.48ms |
| k6 원본 API | 평균 / 중앙값 / p95 / 최대 | 70.986 / 42.767 / 169.352 / 171.514ms |
| k6 분 집계 API | 평균 / 중앙값 / p95 / 최대 | 13.138 / 13.397 / 13.773 / 13.813초 |
| k6 HTTP | 요청 / 실패 / check | 30건 / 0건 / 30건 성공 |
FastAPI- 0.115.12- 비동기 처리로 수천 건의 동시 IO 연결을 효율적으로 처리
PostgreSQL with TimescaleDB extension- 16.9- 시계열 데이터 압축과 파티셔닝으로 대용량 거래 데이터의 저장/조회 성능을 최적화
Debezium: 3.0.0.Final- 데이터베이스 변경사항을 실시간으로 캐치하여 애플리케이션 로직 수정 없이 CDC를 구현
Apache Kafka- 4.0.0- 초당 수천 건의 거래 데이터를 버퍼링하고 여러 컨슈머가 독립적으로 처리할 수 있는 확장성
Apache Flink- Scala 2.12- 스트리밍 데이터에서 실시간 이상 거래 탐지를 위한 복잡한 윈도우 기반 집계 연산을 처리
- Data Source 에서 PostgreSQL 에 거래내역 데이터를 저장한다
- Data Source 라고 지칭하는 거래 데이터는 서비스 레벨에서 구현하여 모킹 함
POST http://<HOST>/api/v1/stock/generate
- 저장된 거래내역은 Change Data Capture 를 통해 Kafka 로 스트리밍된다
- Flink 에서는 거래내역을 실시간으로 통계 및 연산을 수행하여 이상 거래 여부를 판단한다
docker/data-pipeline/docker-compose.yaml — PostgreSQL(TimescaleDB), Kafka, Debezium, Flink를 한 스택으로 띄운다.
docker/data-pipeline/.env.compose.local 생성:
POSTGRES_PASSWORD=<your-password>
POSTGRES_DB=stock
POSTGRES_USER를 넣지 않으면 슈퍼유저는 postgres다.
아래 파일의 사용자명·비밀번호를 위 POSTGRES_PASSWORD와 맞춘다.
| 파일 | 넣을 값 |
|---|---|
postgres/init/02-cdc.sql |
CDC용 replication role (예: debezium) |
postgres/pg_hba.conf |
위 role 이름 (replication 허용 줄) |
debezium/postgres-stock-connector.json |
database.* → CDC role, transforms.timescaledb.database.* → postgres + POSTGRES_PASSWORD |
01-ddl.sql, 02-cdc.sql은 postgres_data volume이 처음 만들어질 때만 실행된다.
cd docker/data-pipeline
docker compose up -d
./debezium/register-connector.shregister-connector.sh는 http://localhost:8083에 postgres-stock-connector.json을 등록한다. Compose에 포함되지 않는다.
| 서비스 | 역할 | 호스트 포트 |
|---|---|---|
postgres-stock |
TimescaleDB, 01-ddl.sql·02-cdc.sql로 초기화 |
5432 |
broker |
Kafka (KRaft) | 9094 |
debezium-connect |
PostgreSQL CDC → Kafka | 8083 |
kafka-ui |
Kafka UI | 8080 |
jobmanager |
Flink JobManager | 8081 |
taskmanager |
Flink TaskManager | — |
네트워크 stock_trade_network. 데이터는 postgres_data, kafka_data volume에 저장된다.
| 호스트에서 실행 | Compose 내부 | |
|---|---|---|
| PostgreSQL | localhost:5432 |
postgres-stock:5432 |
| Kafka | localhost:9094 |
broker:9092 |
- Hypertable:
stock_trades테이블을event_time기준으로 Hypertable로 변환하여 시계열 데이터에 최적화된 파티셔닝을 자동 적용합니다. 이를 통해 대규모 데이터셋에서도 빠른 데이터 삽입 및 범위 기반 조회(Time-based queries) 성능을 보장합니다. - 고성능 삽입: 데이터 생성 시
asyncpg의copy_records_to_table메서드를 사용하여 PostgreSQL의COPY명령을 실행합니다. 이는INSERT를 반복하는 것보다 훨씬 빠른 속도로 대량의 데이터를 벌크 삽입할 수 있게 해주는 핵심 기능입니다.
- CDC 설정: Debezium PostgreSQL 커넥터가
stock_trades테이블의 변경 사항을 실시간으로 감지합니다.publication.name으로dbz_publication을 사용하여 논리적 디코딩(Logical Decoding)을 통해 변경분을 스트림으로 변환합니다. - Kafka 토픽: 감지된 모든 데이터 변경(INSERT, UPDATE, DELETE) 이벤트는
stock.public.stock_trades라는 Kafka 토픽으로 발행(Publish)됩니다. 이 토픽은 실시간 데이터 파이프라인의 중심 허브 역할을 합니다. - 느슨한 결합: 이 아키텍처를 통해 데이터베이스와 실시간 처리 시스템(FastAPI, Flink)이 분리됩니다. 데이터베이스는 데이터 저장에만 집중하고, 실시간 처리가 필요한 모든 애플리케이션은 Kafka 토픽을 구독(Subscribe)하여 독립적으로 확장 및 운영될 수 있습니다.
GET ws://<HOST>/api/v1/stock/real-time- Description: 사용자의 실시간 거래내역(Tick) 을 요청 시 서비스에서 WebSocket 연결을 하여 Kafka 의 메시지를 전달한다
- 동작: Client 가 WebSocket 으로 Ticker·Tick 주기를 요청하면 Server 가 해당 Ticker 의 Kafka 토픽을 구독해 설정 주기로 호가를 전송하고, 전송 중 Ticker/주기 변경 요청을 받으면 기존 구독을 해제하고 신규 구독으로 전환한다
- 구현/진행: 개발 항목 #1~#6
GET http://<HOST>/api/v1/stock/anomaly- Description: 사용자의 거래 이상 거래 탐지 확인 요청 시 SSE 로 발생 내역을 전달한다
- 동작: Flink 이상 거래 결과 스트림을 subscribe 해 이벤트 타입·발생 시각·ticker·탐지 근거를 포함한 SSE 로 변환하며, keep-alive/heartbeat 와 연결 종료 시 consumer 정리·재연결, 이상 없음/지연/실패 예외 정책을 따른다
- 구현/진행: 개발 항목 #12~#16 (선행: Flink 배포)
GET http://<HOST>/api/v1/stock- Description: 지난 거래내역에 대한 조회 요청 시 RestAPI 로 제공한다
- 동작: duration·ticker·tradeType·marketCode 조합과 시작/종료 시각 기간 조회, 분/시간/일 집계, pagination/cursor 를 지원하고, count·filters·aggregate metadata 를 포함한 표준 응답과 파라미터·빈 결과·범위 초과 검증 정책을 따른다
- 구현/진행: 개발 항목 #7~#11
- #1 Kafka consumer 를 애플리케이션 런타임 의존성으로 반영
- #2 Debezium CDC 메시지 스키마 파싱 로직 구현
- #3 WebSocket 연결별 ticker / tick 상태 관리
- #4 수신 이벤트를 tick 주기 기준 candle / high-low 로 집계
- #5 ticker 변경 시 기존 구독 해제 후 신규 구독 연결
- #6 데이터 부재 / 지연 / consumer 오류 WebSocket 예외 처리
- #7 필터 조회 API를 range query 로 확장
- #8 분 / 시간 / 일 aggregation 쿼리 추가
- #9 pagination / cursor 응답 도입
- #10 응답 모델 및 validation 표준화
- #11 대량 데이터 조회 성능 검증
- #12 Flink 출력 토픽 / 결과 스트림 스키마 확정
- #13 이상 거래 이벤트 consumer 구현
- #14 SSE event / data 포맷 표준화
- #15 이상 거래 이벤트 필터링 / 직렬화
- #16 heartbeat / reconnect / graceful shutdown 처리
- #17 Kafka / PostgreSQL / Flink 설정을 env.toml 로 통합
- #18 구조화된 로그와 장애 추적 포인트 추가
- #19 연결 종료 / 재시도 / backpressure 정책 정리
- #20 통합 / 회귀 테스트 추가
- #21 성능 요구사항 점검 시나리오 문서화