SQLite best-effort 로깅으로 DB 실패 방어하기 | 토스 개발기 #3-2

지난 글에서 스키마와 인터페이스만 확정해두고 코드는 안 짰었는데, 이번엔 진짜로 db.py를 만들었다. 설계랑 구현 사이에 하루이틀 텀을 두니까 오히려 더 깔끔하게 짜지더라.

이번에 한 작업

지난 설계 문서 기준으로 get_connection(), init_db(), insert_trade(), insert_alert(), insert_price_snapshot() 다섯 개 함수를 그대로 구현했다. config.py에는 DB_PATH 설정값을 추가해서 DB 파일 경로도 하드코딩 안 하게 뺐다.

핵심 설계 원칙: “최선노력(best-effort)” 기록

제일 신경 쓴 부분은 DB 기록 실패가 매매 자체를 막으면 안 된다는 거였다. 그래서 모든 insert 함수 내부에서 sqlite3.OperationalError를 잡아서, 실패해도 예외를 위로 전파하지 않고 경고 로그만 남기고 조용히 넘어가게 짰다. 원래 있던 텔레그램 알림 전송부랑 같은 철학이다 — 알림이 실패한다고 매매 로직이 죽으면 안 되니까.

def insert_trade(conn, trade_data: dict) -> None:
    try:
        cursor = conn.cursor()
        cursor.execute(
            "INSERT INTO trades (symbol, side, price, quantity, is_dry_run, strategy_name) "
            "VALUES (?, ?, ?, ?, ?, ?)",
            (trade_data["symbol"], trade_data["side"], trade_data["price"],
             trade_data["quantity"], trade_data["is_dry_run"], trade_data["strategy_name"])
        )
        conn.commit()
    except sqlite3.OperationalError as e:
        logger.warning(f"거래 기록 실패, 매매는 계속 진행: {e}")

is_dry_run 컬럼으로 실전 거래랑 시뮬레이션 거래를 같은 테이블에 넣어서 구분한다. 예를 들어 가상 종목 삼성전자(005930)를 시뮬레이션 모드로 매수했다면 is_dry_run=1로, 실전 매수면 0으로 남는 식이다.

검증: 인메모리 테스트 → 실제 파일 테스트

1차로 sqlite3.connect(':memory:')로 실제 파일을 안 건드리는 인메모리 DB에서 12개 테스트를 짰다. 정상 기록, is_dry_run True/False 저장값 확인, 해외주식 소수점 가격이 안 깨지는지, DB 오류 상황에서 예외가 안 새는지까지 확인했다.

근데 인메모리 테스트만으로는 파일 I/O 관련 이슈를 놓칠 수 있어서, 2차로 실제 .db 파일을 만들어서 초기화 → 기록 → 조회까지 직접 돌려봤다. 전체 pytest를 다시 돌리니 기존 46개 + 신규 12개, 총 58개 전부 통과했다.

삽질 기록: 테스트 코드의 컬럼 인덱스 실수

처음 테스트를 돌렸을 때 test_정상_거래_기록 케이스에서 AssertionError가 터졌다. db.py 코드가 문제인 줄 알고 한참 들여다봤는데, 알고 보니 원인은 완전히 다른 데 있었다.

테스트 코드에서 cursor.execute("SELECT * FROM trades")로 가져온 row를 인덱스로 까서 검증하고 있었는데, strategy_name 컬럼이 6번째 자리에 있다고 착각하고 row[5]로 접근했다. 실제로는 7번째 컬럼(row[6])이라서, 엉뚱하게 가격 값이랑 비교하다가 계속 실패한 거였다. 구현 코드(db.py)는 처음부터 멀쩡했고, 테스트 코드 쪽 인덱스 계산 실수였다는 걸 확인한 다음엔 테스트만 수정해서 바로 통과시켰다.

이런 식으로 “구현 문제냐 테스트 문제냐”를 먼저 명확히 나누고 들어가는 습관이 없었으면 db.py를 괜히 더 뜯어봤을 것 같다.

다음 단계

db.py 모듈 자체는 완성돼서 독립적으로는 잘 돌아가는 걸 확인했다. 근데 아직 auto_trader.py, toss_portfolio.py랑은 연동이 안 돼 있어서, 지금 당장 실제 매매가 일어나도 DB에 자동으로 기록되진 않는다. 다음 글에서는 execute_order()send_notification()이 각각 insert_trade(), insert_alert()를 호출하도록 붙이는 연동 작업을 다룰 예정이다. 그거까지 끝나면 Phase 3 완료고, Phase 4(백테스팅)에서 이 데이터를 실제로 써먹어볼 계획이다.

※ 본문에 언급된 종목명·금액은 마스킹 처리된 가상 예시이며, 실제 보유 종목·금액과 무관합니다.

“SQLite best-effort 로깅으로 DB 실패 방어하기 | 토스 개발기 #3-2”에 대한 2개의 생각

댓글 남기기