나는 Django, Redis, WebSockets로 실시간 주식 가격 트래커를 만들었다
Source: Dev.to
나는 백엔드 엔지니어링에서 틈새를 찾고 싶었고 실시간 시스템에 끌렸다. 실시간 시스템이 실제로 어떻게 작동하는지—단순히 사용하지 않고 직접 구축하고 싶었다. 그래서 나는 주식 가격 트래커를 만들었다. 이 트래커는:
- 매 60초마다 실시간 가격을 가져오고,
- SMA를 계산하고,
- 교차 알림을 감지하며,
- 모든 데이터를 WebSocket을 통해 연결된 클라이언트에 푸시한다.
아래는 내가 배운 내용이다.
What it does (every 60 seconds)
- Fetches Finnhub API에서 15개의 주식 실시간 가격을 가져옵니다.
- Saves 데이터를 PostgreSQL에 저장합니다.
- Caches 각 주식당 최근 5개의 가격을 Redis에 캐시합니다.
- Calculates 캐시된 데이터로 5기간 SMA를 계산합니다.
- Detects 상승/하락 교차 알림을 감지합니다.
- Broadcasts 모든 정보를 하나의 메시지로 연결된 WebSocket 클라이언트에 전송합니다.
스택
- Django + DRF – API 레이어.
- Celery + Celery Beat – 작업 스케줄링.
- Redis – 캐싱 및 Channels 백엔드.
- Django Channels – WebSocket 지원.
- Uvicorn – ASGI 서버.
- Finnhub API – 시장 데이터.
- SQLite – 데모 DB에 사용 (Mac에서 PostgreSQL 사용 시 문제가 있었음).
나에게 와닿은 부분
Redis는 다양한 용도로 사용할 수 있습니다. Celery 브로커로 사용해 본 적은 있었지만, 이번 프로젝트를 통해 특히 롤링‑윈도우 데이터 스토어로서의 광범위한 가능성을 깨달았습니다.
하나의 Redis 인스턴스에 할당된 세 가지 작업
| # | 역할 | 설명 |
|---|---|---|
| 1 | Celery 브로커 | Celery Beat와 워커 사이에 작업을 전달합니다. |
| 2 | 가격 캐시 | 각 주식당 최근 5개의 가격을 Redis List 형태로 저장합니다. |
| 3 | 채널 레이어 백엔드 | Celery가 Django Channels와 통신하여 WebSocket 메시지를 방송하도록 합니다. |
하나의 서비스가 세 가지 완전히 다른 용도로 사용되는 것을 보니 전구가 켜지는 순간이었습니다.
캐싱 작동 방식
각 주식은 마지막 5개의 가격을 보관하는 Redis List를 가지고 있습니다. 업데이트가 발생할 때마다 다음 두 명령을 실행합니다:
RPUSH stock:AAPL:prices 255.78 # 새 가격을 리스트 끝에 추가
LTRIM stock:AAPL:prices -5 -1 # 최신 5개 항목만 유지
리스트는 절대 5개를 초과하지 않으며, 가장 오래된 가격은 자동으로 삭제됩니다.
또한 Redis pipelines를 사용해 이러한 명령들을 배치했습니다. 주식당 하나씩 총 15번의 라운드‑트립을 하는 대신, 모든 명령을 큐에 넣고 한 번의 라운드‑트립으로 실행하여 60번의 요청을 단 2번으로 줄였습니다.
SMA와 알림이 작동하는 방식
다섯 개의 가격이 캐시되면, SMA는 단순 평균입니다:
sma = sum(last_5_prices) / 5
교차점 감지
# Bullish: price was below SMA, now above
if previous_price < previous_sma and current_price > current_sma:
alert = "bullish"
# Bearish: price was above SMA, now below
if previous_price > previous_sma and current_price < current_sma:
alert = "bearish"
우리는 교차를 감지하기 위해 이전 값이 필요하므로, SMA를 캐시하는 것이 중요합니다.
실시간 방송 작동 방식
백그라운드 Celery 작업을 WebSocket 클라이언트와 연결하는 것이 가장 까다로운 부분이었습니다. 해결책은 Channel Layer입니다:
Celery task finishes processing
↓
Publishes message to Channel Layer (Redis)
↓
Django Channels picks it up
↓
Pushes to all connected WebSocket clients
Redis는 두 프로세스 사이의 다리 역할을 합니다.
WebSocket 페이로드 예시
{
"type": "stock_update",
"timestamp": "2026-02-14T21:38:22+00:00",
"stocks": [
{ "ticker": "AAPL", "price": 255.78, "sma": 254.32, "alert": null },
{ "ticker": "MSFT", "price": 401.32, "sma": 399.80, "alert": "bullish" },
{ "ticker": "TSLA", "price": 417.44, "sma": 419.10, "alert": "bearish" }
]
}
모든 15개의 주식이 한 번에 전송되며, 60 초마다 자동으로 전송됩니다.
개발 환경에서 두 서버 실행하기
manage.py runserver는 WSGI 서버로, 요청/응답 사이클만 처리합니다. WebSocket은 지속적인 연결이 필요하므로 ASGI 서버가 필요합니다.
# DRF browsable API (WSGI)
python manage.py runserver # → http://localhost:8000
# WebSocket server (ASGI)
uvicorn core.asgi:application --port 8001 # → ws://localhost:8001
REST 엔드포인트는 포트 8000에서, WebSocket 연결은 포트 8001에서 동작합니다.
내가 실제로 배운 것
들어가기 전에는 Django를 알고 있었고 Redis를 조금 사용해 본 적이 있었습니다. 이제는 다음을 이해하게 되었습니다:
- Celery Beat 로 백그라운드 작업 스케줄링이 어떻게 작동하는지.
- Redis Lists 가 데이터의 롤링 윈도우에 완벽하게 맞는 이유.
- Redis pipelines 가 명령을 배치 처리할 때 왜 중요한지.
- WSGI 와 ASGI 의 차이점.
- Django Channels 가 비동기와 동기 코드를 연결하기 위해 Channel Layer 를 어떻게 사용하는지.
- 실시간 데이터 파이프라인을 엔드‑투‑엔드로 구조화하는 방법.
이 과정을 통해 실시간 시스템이 훨씬 덜 신비롭게 느껴졌습니다. 마법이 아니라, 생산자, 채널, 그리고 소비자일 뿐입니다.
소스 코드
향후 계획
- 작동 방식을 보여주는 간단한 프론트엔드
- 시장이 폐쇄된 경우(자동으로) API 호출이 이루어지지 않도록 보장
- 데이터베이스를 PostgreSQL로 마이그레이션
