DB에 데이터를 저장할 수 있는 기반을 만들었을 때는 큰 산을 넘었다고 생각했다. 기존에는 API에서 데이터를 받아 메모리에 올리고 화면에 보여줬다. 이제 같은 데이터를 DB에도 쌓을 수 있게 됐으니, 화면에서 DB를 먼저 읽도록 연결하면 될 것 같았다. 적어도 처음에는 그게 제일 쉬운 일인 줄 알았다.
DB에 데이터가 있으면 먼저 보여주고,
부족하거나 오래된 데이터만 API에서 다시 받으면 되지 않을까?
문제는 여기서 말한 “부족하다”와 “오래됐다”를 정확히 정의하지 않았다는 것이다. 구현을 시작하고 실제 화면에서 사용해보니 날짜와 시간에 따라 예상하지 못한 상황이 하나씩 나타났다. 그때마다 눈앞의 문제를 해결하는 조건을 추가했다. 각 수정만 놓고 보면 대부분 이유가 있었다. 문제는 전체 요구사항을 먼저 정하지 않은 상태에서 그런 조건들이 계속 쌓였다는 것이다.
종가 확정 시점은...?
가장 먼저 헷갈린 건 Daily 데이터였다. 오늘 날짜의 일봉이 DB에 있다고 해서 그 값이 하루가 끝난 뒤의 종가라는 보장은 없었다. 장중에 조회하면 같은 날짜의 시가, 고가, 저가, 종가와 거래량은 계속 변한다. 그렇다면 오늘 row가 있으면 최신이라고 봐도 되는지, 장이 끝난 뒤에는 언제부터 확정된 값이라고 판단해야 하는지부터 결정해야 했다. 처음에는 장 종료 시각이나 특정 cutoff를 이용해 finality를 판단하는 방향을 검토했다. 하지만 이 접근은 곧 다른 문제를 불러왔다.
달력과 거래일 사이
주말이 끼면 DB의 마지막 거래일과 오늘 날짜가 당연히 달라진다. 평일이라고 항상 거래일인 것도 아니며 공휴일과 휴장일도 있다. DB가 오늘 날짜까지 채워져 있지 않다는 이유만으로 데이터가 부족하다고 판단하면 정상적인 휴장 구간에서도 다시 데이터를 받게 된다. 그래서 주말 경계를 보정하고, 평일인데 다음 거래일이 늦게 나타나는 경우를 처리하고, coverage가 충분한지를 판단하는 조건을 추가했다.
버그 하나를 고치면 다른 날짜 경계가 나타났다. 그 문제를 고치면 또 다른 예외가 생겼다. 결과적으로 MyStock은 거래소 달력을 완벽하게 알지도 못하면서 거래일과 데이터 확정 시점을 추론하려고 하고 있었다.
버그 수정이 다른 버그 생성
문제가 발생할 때마다 Codex로 코드를 추적하고 테스트를 추가해 수정했다. 주말에 반복 fetch가 발생하면 주말 조건을 고쳤고, 평일의 non-trading boundary에서 같은 문제가 나타나면 다시 조건을 보완했다. 테스트는 통과했고 발견한 버그도 하나씩 없어졌다. 그런데 코드는 점점 많은 상황을 처리하고 있었지만, 정작 “MyStock이 Daily 데이터를 어떻게 관리해야 하는가”를 한 문장으로 설명하기는 점점 어려워졌다.
버그가 많아서 복잡해진 것이 아니라, 정확한 요구사항을 먼저 결정하지 않은 채 발생한 버그만 계속 수정하고 있었던 것이 문제였다.
우리가 원하는 건 무엇이었나
다시 원점으로 돌아가 MyStock에서 필요한 것은 거래소의 모든 휴장일을 추론하는 것도 아니었고, 특정 시각을 기준으로 “이 일봉은 이제 영원히 확정됐다”고 인증하는 것도 아니었다. 내가 원한 건 훨씬 단순했다. 앱을 실행하거나 사용자가 새로고침했을 때 최근 데이터가 바뀌었는지 provider에게 다시 확인하고, 실제로 받은 값으로 DB를 갱신하면 됐다.
이렇게 요구사항을 다시 정의하자 장 종료 시각, finality cutoff, 오늘이 실제 거래일인지에 대한 많은 추론이 핵심 요구사항에서 빠졌다.
최근 14일을 다시 확인한다
Domestic Daily의 routine synchronization은 Startup과 User Refresh라는 이벤트를 기준으로 동작하도록 정리했다. 이미 historical data가 있는 종목이라면 이벤트가 발생할 때 최근 14 calendar-day lookback 구간을 provider에게 다시 요청한다. 예를 들어 기준일이 9월 9일이면 8월 26일부터 9월 9일까지를 요청하는 방식이다.
중요한 건 14 거래일을 계산하는 것이 아니라 달력 기준으로 최근 구간을 다시 확인한다는 점이다. Provider가 실제로 반환한 거래일 row만 검증해서 저장한다. 주말이나 휴장일 데이터를 MyStock이 임의로 만들지 않는다. 같은 날짜의 값이 이미 있다면 최신 provider 결과로 갱신하고, provider가 반환하지 않은 날짜를 억지로 채우지도 않는다. 오늘 날짜의 값도 한 번 저장됐다는 이유만으로 불변의 확정값이라고 보지 않는다. 이후 Startup이나 Refresh가 발생하면 최근 구간을 다시 조회하면서 provider 결과에 수렴하도록 했다.
결국 복잡한 거래일 추론 대신 “최근 구간은 다시 물어본다”는 단순한 규칙으로 돌아왔다.
DB-first와 동기화는 다르다
이 과정에서 하나 더 분명해진 것이... DB-first는 화면에서 무엇을 먼저 보여줄 것인가에 대한 문제이고, synchronization은 provider에게 언제 다시 확인할 것인가에 대한 문제였다. DB에 usable한 데이터가 있으면 화면은 그 데이터를 먼저 사용할 수 있다. 그렇다고 같은 날짜에 이미 성공한 적이 있다는 이유만으로 이후 Startup이나 User Refresh의 provider 확인까지 생략하면 안 된다.
반대로 provider 확인이 진행 중이라고 기존 화면을 비워둘 이유도 없다. 마지막으로 정상 저장된 데이터를 먼저 보여주고, 새로운 결과가 들어오면 갱신하면 된다.
ETF에서도 날짜를 의심했다
Daily에서 날짜 때문에 한참 돌아온 뒤 ETF 구성종목을 DB에 쌓기 시작하면서는 요청 날짜를 그대로 믿지 않게 됐다. 실제로 provider마다 상황이 달랐다. KODEX, KoAct, PLUS는 historical snapshot을 저장할 근거를 확인할 수 있었고, TIGER는 요청 날짜와 실제 구성 기준일이 다를 수 있어 공식 metadata의 basis date를 확인한 뒤 historical snapshot으로 저장했다.
RISE는 현재 구성종목과 편입비중은 조회할 수 있었지만 historical response에서 authoritative basis date를 증명할 수 없어서 요청 날짜를 과거 기준일인 것처럼 만들어 저장하지 않았다. 현재 조회가 가능하다는 것과 과거 어느 날짜의 canonical snapshot인지 증명할 수 있다는 것은 다른 문제였다. Daily에서 한 번 크게 돌아온 덕분에 ETF에서는 적어도 날짜를 먼저 의심하게 됐다.
문제는 코드보다 정확한 요구 명세
이번 작업에서 가장 크게 바뀐 건 코드보다 문제를 보는 순서였다. 처음에는 화면에서 이상한 동작을 발견하면 그 동작을 없애는 조건부터 생각했다. 그러다 보니 각 버그는 해결돼도 전체 정책은 계속 복잡해졌다. 결국 먼저 정해야 했던 건 “이 상황에서는 어떤 코드가 맞는가”가 아니라 “MyStock은 어떤 데이터를 언제 다시 확인해야 하는가”였다. 요구사항을 먼저 정하고 나니 코드가 판단해야 할 것도 줄었다.
물론 이것으로 DB 작업이 모두 끝난 것은 아니었다. 실제로 사용해보니 시장현황에 들어갈 때 여전히 긴 historical 데이터를 다시 받는 것 같은 느낌이 있었고, 투자분석에서 종목을 바꿀 때도 Daily나 수급, 매매시점 관련 데이터를 다시 가져오는 것처럼 보였다. 이번에는 바로 코드를 고치지 않기로 했다. DB에 최초 baseline이 제대로 구축되지 않아서 발생하는 정상적인 조회인지, 아니면 DB가 있는데도 기존 provider 중심 lifecycle이 남아 있는 것인지부터 확인해야 했다.
같은 실수를 또 반복하지 않으려면, 이번에는 수정하기 전에 요구사항과 현재 상태부터 확인해야 했다.