시리즈: 토스 자동매매 개발기 (Phase 1-2 / 총 4편)
- Phase 0 — 기반 시스템 구축
- Phase 0-1 — 통화 표시 버그 수정
- Phase 1 — 설정 분리 & 재시도 로직 (1)
- Phase 1-2 — 설정 분리 & 재시도 로직 통합 (현재 글)
이전 글: Phase 1 – 설정 분리 & 재시도 로직 · 다음 글: 발행 예정
이번 Phase에서 한 일
Phase 1 로드맵 중 남은 두 항목을 마저 처리했다. 하나는 여기저기 흩어져 있던 설정값을 config.py 하나로 몰아넣는 작업, 다른 하나는 네트워크 오류 재시도 로직을 데코레이터로 통합하는 작업이었다.
1. config.py로 설정 중앙화
기존에는 종목코드, 감시 주기, 임계값 같은 값들이 get_price.py, auto_trader.py, toss_common.py 여기저기에 하드코딩돼 있었다. 그걸 전부 config.py 하나로 모았다. TARGET_STOCKS, MONITOR_INTERVAL, ERROR_BACKOFF_INTERVAL, MAX_CONSECUTIVE_ERRORS, DEFAULT_TIMEOUT 같은 값은 물론이고, DRY_RUN과 목표가 같은 안전장치 값도 함께 옮겼다.
이 안전장치 값들은 옮기는 과정에서 값이 바뀌면 안 되는 것들이라 옮긴 뒤에 다시 한 번 값을 확인했다. 그리고 가상 종목(삼성전자 005930)으로 get_price.py를 실행해 시세가 정상적으로 조회되는지 확인하고 넘어갔다.
2. 재시도 로직 데코레이터로 통합
토큰 발급, 계좌 조회, 시세 조회, 보유 종목 조회 — 이 네 군데에 거의 똑같은 재시도 코드가 복붙돼 있었다. 그래서 데코레이터 하나로 뽑아냈다.
def retry_on_network_error(max_retries=None, backoff=None):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
retries = max_retries or config.MAX_CONSECUTIVE_ERRORS
wait = backoff or config.ERROR_BACKOFF_INTERVAL
for attempt in range(retries):
try:
return func(*args, **kwargs)
except (requests.Timeout, requests.ConnectionError) as e:
logging.warning(f"[재시도 {attempt+1}/{retries}] {e}")
time.sleep(wait)
logging.error("재시도 초과, 기본값 반환")
return None
return wrapper
return decorator
중요한 건 이걸 어디에 적용하고 어디에 적용하지 않을지였다. 토큰 발급이나 계좌 조회처럼 실패하면 안 되는 호출에는 데코레이터를 적용했지만, auto_trader.py의 감시 루프 안에 있는 시세 조회 함수에는 일부러 적용하지 않았다. 감시 루프 자체가 이미 일정 주기로 계속 재시도하는 구조라, 여기에 즉각 재시도까지 겹치면 오류 상황에서 오히려 요청이 몰리는 문제가 생길 수 있었다. 그래서 “즉각 재시도”와 “장기 백오프”를 계층으로 나눠서 역할을 분리하는 쪽으로 정리했다.
삽질 기록: 통화 분리 기능을 날려먹을 뻔한 이야기
재시도 로직 작업 중에 사고가 하나 있었다. 며칠 전에 고쳐놨던 toss_portfolio.py의 원화/외화 분리 표시 기능이 어느 순간 사라져 있었던 것.
원인을 추적해보니, 작업을 시작할 때 최신 로컬 파일이 아니라 예전 버전 파일을 기준으로 코드를 이어붙인 게 문제였다. 즉 내가 최신인 줄 알고 작업한 파일이 사실은 구버전이었던 거다. 다행히 실제로 원화만 표시되는 실행 결과를 보고 바로 이상하다는 걸 눈치챘고, 큰 사고로 번지기 전에 잡았다.
복구한 뒤에는 가상 테스트 데이터로 함수를 직접 실행해서 통화 분리가 다시 정상 동작하는 걸 확인했다. 그리고 재발 방지책도 하나 정했다. 이제 파일을 교체하는 작업을 할 때는 작업 전에 grep(또는 Select-String)으로 핵심 함수명이 파일 안에 실제로 있는지부터 확인하기로 했다.
Phase 1-2에서는 설정값을 config.py로 중앙화하고, 네트워크 재시도 로직을 데코레이터 하나로 통합했다. 작업 중 파일 버전 불일치로 통화 분리 기능이 잠깐 유실됐지만 조기에 발견해 복구했고, 이 경험을 계기로 파일 교체 전 함수 존재 여부를 먼저 확인하는 절차를 도입했다.
다음 계획
Phase 1의 남은 항목은 로그 파일이 무한정 커지지 않게 RotatingFileHandler를 도입하는 것과, pytest 기반 단위 테스트를 붙이는 것이다. 이번에 겪은 회귀 사고가 사실 pytest가 왜 필요한지를 보여주는 딱 좋은 사례라, 다음 글에서 자연스럽게 이어서 다룰 예정이다.
FAQ
Q1. 왜 모든 함수에 재시도 데코레이터를 적용하지 않았나요?
감시 루프처럼 이미 자체적으로 주기적 재시도가 이뤄지는 곳에 즉각 재시도까지 겹치면, 오류 상황에서 요청이 한꺼번에 몰릴 수 있기 때문이다. 즉각 재시도와 장기 백오프의 역할을 분리해서 관리하는 쪽을 택했다.
Q2. 회귀 사고는 어떻게 방지하나요?
파일을 교체하는 작업을 할 때 grep이나 Select-String으로 핵심 함수명이 실제로 파일에 존재하는지 먼저 확인하는 절차를 도입했다. 근본적으로는 pytest 단위 테스트를 붙여서 이런 회귀를 코드 실행 전에 잡는 게 다음 목표다.
Q3. config.py로 옮긴 안전장치 값(DRY_RUN 등)은 안전한가요?
값 자체는 옮기는 과정에서 절대 바꾸지 않았고, 이동 후 다시 한 번 값을 확인했다. 실제 매매 전 3중 안전장치 원칙은 이번 리팩터링에서도 그대로 유지된다.
“재시도 로직 후속 보완 트러블슈팅 | 토스 개발기 #1-2”에 대한 4개의 생각