2026-06-19-TIL: monticker 시작, 그리고 IMMUTABLE이 아닌 인덱스 식
이번 주에 monticker 저장소를 새로 시작했다. 가격 급등이나 거래량 급증 같은 이벤트를 실시간으로 감지해서 차트 타임라인과 모바일 알림으로 보여 주는 주식 관찰 앱이다. Claude Code의 /init으로 CLAUDE.md를 만드는 것부터 시작했고, 커밋 대부분을 Claude Sonnet 4.6과 같이 썼다.
이번 주에 한 작업
코드보다 문서를 먼저 썼다
처음 한 일은 제품 정의, 아키텍처, 데이터 모델을 문서로 정리하는 것이었다. 큰 결정 네 가지는 아키텍처 결정 기록으로 따로 남겼다.
- 마이크로서비스 대신 모듈러 모놀리스로 시작한다. API 서버 하나와 비동기 작업용 worker 하나로 나눈다.
- 가격 틱과 캔들은 TimescaleDB에 둔다. PostgreSQL 확장이라 마이그레이션 도구와 커넥션을 그대로 쓸 수 있다.
- 가격, 뉴스, 공시 등 출처와 상관없이 모든 이벤트를
stock_events한 테이블에 모은다. - 메시지 버스는 일단 Redis Streams로 하고, Kafka는 나중의 선택지로 남긴다.
결정마다 언제 다시 볼지 조건도 같이 적어 두었다.
스켈레톤에서 이벤트 타임라인까지
문서를 정리한 뒤로는 기능 단위 커밋이 빠르게 이어졌다. Spring Boot Kotlin 기반 API와 worker, Next.js 웹, Expo 모바일 스켈레톤을 세우고, Mock 시세 수집기와 거래량 급증, 가격 급등 감지기, 이벤트 타임라인 화면, 캔들 차트까지 붙였다. 알림 평가기는 규칙을 주기적으로 읽어 최신 가격과 비교하고, 같은 알림이 짧은 시간 안에 반복되지 않게 쿨다운을 둔다.
저장소 안에 Claude Code 설정도 같이 넣었다. 리뷰 관점별 서브에이전트, 위험한 명령을 막는 훅과 권한 목록, PR diff를 읽고 리뷰 코멘트를 다는 워크플로다.
처음 띄워 보니 바로 깨졌다
API를 처음 실제로 띄웠더니 마이그레이션에서 실패했다. 이벤트 중복 방지 인덱스가 PostgreSQL에 거부당했다. worker 쪽에서는 Jackson 자동 설정이 돌지 않아 ObjectMapper 빈이 없다는 오류가 났다.
배운 점: 인덱스 식은 IMMUTABLE이어야 한다
stock_events의 중복은 쓰기 시점에 막기로 했다. 같은 종목, 같은 이벤트 종류는 1분 안에 한 번만 기록한다. 처음 마이그레이션은 이랬다.
1
2
CREATE UNIQUE INDEX uq_stock_events_dedup
ON stock_events (stock_id, event_type, date_trunc('minute', event_time));
event_time은 TIMESTAMPTZ다. PostgreSQL은 인덱스 식에 쓰는 함수가 IMMUTABLE이어야 한다. 같은 입력이면 언제나 같은 결과가 나와야 인덱스에 저장한 값을 믿을 수 있기 때문이다. 그런데 date_trunc(text, timestamptz)는 STABLE이다. 자르는 기준이 세션의 TimeZone 설정에 따라 달라질 수 있어서다. 분 단위에서는 차이가 안 보여도, day로 자르면 서울 기준 자정과 UTC 기준 자정이 다르다.
수정은 타임존을 식 안에서 고정하는 것이었다.
1
2
CREATE UNIQUE INDEX uq_stock_events_dedup
ON stock_events (stock_id, event_type, date_trunc('minute', event_time AT TIME ZONE 'UTC'));
AT TIME ZONE 'UTC'를 거치면 타임존 없는 timestamp가 되고, 그 위의 date_trunc는 세션 설정에 의존하지 않으니 IMMUTABLE이다. 결과적으로 “UTC 기준 같은 분”이 중복 판정 단위가 된다.
이 인덱스는 애플리케이션 쪽 검사와도 관계가 있다. 이벤트를 쓰기 전에 같은 구간에 같은 이벤트가 있는지 SELECT로 먼저 확인하는데, 두 쓰기가 동시에 이 검사를 통과하면 둘 다 INSERT하려 한다. 그때 실제로 중복을 막는 건 유니크 인덱스다. 애플리케이션 검사는 불필요한 INSERT를 줄이는 정도의 역할이다.
느낀 점
스켈레톤부터 이벤트 타임라인까지 커밋은 금방 쌓였지만, 실제로 띄워 본 건 한참 뒤였다. 테스트와 CI 설정이 먼저 있었는데도 첫 실행에서 마이그레이션과 DI가 바로 깨졌다. 마이그레이션 SQL은 실제 PostgreSQL에 한 번 적용해 보기 전까지는 검증된 게 아니었다.
결정 기록을 먼저 써 둔 것은 수정할 때 기준이 됐다. 인덱스가 깨졌을 때 인덱스를 빼는 대신 식을 고쳤는데, 쓰기 시점 중복 제거가 필요하다는 이유가 이미 적혀 있었기 때문이다.
Claude Fable 5와 수출 통제
이번 주 AI 쪽에서 가장 시끄러웠던 건 Anthropic의 Claude Fable 5였다. 얼마 전 Fable 5와 Mythos 5를 같이 발표했는데, 둘은 같은 모델이다. Fable 5는 일반 공개용으로 사이버 보안, 생물/화학, 증류(다른 모델 학습용으로 능력을 뽑아 가는 것) 관련 요청을 분류기로 걸러서 Opus 4.8로 넘긴다. Mythos 5는 이 안전장치를 일부 푼 버전으로, 정부 보안 파트너 같은 제한된 곳에만 준다. 회사는 거의 모든 벤치마크에서 최고 성능이라고 밝혔고, API 가격은 입력 100만 토큰당 10달러, 출력 50달러다.
그런데 공개 며칠 만에 미국 상무부가 수출 통제 규정을 근거로 두 모델의 접근을 중단시켰다. 외국인의 접근을 막으라는 지시였는데, 국적을 실시간으로 가려낼 방법이 없어서 결국 전 세계 모든 사용자에게서 내려갔다. 정부는 탈옥 가능성을 이유로 들었고, Anthropic은 근거가 좁은 범위의 구두 주장뿐이었다는 입장이다. Opus 4.8 같은 다른 모델은 그대로 쓸 수 있다.
개발자 입장에서 보면 모델 성능보다 이쪽이 더 생각할 거리였다. API로 쓰던 모델이 규제 때문에 하루아침에 사라질 수 있다는 뜻이다. 모델 이름을 코드에 박아 두기보다 설정으로 빼고, 다른 모델로 바꿔도 돌아가게 해 두는 게 맞겠다.
참고:
- Claude Fable 5 and Claude Mythos 5 - Anthropic
- Fable 5 Suspension: Enterprise AI Under Export Controls - Cloud Security Alliance
다음에 확인할 것
- 이벤트 쓰기의 SELECT 후 INSERT를
INSERT ... ON CONFLICT DO NOTHING한 번으로 바꿀 수 있는지 - Claude Code 훅이 실제로 명령을 막는지. 훅이 입력을 어떻게 받는지 문서와 대조해 볼 것
- 알림 평가기의 Expo 푸시 발송 연결
- 나중에 LLM API를 붙이게 되면 모델 이름을 처음부터 설정으로 빼 둘 것
댓글
아직 댓글이 없습니다