포스트

2026-07-01-TIL: monticker 핫 패스와 풀 크기, 그리고 Claude Sonnet 5

요즘은 거의 monticker만 붙잡고 있었다. 처음에 만든 스켈레톤 위에 실제 데이터 연동, 인증, 차트, 모의 투자, 분석 기능이 빠르게 올라갔고, 시세 파이프라인도 한 번 다시 짰다. 중간에 spring-ops-lab에서 가상 스레드 실험도 하나 끝냈다.

최근에 한 작업

monticker: 실제 데이터와 화면

Mock으로만 돌던 부분을 실제 연동으로 바꿨다. 뉴스, 종목 마스터, 공시, 실시간 시세를 받는 수집기를 붙였고, API 키가 없으면 Mock으로 돌아가게 했다. JWT 인증, 폴링 대신 WebSocket, 알림의 실제 푸시 발송, Claude를 이용한 종목 요약과 뉴스 감성 분석도 이때 들어갔다.

화면 쪽에서는 차트 라이브러리를 Apache ECharts로 바꾸면서 어댑터를 사이에 두었다. 상승을 빨강, 하락을 파랑으로 쓰는 한국식 차트 테마도 넣었다. 상세 페이지가 깜빡이는 문제는 같은 데이터면 상태를 갱신하지 않게 하고, TanStack Query로 같은 요청을 하나로 합쳐서 잡았다. 그 위로 모의 투자, 백테스트, 포트폴리오 위험 지표 같은 분석 기능이 계속 늘었다.

monticker: Kafka, Go, Netty로 다시 짠 시세 파이프라인

Go로 만든 수집 게이트웨이가 종목마다 고루틴을 띄워 Kafka에 틱을 넣고, worker는 Kafka 컨슈머가 되어 감지한 이벤트를 다른 토픽에 낸다. Netty로 만든 브로드캐스트 게이트웨이가 두 토픽을 읽어 WebSocket 클라이언트에 바로 보낸다.

처음에는 메시지 버스를 Redis Streams로 하기로 했으니 그 결정을 뒤집은 셈이다. 다만 결정 기록에 동기가 부하가 아니라 증권사 시세 팀이 쓰는 기술을 직접 다뤄 보기 위해서라고 처음부터 밝혔다. 실제 운영 규모의 틱이 없기 때문이다. 기존 경로는 설정으로 남겨 두어 기본 개발 흐름은 그대로다.

spring-ops-lab: 가상 스레드 고정(pinning)

블로킹 호출 하나를 하는 엔드포인트 여러 개를 두고, 블로킹하는 동안 무엇을 쥐고 있는지만 다르게 했다. 가상 스레드를 켜자 synchronized 안에서 블로킹하는 엔드포인트만 처리량이 수십 분의 일로 떨어졌다. 경합이 없는 락인데도 그랬고, 핸들러 안에 동시에 들어와 있는 요청도 거의 하나로 줄었다. 블로킹하는 동안 가상 스레드가 캐리어 스레드를 놓지 못하는 고정 현상이다. 같은 경로를 ReentrantLock으로 바꾸면 차이가 없었다.

배운 점: 핫 패스에서 무엇이 기다리는가

monticker worker는 스케줄러 하나가 모든 종목의 틱을 순서대로 처리한다. Redis 쓰기, 틱 저장, 캔들 집계, 이벤트 감지와 저장이 한 스레드에서 이어진다. 문제는 이벤트를 저장한 직후 푸시 API를 동기 HTTP로 호출하던 부분이었다. 외부 호출 하나가 수백 ms씩 걸리면 종목 몇 개만으로도 한 사이클이 몇 초로 늘고, 다음 틱 수집이 그만큼 밀린다.

수정은 푸시 발송을 별도 스레드 풀로 넘기는 것이었다. 수집 스레드는 submit()만 하고 바로 돌아온다. 이벤트는 가끔 몰려서 오니 CachedThreadPool을 골랐고, 푸시 실패는 치명적이지 않아서 로그만 남긴다.

