들어가며

3편까지 데이터를 수집하고 DB에 넣었다. 이제 이 데이터를 웹 브라우저에서 볼 수 있게 만들어야 한다.

DB에 직접 접속해서 SQL을 치는 건 나만 할 수 있다. 누구든 브라우저만 열면 볼 수 있으려면, DB와 브라우저 사이에 "중간 다리"가 필요하다. 이 역할을 하는 게 백엔드 서버다.

이번 편에서는 FastAPI로 55개 API 엔드포인트를 만들고, 실시간 시세까지 연동한 과정을 다룬다.


왜 FastAPI인가

Python으로 API 서버를 만들 수 있는 프레임워크는 여러 가지다. Django, Flask, FastAPI 등. 이 중에서 FastAPI를 선택한 이유:

  • 빠르다 — 비동기(async) 기반이라 여러 요청을 동시에 처리할 수 있다
  • 코드가 적다 — 함수 하나 만들면 API 하나가 된다. 설정 파일 따로 필요 없다
  • 자동 문서화 — API를 만들면 테스트 페이지가 자동으로 생긴다
  • 데이터 수집기도 Python — 같은 언어라서 DB 접근 코드를 공유할 수 있다

Django는 기능이 너무 많고(관리자 페이지, ORM 등 안 쓸 것들이 많다), Flask는 비동기 지원이 약하다. FastAPI는 딱 필요한 것만 가볍게 쓸 수 있어서 개인 프로젝트에 적합했다.

설명은 이렇지만 사실은 Claude Code가 추천한 것이다.


API가 뭔데?

비개발자를 위해 잠깐 설명하면 — API는 "이런 주소로 요청하면, 이런 데이터를 돌려줄게"라는 약속이다.

예를 들어:

  • /api/dashboard → 대시보드에 보여줄 전체 시장 요약 데이터
  • /api/stock/005930/info → 삼성전자의 상세 정보
  • /api/stock/005930/prices → 삼성전자의 주가 차트 데이터
  • /api/screener?per_max=10&roe_min=15 → PER 10 이하, ROE 15% 이상 종목 목록

브라우저(프론트엔드)가 이 주소로 요청을 보내면, 서버(백엔드)가 DB에서 데이터를 꺼내서 JSON 형태로 돌려준다. 프론트엔드는 받은 데이터를 예쁘게 그려주기만 하면 된다.


55개 API, 9개 라우터

처음에는 API가 5~6개였다. 대시보드, 종목 상세, 주가 차트 정도. 그런데 기능이 하나씩 추가되면서 55개까지 늘어났다.

API가 50개가 넘으면 파일 하나에 다 넣기 어렵다. 그래서 라우터라는 단위로 분리했다:

라우터 담당 주요 API 예시
종목 (stock) 종목 검색, 상세 정보, 주가 종목 검색, 종목 상세, 차트 데이터
시장 (market) 대시보드, 지수, 환율 대시보드, 지수 데이터, 환율/원자재
시그널 (signal) 52주 신고가, 수급, 퀀트 시그널 목록, 트리맵, 퀀트 추천
스크리너 (screener) 조건 검색 필터링 + 정렬 + 페이지네이션
ETF (etf) ETF 분석 ETF 대시보드, 카테고리, 랭킹
증권사 (broker) 증권사 추천 컨센서스, 개별 리포트
비교 (compare) 종목 비교 최대 5종목 병렬 비교
이벤트 (event) 이벤트 캘린더 실적 발표, 배당락일 등
방문자 (visitor) 방문 통계 일별 방문자 수

 

라우터를 나누면 좋은 점은, ETF 관련 수정을 할 때 ETF 파일만 보면 된다는 것이다. 다른 기능에 영향을 줄 걱정 없이 독립적으로 개발할 수 있다.


실시간 시세는 어떻게?

DB에 저장된 건 장 마감 후의 종가다. 그런데 장중에 사이트를 열면 실시간 시세를 보고 싶다.

직접 증권사 실시간 API를 연동하면 좋겠지만, 웹소켓 연결을 유지해야 하고 3,970종목을 동시에 구독하면 트래픽이 상당하다. 개인 프로젝트에서는 과한 접근이다.

대신 1분 주기로 전 종목 시세를 갱신하는 방식을 택했다. 서버가 백그라운드에서 1분마다 외부 API를 호출해서 캐시에 저장하고, 프론트엔드는 이 캐시를 읽는다.

완벽한 실시간은 아니지만, 1분 지연이면 일반 투자자 입장에서 충분하다. HTS처럼 초 단위 틱 데이터가 필요한 게 아니니까.


캐시 - 같은 걸 반복해서 계산하지 않기

대시보드를 한 번 열면 내부적으로 API 호출이 여러 번 일어난다. 시장 요약, 상승/하락 TOP, 수급 순위, 시그널... 각각이 DB에서 수천 ~ 수만 행을 조회하고 계산한다.

매번 같은 계산을 반복하면 느리다. 장 마감 후 데이터는 다음 날까지 바뀌지 않으니, 한 번 계산한 결과를 메모리에 저장해두고 다음 요청에는 저장된 결과를 바로 돌려준다. 이게 캐시(cache)다.

