세션 기록을 통째로 파싱하지 않고, NIO RandomAccessFile로 파일 끝에서부터 필요한 레코드까지만 역순으로 읽는다. 읽을 상한(DEFAULT_MAX_BYTES)은 상한별 소요 시간과 정보 손실을 표로 뽑아 정했다
여러 Claude Code 세션 중 답변 대기·멈춤 세션을 한 화면과 메뉴바에 보여주는 로컬 대시보드입니다.
과제
- 세션 목록 CLI는 작업 내용·브랜치·답변 대기 여부를 알려주지 않음
- 기획 시점 실측: 살아 있는 세션 10개 중 7개가 답변 대기, 1개는 27일째 방치
담당 범위
- 수집·상태 판별: Java / Spring Boot, 세션 기록 역순 읽기 (1인)
- 실시간 화면: SSE + Vue 3 대시보드, macOS 메뉴바 앱(SwiftUI)
- 배포·CI: launchd, 로컬 전용 바인딩 검사 게이트
설계 판단
- 세션 기록을 끝에서부터 필요한 만큼만 읽음 → 파일이 커져도 속도가 거의 일정 (대안: 전체 파싱)
- 상태를 레코드 타입 대신 메시지 블록으로 판정 → 도구 결과가 사용자 입력으로 기록되는 함정 회피
- 외부 전송 없이 로컬에서만 동작하고 CI로 강제 → 세션 기록의 프롬프트·경로 보호
결과 지표
- 역순 읽기 96ms vs 전체 파싱 1,684ms (17.5배, 기록 421MB 기준)
- 테스트 54개, 백엔드 1,730줄 대비 테스트 1,238줄
- 이틀 동안 23커밋, 단독 작업
원문 자세히 보기
과제
여러 프로젝트에서 Claude Code 세션을 동시에 띄워두고, 컨텍스트가 차면 새 세션을 여는 방식으로 쓰다 보니 세션이 계속 쌓였다. claude agents --json은 살아있는 세션 목록은 주지만 무슨 작업 중인지·어느 브랜치인지·내 답을 기다리는지는 알려주지 않는다. 그 정보는 세션 기록 파일에 따로 있다. 이 둘을 이어서 '지금 내 답을 기다리는 세션'과 '멈춰 있는 세션', '컨텍스트가 얼마나 찼는지'를 한 화면에 놓는 것이 목표였다. 기획 시점 실측으로 살아있는 세션 10개 중 7개가 답변 대기 상태였고, 한 세션은 27일째 떠 있었다. 만들고 끝낸 도구가 아니라 2026-09-04 확인 기준 매일 사용 중이며, 쓰면서 나온 불편을 계속 반영하고 있다.
담당 범위
Java 1,730줄 + 프론트 1,697줄 + Swift 894줄 · 테스트 54개 · 문서 2,332줄1인 개발. (1) 데이터 수집 — claude agents --json을 ProcessBuilder로 실행해 살아있는 세션 목록을 얻고, 세션 기록 JSONL을 역순으로 읽어 제목·마지막 발화·컨텍스트 사용량을 붙이는 수집 계층, (2) 상태 판별 — 레코드 타입이 아니라 message.content의 블록(tool_use / tool_result)까지 봐서 '답변 대기 / 작업 중 / 멈춤 의심'을 가르는 순수 도메인 로직, (3) 성능 — 대용량 기록을 통째로 파싱하지 않고 NIO RandomAccessFile로 파일 끝에서부터 필요한 레코드까지만 읽는 역순 리더와 상한 튜닝, (4) API — @Scheduled 주기 수집 + SseEmitter 실시간 스트림, record DTO, (5) 프론트 — Vue 3 + Vite + Tailwind로 프로젝트별 그룹 화면과 SSE 구독 훅, (6) macOS 메뉴바 앱 — SwiftUI로 답변 대기 수를 메뉴바에 띄우고 SSE를 구독, 백엔드가 죽으면 되살리는 클라이언트, (7) 배포 — 빌드 스크립트와 launchd 에이전트, (8) CI — 빌드·테스트에 더해 '바인딩이 로컬 전용 바인딩인가', '배포 산출물에 세션 데이터가 섞이지 않았는가'를 검사하는 게이트.
선택안 · 대안 · 근거 · 비용
세션 상태를 레코드 타입(user/assistant)이 아니라 message.content의 블록까지 보고 판정한다 — assistant이면서 tool_use면 작업 중, text로 끝나면 답변 대기, user이면서 tool_result면 작업 중
type: "user"로 기록된다. 이걸 사용자 입력으로 오인하면 '작업 중'과 '답변 대기'가 정확히 뒤바뀌는데, 이 도구의 존재 이유가 바로 그 둘을 가르는 것이라 판정이 틀리면 도구 전체가 무의미해진다. 함정이 user·assistant 양쪽에 대칭으로 있다.비용판정이 기록 파일의 내부 포맷에 묶였다. Claude Code가 레코드 구조를 바꾸면 조용히 틀린 상태를 보여주게 되고, 컴파일도 테스트도 그걸 잡아주지 못한다(형태가 바뀌어도 JSON 파싱은 통과한다). 그래서 판정부를 외부 의존 없는 순수 로직으로 떼어 JUnit으로 고정해 두었지만, 그건 '규칙이 유지되는 한' 맞다는 보장일 뿐이다.중앙 서버·계정·외부 전송을 두지 않고 백엔드를 로컬 전용 바인딩에만 바인딩한다. 그리고 그 원칙을 문서가 아니라 CI 게이트로 검사한다 — '바인딩이 로컬 전용 바인딩인가', '배포 산출물에 세션 데이터가 섞이지 않았는가'
모두 재현 명령과 함께 표기
./gradlew test --tests '*ReaderBenchmark*' -Dbenchmark=true (※ `-Dbenchmark=true` 없이 돌리면 @EnabledIfSystemProperty 때문에 skipped 2로 조용히 건너뛴다)
위와 같은 명령의 '상한별_시간과_정보손실' 테스트 출력
git log --oneline | wc -l → 23 / git log --format='%an' | sort | uniq -c
git ls-files 'src/main/**/*.java' | xargs wc -l | tail -1 → 1730 / git ls-files 'src/test/**/*.java' | xargs wc -l | tail -1 → 1238
grep -rhoE '@Test' src/test | wc -l → 54
git ls-files 'frontend/src/**' | grep -E '\.(ts|vue)$' | xargs wc -l | tail -1 → 1697 / git ls-files 'frontend/**' | grep -c 'spec.ts' → 5
git ls-files 'macos/**/*.swift' | xargs wc -l | tail -1 → 894
cat docs/*.md | wc -l → 2332 / ls docs/*.md | wc -l → 8
grep -n 'name:' .github/workflows/ci.yml