나는 Django, Redis, WebSockets로 실시간 주식 가격 트래커를 만들었다

발행: (2026년 2월 15일 오전 07:46 GMT+9)
8 분 소요
원문: Dev.to

Source: Dev.to

Ebenezer Lamptey

나는 백엔드 엔지니어링에서 틈새를 찾고 싶었고 실시간 시스템에 끌렸다. 실시간 시스템이 실제로 어떻게 작동하는지—단순히 사용하지 않고 직접 구축하고 싶었다. 그래서 나는 주식 가격 트래커를 만들었다. 이 트래커는:

  • 매 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 인스턴스에 할당된 세 가지 작업

#역할설명
1Celery 브로커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 runserverWSGI 서버로, 요청/응답 사이클만 처리합니다. 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 가 명령을 배치 처리할 때 왜 중요한지.
  • WSGIASGI 의 차이점.
  • Django Channels 가 비동기와 동기 코드를 연결하기 위해 Channel Layer 를 어떻게 사용하는지.
  • 실시간 데이터 파이프라인을 엔드‑투‑엔드로 구조화하는 방법.

이 과정을 통해 실시간 시스템이 훨씬 덜 신비롭게 느껴졌습니다. 마법이 아니라, 생산자, 채널, 그리고 소비자일 뿐입니다.

소스 코드

향후 계획

  • 작동 방식을 보여주는 간단한 프론트엔드
  • 시장이 폐쇄된 경우(자동으로) API 호출이 이루어지지 않도록 보장
  • 데이터베이스를 PostgreSQL로 마이그레이션
0 조회
Back to Blog

관련 글

더 보기 »

Python에서 Zig 함수 사용

개요 이 문서는 Zig로 작은 HTTP 클라이언트를 작성하고, 이를 C에 노출한 뒤 Python에서 호출하는 방법을 보여줍니다. 초점은 다음에 있습니다: Zig의 `:0const u8 nul`…

어려운 방법으로 블랙잭 시뮬레이션

나는 블랙잭 시뮬레이터를 만들었다. 그 문장은 실제보다 과소평가한다. 내가 실제로 만든 것은 이벤트‑드리븐(event‑driven) 방식으로 카드 게임을 시뮬레이션하기 위한 28,000줄짜리 Python 프레임워크이다.