2026-09-28-TIL: 통과하는 테스트가 무엇을 잡는가
지난 TIL 이후를 묶어서 정리한다. 기능을 새로 넣기보다 이미 만든 것을 검증하고, 처음 보는 사람이 읽을 수 있게 정리한 작업이 많았다. 코드 작업은 대부분 Claude Code 세션에서 진행했고, neon-diary만 Codex로 진행했다.
요즘 한 작업
proofu: 생성에서 제출, 내보내기까지
proofu는 이력서 생성 흐름의 뒷부분을 채웠다. 승인된 주장에서 문서 초안을 만들고, 모델은 허용된 참조만 인용할 수 있게 했다. 승인된 버전만 제출 스냅샷으로 얼리고, 여러 형식으로 내보낼 때는 렌더링한 파일에서 텍스트를 다시 추출해 원본과 맞는지 검증한 뒤에 저장한다. 문장 다듬기에는 의미를 바꾸거나 원문의 숫자를 빠뜨린 수정을 거부하는 가드를 붙였다. 그 위에 로그인, 계정 삭제, 데이터 내보내기 같은 운영에 필요한 기능도 올렸다.
작은 버그 하나가 기억에 남는다. 키셋 페이지네이션 커서를 UTC로 만들고 있었는데, Postgres의 cast(ts as date)는 세션 타임존을 쓴다. 그래서 한국 시간 새벽 시간대에는 목록의 두 번째 페이지가 비어 있었다.
워커 쪽도 손봤다. 작업 도중 워커가 죽으면 실행 중 상태로 남은 작업이 영영 다시 시도되지 않았다. lease가 지난 작업은 다시 큐에 넣게 하고, 모델 호출 시간에는 lease보다 짧은 상한을 걸었다. 호출이 lease보다 길어지면 같은 작업이 두 번 돌 수 있기 때문이다.
monticker: 보안 리뷰
영역별로 나눠 코드 수준 보안 리뷰를 하고 문서로 남겼다. 가장 심각한 것은 셋이었다.
- 어떤 배포 경로도 운영 프로파일을 켜지 않아서, 공개 저장소에 커밋된 개발용 JWT 비밀키와 암호화 키가 그대로 쓰일 상황이었다.
- 리프레시 토큰을 브라우저 저장소에 두고 있었다.
- 실계좌 주문 API에 입력 검증이 없었다.
기본 비밀키로는 서버가 뜨지 않게 했고, 리프레시 토큰은 HttpOnly 쿠키로 옮겼다. 토큰 재사용을 감지하면 세션 계열 전체를 폐기하게 했고, 주문은 브로커로 보내기 전에 검증하게 바꿨다. 현금 예약 락 전략도 실제 Postgres에서 비교했는데, 첫 측정은 커넥션 풀 없이 돌려서 전략 간 차이가 드러나지 않았다. 이 방법론 실수도 결정 기록에 같이 남겼다.
그 밖의 저장소
- code-drill: 프로젝트형 문제에 결함 검출 채점을 붙였다. 아래에 따로 정리했다. MinIO 이미지를 익명으로 받을 수 없게 된 것이 CI에서 드러나서 객체 저장소를 SeaweedFS로 옮겼다.
- parity-pay: README를 처음 보는 사람이 읽는 입구로 다시 쓰고, 실행과 데모 절차는 별도 문서로 뺐다.
- neon-diary: 첫 구간을 처음부터 끝까지 이어서 플레이하고, 저장한 뒤 새 프로세스에서 복원되는 것까지 확인했다.
배운 점: 통과하는 테스트가 무엇을 잡는가
code-drill의 프로젝트형 문제에는 패키지마다 참조 구현과 대표 오답(mutant)이 있다. 원래 오답은 숨은 테스트가 충분히 촘촘한지 확인하는 용도였는데, 이걸 사용자 쪽으로 돌렸다. 사용자가 쓴 테스트가 실제로 결함을 잡는지 재는 것이다.
규칙은 두 가지다.
- 사용자의 테스트는 참조 구현에서 통과해야 한다. 틀린 동작을 기대하는 테스트는 테스트가 아니다.
- 그 테스트를 오답 구현 위에서 돌렸을 때 오답의 절반 이상을 떨어뜨리면 성공이다.
첫 스모크에서 바로 구멍이 보였다. 사용자 테스트 파일에는 시작 저장소의 공개 테스트가 그대로 들어 있다. 그래서 사소한 테스트 하나만 더 써도, 공개 테스트가 원래 잡던 오답이 전부 사용자가 잡은 것으로 계산됐다.
고친 방법은 단순하다. 사용자 테스트로 떨어진 오답마다 공개 테스트만으로 한 번 더 돌린다. 그때도 떨어지면 “원래 잡히던 오답”으로 분류해 사용자의 몫에서 뺀다. 문제를 만들 때도, 공개 테스트가 못 잡는 오답이 최소 둘은 있어야 한다는 조건을 붙였다.
이건 뮤테이션 테스트의 기본 아이디어와 같다. 테스트가 통과했다는 사실만으로는 아무것도 알 수 없고, 일부러 틀린 구현을 넣어 봐야 그 테스트가 무엇을 잡는지 보인다. 그리고 점수를 매길 때는 기준선을 빼야 한다. 기존 테스트가 이미 하던 일까지 새 테스트의 성과로 세면 숫자는 좋아 보여도 아무것도 재지 못한다.
느낀 점
요즘 고친 것들은 대부분 “통과는 하는데 실제로는 아무것도 보장하지 않는” 경우였다. 테스트는 통과하는데 결함을 잡지 못했고, 설정 파일에는 운영 값이 있는데 어떤 배포 경로도 그 프로파일을 켜지 않았다. 측정은 돌아갔지만 커넥션 풀이 없어서 비교가 되지 않았다. 오답 위에서 다시 돌리기, 배포 경로를 실제로 따라가기, 측정 조건을 다시 보기처럼 확인 방법을 하나 더 붙이면 이런 것들이 보였다.
Claude Opus 5.5
얼마 전 Anthropic이 Claude Opus 5.5를 냈다. Claude 5.5 계열의 첫 모델이고, Claude Code에서도 바로 쓸 수 있다.
개발자 입장에서 눈에 띈 건 성능보다 가격 구조였다. 입력과 출력 토큰 단가는 이전 Opus보다 20% 내렸고, 캐시 읽기 단가는 60% 내렸다. Anthropic이 따로 낸 글의 설명으로는 코딩 세션이 길어지면서 요청마다 실어 보내는 컨텍스트가 크게 늘었고, 에이전트 작업에서는 출력보다 이미 본 컨텍스트를 다시 읽는 비용이 대부분을 차지한다고 한다. 그래서 캐시 읽기를 싸게 하고, Claude Code 쪽에서도 캐시가 의도치 않게 깨지는 경우를 줄였다는 것이다.
같은 작업을 더 적은 단계로 끝낸다거나, 큰 코드 마이그레이션을 하루 만에 끝냈다는 사례는 회사와 초기 테스터가 밝힌 주장이다. 내 쪽에서 확인할 수 있는 건 세션이 길어질수록 캐시 적중이 비용을 좌우한다는 점이다. 컨텍스트를 자주 갈아엎는 작업 방식은 모델을 바꿔도 비싸다는 뜻이기도 하다.
참고:
- Introducing Claude Opus 5.5 - Anthropic
- Coding sessions are longer and use more context. Claude Opus 5.5 is built with that in mind. - Claude Blog
다음에 확인할 것
- proofu 로컬 스택을 명령 하나로 띄우고 점검하는 스크립트
- monticker 보안 리뷰에서 남은 항목과 배포 전 후속 작업
- code-drill 결함 검출 문제에서 오답 개수와 난이도의 관계
- neon-diary 다음 구간의 남은 경로
댓글
아직 댓글이 없습니다