캐시 덕분에 첫 번째 요청은 2~3초 걸려도, 두 번째 요청부터는 수십ms 만에 응답한다. 서버를 시작할 때 미리 캐시를 채워놓으면 첫 방문자도 기다릴 필요가 없다.


무거운 계산은 서버 시작할 때 미리

시그널 페이지는 3,970종목의 1년치 주가를 전부 분석해서 52주 신고가, 골든크로스, 거래량 급증 등을 찾아낸다. 이 계산이 꽤 무거운데, 첫 방문자가 10초를 기다리게 할 수는 없다.

그래서 서버가 시작될 때 백그라운드에서 미리 계산을 돌린다. 서버가 준비되는 동안 캐시가 채워지고, 사용자가 접속했을 때는 이미 결과가 준비되어 있다.

퀀트 추천도 마찬가지다. 3개 전략의 추천 종목을 산출하는 데 각각 수 초씩 걸리는데, 서버 시작 시 미리 계산해놓는다.


보안과 안정성

요청 횟수 제한 (Rate Limit)

누군가 악의적으로 API를 초당 수백 번 호출하면 서버가 다운될 수 있다. 그래서 IP별로 분당 요청 횟수를 제한한다. 정상적인 사용에는 걸리지 않는 수준으로 설정했다.

특히 외부 API를 호출하는 경로(공시, 뉴스 등)는 더 엄격하게 제한한다. 외부 서비스에 과도한 요청을 보내면 우리 쪽이 차단당할 수 있기 때문이다.

요청 로깅

모든 API 요청을 로그로 남긴다. 어떤 경로가 몇 ms 걸렸는지, 에러가 났는지 기록한다. 느린 요청(2초 이상)이나 서버 에러(500)는 별도로 경고를 남겨서, 문제가 생기면 바로 알 수 있게 했다.

CORS 설정

브라우저 보안 정책상, 다른 도메인에서 API를 호출하면 차단된다. 우리 프론트엔드 도메인만 허용하도록 CORS(Cross-Origin Resource Sharing)를 설정했다.


구조 요약

백엔드 전체를 그림으로 그리면 이렇다:

브라우저 요청
    ↓
[Rate Limit] → 초과 시 차단
    ↓
[요청 로깅] → 시간/상태 기록
    ↓
[라우터 9개] → 담당 API로 분배
    ↓
[캐시 확인] → 있으면 즉시 반환
    ↓ (없으면)
[DB 조회] → SQLite에서 데이터 가져오기
    ↓
[실시간 시세 병합] → 장중이면 캐시된 실시간 데이터 합침
    ↓
JSON 응답

요청이 들어오면 보안 체크 → 라우터 분배 → 캐시/DB 조회 → 응답 순서로 흐른다. 단순한 구조지만 55개 API가 이 흐름을 공유한다.


삽질 기록

처음에는 파일 하나였다

초기에는 main.py 하나에 모든 API를 넣었다. 500줄이 넘어가니까 함수 찾기도 힘들고, 한 곳을 고치면 다른 데가 깨지기 시작했다. 라우터 분리를 하고 나서야 관리가 수월해졌다.

주기적으로 리팩토링이 필요한지 체크했는데 어느 규모 이상이 되니 AI가 리팩토링이 필요하다고 한다.

캐시 안 비웠더니

데이터를 교정한 적이 있는데, DB는 수정했는데 대시보드에는 여전히 옛날 데이터가 보였다. 서버 메모리에 캐시된 옛날 값이 남아있었기 때문이다. 캐시를 비우고 서버를 재시작해야 했다. 이후로는 데이터 수정 후 반드시 캐시를 초기화하는 습관이 생겼다.

첫 방문자 10초 대기

시그널 캐시를 미리 안 채워놨더니, 첫 방문자가 시그널 탭을 열었을 때 10초 넘게 로딩이 걸렸다. "페이지 로딩이 느리다"는 피드백을 줬더니 서버 시작 시 미리 캐시를 채우는 방식으로 바꿨다.

외부 API 장애 전파

실시간 시세를 가져오는 외부 API가 느려지면, 그 영향이 대시보드 전체로 퍼졌다. 외부 호출에 타임아웃을 짧게 걸고, 실패해도 DB 데이터(장 마감 종가)를 보여주도록 폴백 처리를 넣었다.


정리

  • FastAPI: 가볍고 빠른 Python 웹 프레임워크, 비동기 지원
  • 55개 API, 9개 라우터: 기능별로 분리해서 독립적으로 관리
  • 실시간 시세: 1분 주기 백그라운드 갱신, 완벽하진 않지만 실용적
  • 캐시: 한 번 계산한 결과를 재활용, 서버 시작 시 미리 채움
  • 보안: Rate Limit + 요청 로깅 + CORS

이제 데이터 수집(2편), DB(3편), 백엔드(4편)까지 완성했다. 남은 건 사용자가 실제로 보는 화면 - 프론트엔드다.


다음 편 예고

  • 5편: 모든 정보를 화면에서 보여주는 프론트엔드 만들기

 

+ Recent posts