8장까지 우리는 바이브 코딩을 통해 꽤 그럴싸한 화면을 만들어냈습니다. 화려한 차트 및 테이블과 심플한 스파크라인이 들어간 자산 리스트 까지, 프로급 금융 대시보드의 핵심은 모두 만들었다고 해도 과언이 아닙니다.
하지만 지금 우리의 웹사이트 프로젝트에는 치명적인 단점이 하나 있습니다. 바로 ‘가격이 변하지 않는다’는 것입니다. 사용자가 처음에 만든 차트페이지의 수집 버튼을 클릭하지 않는 이상, 화면 속 비트코인이나 애플 같은 주식 가격은 멈춰 있습니다.
실제 거래소나 증권사 앱처럼 숫자가 실시간으로 빨갛게, 파랗게 깜빡이며 변하게 만들려면 어떻게 해야 할까요?
상식선에서 생각할 수 있는 방법은 우리가 배운 REST API를 00초마다 해당 클라이언트(FMP나 바이낸스)에게 요청하는 방법입니다.
이는 문제가 있는 방식으로 FDP별 유무료 상관없이 분당 시간당 일일 달에 요청제한을 하고 있다는 것입니다. 그리고 유저가 웹사이트에 접속했을때 계속 API요청이 누적되어 메모리를 잡아 먹고 우리의 서버도 트래픽이 과하게 요청되어 감당을 할수 없게 됩니다.
항상 우리의 선배 하드코어분들은 대답을 만들어 놓습니다. 이를 웹소켓(WebSocket)이라고 부르며 실시간 파이프라인이라고 합니다. 용어 그대로 실시간 가격 생산자와 소비자간을 연결하는 파이프 라인입니다.
웹소켓(WebSocket)
웹소켓의 목적은 “100명에게 일일이 전화하지 마라, ‘방송(Broadcast)’하라!” 입니다.
웹소켓을 이해하기 가장 쉬운 비유는 ‘전화 통화’와 ‘사내 방송’의 차이입니다.
일반적인 API 방식 (Polling)
1:1 전화 통화 유저가 “지금 가격 얼마야?” 하고 서버에 전화를 겁니다. 서버는 거래소에서 가격을 확인한 뒤 대답합니다. 유저가 100명이면 서버는 100번의 전화를 받아야 하고, 거래소에도 100번이나 가격을 물어봐야 합니다. 서버 자원이 순식간에 고갈됩니다.
웹소켓 브로드캐스팅(Broadcasting)
사내 방송 유저들이 웹사이트에 들어오면 서버와 항상 연결된 ‘파이프(웹소켓)’를 하나씩 연결합니다. 서버는 거래소에서 가격을 딱 한 번만 확인한 다음, 연결된 파이프를 통해 100명에게 동시에 쫙 뿌려줍니다(Broadcast).
- 서버의 부담이 100분의 1로 줄어듭니다.
- API 호출 비용이 극적으로 절약됩니다.
특히 우리가 백엔드로 선택한 FastAPI는 이름 그대로 엄청나게 빠르며, 비동기(Async) 처리에 특화되어 있어 이런 웹소켓 브로드캐스팅을 구현하는 데 있어 현존하는 파이썬 프레임워크 중 최고의 효율을 자랑합니다. 1GB RAM 서버라는 극한의 환경에서 살아남기 위한 필수 생존 기술인 셈입니다.
웹소켓 베이스 컨슈머(Base Consumer)
웹소켓 또한 일종의 API라 앞서 REST API의 근본이 되는BaseClient를 먼저 만들었듯이 웹소켓 또한 FMP로 부터 방송을 받는(구독이라고 함) 근본이 되는 BasedConsumer.tsx를 먼저 만들겠습니다.
백엔드에 아무 폴더에 넣는것 보다는 알수 있는 폴더에 파일을 넣는 것이 유지 보수가 용이 합니다.
services/ ├── websocket/ │ ├──base_consumer.py │ ├──binance_consumer.py │ ├──finnhub_consumer.py
REST API를 다루던 BaseClient가 똑똑하고 예의 바른 ‘배달원’이었다면, 실시간 웹소켓을 다루는 BaseConsumer는 거칠게 쏟아지는 물줄기를 안전하게 받아내는 ‘견고한 저수지 관리자’ 같은 포지션입니다. 이 관리자가 갖춰야 할 3가지 필수 생존 능력을 알아보겠습니다.
하나, 자동재연결
웹소켓은 한 번 연결되면 계속 유지되는 특성이 있지만, 현실에서는 인터넷 회선이 불안정하거나 외부 FDP 서버가 주기적으로 재시작하면서 선이 툭툭 끊어집니다.
이때 평범한 컨슈머는 에러를 뱉고 장렬히 전사(앱 크래시)해 버립니다. 하지만 똑똑한 BaseConsumer는 당황하지 않습니다.
연결이 끊어지면 “1초 뒤에 다시 붙어보고, 안 되면 2초 뒤, 그래도 안 되면 4초 뒤에 시도”하는 끈기(Exponential Backoff, 지수 백오프)를 발휘하여 우리가 자는 동안에도 서비스가 멈추지 않게 지켜줍니다.
둘, 데이터 폭탄 막아내기
FMP가 제공하는 가격은 틱단위로 하나의 티커면 문제가 없지만 수신하는 티커가 많아 지면 기하 급수적으로 데이터 폭탄이 몰아 칩니다.
이러한 테이터 폭탄에 브라우저는 과부하가 걸려 화면을 멈춰버립니다. (이를 Throttling 현상이라고 합니다.) 따라서 BaseConsumer는 쏟아지는 데이터를 0.5초나 1초 단위로 모은 뒤, 가장 마지막 최신 가격 딱 하나만 유저들에게 전달하도록 속도를 조절하는 ‘댐’ 역할을 해야 합니다.
셋, 수신되는 데이터 형식 통일 (응답 표준화)
REST API와 마찬가지로 외부 FDP마다 실시간 데이터를 던져주는 모양이 다릅니다. 바이낸스는 {“p”: “50000”, “q”: “0.1”}처럼 약어를 쓰고, 다른 곳은 {“trade_price”: 50000}처럼 씁니다.
이걸 그대로 프론트엔드에 보내면 화면 코드가 엉망진창이 됩니다. 어느 거래소에서 데이터가 오든 상관없이, 우리 방송국을 거칠 때는 무조건 {“ticker”: “BTC”, “price”: 50000}처럼 우리 식당만의 똑같은 규격 상자에 예쁘게 담아서(포맷팅) 전달해야 합니다.
🤖 실전! 베이스 컨슈머 만들기(2-11)
🧠 이번 실습은 백엔드에 웹소켓으로 방송(실시간 가격)을 수신하는 기본 컨슈머(BaseConsumer) 추가하는 과정입니다.
✨ 사용자의 의도(Vibe)
웹소켓으로 방송(실시간 가격)을 수신하는 기본 컨슈머(BaseConsumer) 추가하는데 아래 내용을 적용해줘
- 자동 재연결 기능을 넣어서 우리가 자는 동안에도 서비스가 멈추지 않게 지주게해(Exponential Backoff)
- 수신되는 데이터 폭탄을 효과적으로 막아내게 해줘(버퍼 및 시간설정(예:1초))
- 수신되는 데이터 형식 통일 (응답 표준화 기능, /base_client.py와 같은 형식유지)
- 파일명 : backend/app/services/websocket/base_consumer.py
💻 AI가 생성한 코드 (Syntax) 및 결과
- backend/app/services/websocket/base_consumer.py
import asyncioimport jsonimport loggingimport timefrom typing import Any, Dict, List, Optional, Callableimport websocketsfrom websockets.exceptions import ConnectionClosedlogger = logging.getLogger(__name__)class BaseConsumer: """ WebSocket을 통해 실시간 데이터를 수신하는 기본 컨슈머 클래스입니다. 자동 재연결, 데이터 스로틀링(버퍼링), 응답 표준화 기능을 제공합니다. """
def __init__( self, url: str, throttle_interval: float = 1.0, max_reconnect_attempts: int = 10, initial_backoff: float = 1.0, max_backoff: float = 60.0 ): 이하 생략–