spring-ops-lab의 다른 실험이 같은 질문을 반대쪽에서 보여 줬다. 작은 커넥션 풀에 용량을 훨씬 넘는 부하를 걸었더니, 플랫폼 스레드든 가상 스레드든 성공 처리량은 풀 용량에서 같았다. 달라진 건 줄이 어디에 서느냐였다. 플랫폼 스레드는 Tomcat 스레드 수가 입구를 막아 서버 밖에 줄이 섰고, 가상 스레드는 요청을 전부 받아들인 뒤 풀 앞에서 커넥션 타임아웃까지 기다렸다. 풀 크기만큼의 세마포어를 앞에 두니 거절이 바로 나왔고 성공 요청의 지연도 내려갔다.

정리하면 스레드를 더 주거나 비동기로 넘기는 건 대기 위치를 옮기는 일이다. 푸시를 다른 풀로 넘긴 건 수집 루프가 외부 API를 기다리지 않게 했다는 점에서 맞는 수정이었다. 하지만 CachedThreadPool은 상한이 없어서, 푸시 서버가 느려지면 기다리는 스레드가 그만큼 늘어난다. 나중에 가상 스레드로 바꿀 때도 푸시 경로 안에 synchronized가 있는지부터 봐야 한다.

느낀 점

기능이 빠르게 늘어난 만큼 되돌아가는 일도 있었다. 다른 기능을 고치는 커밋에서 보안 설정의 빈과 JWT 필터가 두 번이나 빠져서 다시 복구했다. 파일을 크게 다시 쓰는 변경은 diff를 끝까지 읽지 않으면 이런 게 그대로 들어간다. 인증 설정처럼 빠지면 바로 티가 나야 하는 부분은 테스트로 묶어 두는 편이 낫겠다.

Kafka를 넣은 동기를 솔직하게 적은 것은 잘한 일 같다. 부하가 없는데 Kafka를 넣었다고만 쓰면 나중에 읽는 사람이 이유를 오해한다.

Claude Sonnet 5

이번 주에 Anthropic이 Claude Sonnet 5를 내놨다. Haiku와 Opus 사이의 중간급 모델인데, 회사는 “가장 에이전트다운 Sonnet”이라고 소개한다. 계획을 세우고 브라우저나 터미널 같은 도구를 쓰면서 여러 단계 작업을 끝까지 하는 쪽에 초점을 맞췄다는 말이다.

눈에 띄는 건 가격이다. 출시 기간 가격이 입력 100만 토큰당 2달러, 출력 10달러이고 이후 3달러, 15달러로 돌아간다고 한다. Anthropic은 많은 작업에서 최상위 모델인 Opus 4.8에 가까운 성능을 절반 이하 비용으로 낸다고 주장한다. 다만 TechCrunch가 옮긴 수치를 보면 에이전트 코딩 벤치마크에서는 아직 Opus 4.8보다 몇 포인트 낮다. 무료와 Pro 플랜의 기본 모델이 됐고, Claude Code에서도 사용량 한도가 늘었다.

에이전트는 한 작업에 모델을 여러 번 부르니, 성능이 조금 낮아도 싼 모델이 실제 비용을 크게 바꾼다. monticker의 종목 요약과 뉴스 감성 분석처럼 정해진 형식의 반복 작업이 그런 경우다.

참고:

다음에 확인할 것

  • worker의 scheduling.pool.size 설정이 실제로 적용되는지. Spring Boot의 스케줄러 풀 속성은 spring.task.scheduling.pool.size이고, 저장소 안에 앞의 키를 읽는 코드는 없다
  • 푸시 발송 풀에 상한을 두거나, 세마포어로 동시 발송 수를 제한하는 방식
  • Kafka 경로는 at-least-once라서 같은 이벤트가 다시 들어올 수 있다. stock_events 유니크 인덱스가 이 경우까지 막는지
  • 보안 설정이 다시 빠지지 않도록 테스트 추가
  • 종목 요약과 감성 분석을 Sonnet 5로 돌렸을 때 결과와 비용 비교
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.
고쳐 쓴 기록 1번 수정 ·
  1. docs(til): drop dates and internal codes and add ai news to the 2026-07-01 til

댓글

아직 댓글이 없습니다