🌐 한국어 (현재 문서) · English
Java 소스 파일과 메서드를 복합 핫스팟 점수(Composite Hotspot Score) 로 순위화합니다. 최근성 가중 변경 횟수, 인지 복잡도, 커버리지 공백을 결합해 역사적 증거상 버그가 가장 많이 살 만한 곳에 테스트 노력을 집중하게 합니다. Adam Tornhill의 Your Code as a Crime Scene 방법론에 기반합니다.
로컬 git 저장소를 분석해 파일과 메서드에 점수를 매기고 CSV / YAML / Markdown / HTML 리포트를 내보내는, 완전히 연결된 CLI입니다.
이 저장소는 Claude Code 플러그인 마켓플레이스이기도 합니다. 번들된
hotspot-analysis 스킬이 에이전트에게 CLI를 받아·설정·실행·해석하는 방법을
가르칩니다(릴리스 jar 자동 다운로드, 빌드 불필요). Claude Code 안에서:
/plugin marketplace add baekchangjoon/hotspot-analysis
/plugin install hotspot-analysis@hotspot-analysis
/reload-plugins
이후 "이 Java 저장소의 핫스팟 찾아줘" 처럼 요청하면, 모델이 스킬을 호출해 아래 워크플로를 수행합니다.
Agent Skills 호환 에이전트(Claude Code, Claude.ai, Cursor, Gemini CLI 등)는 스킬 폴더만 직접 복사해 쓸 수 있습니다:
git clone https://github.com/baekchangjoon/hotspot-analysis
cp -r hotspot-analysis/skills/hotspot-analysis ~/.claude/skills/스킬의
scripts/get-jar.sh가 릴리스 jar를 자동으로 내려받으므로 빌드는 필요 없습니다(JDK 21 런타임만 필요). JDK 없이 쓰려면 아래 빠른 시작의 Docker 경로를 쓰세요.
- 두 가지 단위 — 파일 단위와 메서드 단위 핫스팟, 점수 기준 정렬.
- 두 가지 VCS 소스 — 로컬 git 작업 트리(JGit) 또는 원격 GitHub(kohsuke
github-api, WireMock으로 hermetic 테스트). - Java 21 파싱 — JavaParser 3.26이 record, sealed 타입, switch 식, 패턴 매칭을 이해.
- YAML 설정 — 강타입, Jakarta Bean Validation 검증, 환경변수 치환.
- 네 가지 출력 형식 — CSV(엑셀 친화), YAML(기계 판독), Markdown(PR 친화), HTML(브라우저에서 바로, 정렬·필터 가능, XSS 안전, 다크모드 대응).
- CI 자체 분석 데모 — 매 CI 실행마다 다운로드해 브라우저로 열 수 있는
hotspot-demo-report-<N>아티팩트 생성. - 모든 계층의 포괄적 테스트 — 계약 테스트 + 단위 + Spring Boot E2E, 실패 0.
| 용도 | 요구사항 |
|---|---|
| 빌드 | PATH에 JDK 17+ (Gradle이 Java 21 툴체인을 자동 프로비저닝) |
| jar 실행 | PATH에 JDK 21+ — java -version으로 확인 |
| 분석 대상 | .git/ 폴더가 있는 디렉터리(실제 git 작업 트리) |
jar를 직접 받는 대신, 설치 스크립트가 최신 릴리스 jar를 내려받고 PATH에
hotspot 래퍼를 설치합니다. 이후 java -jar hotspot.jar ... 대신 hotspot ...로
실행합니다(JDK 21 런타임 필요):
curl -fsSL https://raw.githubusercontent.com/baekchangjoon/hotspot-analysis/main/install.sh | bash~/.local/share/hotspot/hotspot.jar에 jar를, ~/.local/bin/hotspot에 래퍼를
설치합니다(재실행해도 안전). ~/.local/bin이 PATH에 없으면 스크립트가 추가
방법을 안내합니다. 설치 위치·버전은 환경변수로 바꿀 수 있습니다(HOTSPOT_PREFIX,
HOTSPOT_VERSION). 설치 후:
hotspot analyze # 현재 디렉터리 (zero-config)JDK 21 런타임이 필요합니다. 설치하기 어렵다면 아래 Docker 경로를 쓰세요. (Homebrew tap은 추후 제공 예정 — 그때까지는 위 스크립트를 권장합니다.)
아래는 jar를 직접 받아
java -jar로 실행하는 수동 경로입니다. 위 한 줄 설치를 쓴다면 예시의java -jar hotspot.jar를hotspot으로 바꿔 읽으세요.
최신 Release에서 self-contained 실행 jar를 내려받습니다(JDK 21 런타임만 있으면 됨):
curl -fsSL https://github.com/baekchangjoon/hotspot-analysis/releases/latest/download/hotspot.jar -o hotspot.jar소스에서 빌드하려면(선택):
./gradlew bootJar→build/libs/hotspot-*.jar. 아래 예시의hotspot.jar를 그 경로로 바꾸세요.
JDK 없이 — Docker. 분석 대상 저장소를
/work에 마운트해 실행합니다. 설정 파일 없이 바로(zero-config):docker run --rm -v "$PWD":/work ghcr.io/baekchangjoon/hotspot-analysis:latest analyze설정 파일로 세밀하게 제어하려면:
docker run --rm -v "$PWD":/work ghcr.io/baekchangjoon/hotspot-analysis:latest \ analyze --config /work/hotspot.yml
설정 파일을 만들기 전에, 저장소 루트에서 바로 실행할 수 있습니다. git 루트· 모듈 레이아웃(단일/멀티)·JaCoCo 리포트·Spring API 분석을 자동 감지합니다:
java -jar hotspot.jar analyze # 현재 디렉터리
java -jar hotspot.jar analyze /path/to/repo감지 결과를 설정 파일로 저장해 손보려면:
java -jar hotspot.jar analyze --print-config > hotspot.yml저장소 루트에서 실행하세요. 하위 디렉터리나 비-git 경로에서는 오류가 나며, 루트 경로를
[path]로 넘기거나--config를 쓰라는 안내가 출력됩니다. linked git worktree도 지원합니다. 멀티 모듈에서 서브모듈별 JaCoCo 리포트는 자동 합산하지 않습니다(루트 레벨만 탐지). zero-config는topN: 50등 안전한 기본값을 쓰며(아래 수동 예시의20과 다름),--print-config로 확인·조정할 수 있습니다. 세밀한 제어가 필요하면 아래의--config방식을 쓰세요.
java -jar hotspot.jar init -o hotspot.ymlanalysis:
target:
type: local-git
# .git/ 폴더가 있는 디렉터리여야 함
path: /path/to/your/repo
window:
# 모드 A: 상대 윈도우("지금"부터 최근 N일) -- 안전한 기본값
days: 365
# 모드 B: 절대 ISO-8601 날짜 범위 (`days` 대신 사용)
# since: "2024-01-01"
# until: "2026-01-01"
scope:
granularity: [file, method]
include:
# 단일 모듈 Spring Boot 프로젝트
- "src/main/java/**/*.java"
# 멀티 모듈 Gradle/Maven 프로젝트 -- 서브모듈이 있으면 주석 해제
# (ftgo, jhipster 등 대부분의 실제 스택).
# - "**/src/main/java/**/*.java"
exclude:
- "**/generated/**"
- "**/test/**"
- "**/build/**"
- "**/target/**"
scoring:
decayHalfLifeDays: 90 # 최근성 감쇠 반감기(일)
# excludeCoverage: false # true면 Composite = CC × Decay (커버리지는 관측용)
# REST API 엔드포인트 핫스팟 (Spring 컨트롤러를 콜그래프 따라 집계).
# RestAssured 테스트 생성 우선순위 입력. Spring 앱이면 켜세요.
apiAnalysis:
enabled: true
sharedComponentMode: BOTH # CUMULATIVE | SEPARATE | BOTH
classpathDirectories: # 콜그래프 심볼 해석 향상(선택)
- build/libs
# JaCoCo XML 리포트(선택). 실제 리포트가 있을 때만 주석을 해제하세요.
# 커버리지가 점수에 반영됩니다(배수 1/(coverage+0.1)). 존재하지 않는 경로를
# 가리키면 커버리지 없이 진행한다는 경고가 출력됩니다(점수는 왜곡되지 않음).
# jacocoReportPath: build/reports/jacoco/test/jacocoTestReport.xml
output:
# 대소문자 무시: csv | yaml | md | html (여러 개 허용)
formats: [csv, yaml, md, html]
apiLayout: BOTH # COMBINED(hotspots.*에 통합) | STANDALONE(api_report.*) | BOTH
path: ./hotspot-report
topN: 20 # 0 = 무제한java -jar hotspot.jar analyze --config hotspot.yml출력:
hotspot-report/
├── file_hotspots.csv ← CSV는 단위별로 분리(파일 10열, 메서드 15열)
├── method_hotspots.csv
├── hotspots.yml ← YAML/MD/HTML은 두 단위를 한 문서에 묶음
├── hotspots.md
└── hotspots.html ← 아무 브라우저에서나 열기 — 정렬 가능 열 + 필터 박스
# apiAnalysis.enabled=true 이고 apiLayout이 STANDALONE | BOTH면 다음이 추가됩니다:
# api_hotspots.csv, shared_components.csv, api_report.{yml,md,html}
# output.coverageBreakdown=true 이면 coverage_breakdown.yml 도 함께 출력됩니다.
콘솔에는 상위 핫스팟(복합 점수 기준)과, HTML을 출력했다면
hotspots.html의 절대 경로가 함께 표시됩니다.--quiet로 끌 수 있습니다.
CSV는 분리하고 YAML/MD/HTML은 합치는 이유? CSV는 단일 헤더의 표 형식이라 파일(10열)과 메서드(15열) 리포트가 헤더를 공유할 수 없어, 엑셀/시트 편의를 위해 두 파일로 내보냅니다. YAML, Markdown, HTML은 두 표를 자연스럽게 한 파일에 담는 문서 형식이라 — PR에 첨부(
.md), 다운스트림 자동화에 투입(.yml), 또는 자체 완결형 증거 페이지로 브라우저에서 열기(.html)가 더 쉽습니다.
HTML 리포트 기능.
hotspots.html은 단일 자체 완결형 파일입니다 (원격 CSS/JS 없음, CDN 없음). 아무 열 헤더나 클릭해 오름/내림차순 정렬; 검색 박스에 입력해 경로·클래스·메서드·매개변수로 행 필터링. 라이트/다크 모드는 브라우저 설정을 따릅니다. 사용자 제어 값은 모두 HTML 이스케이프되어, 악의적인 파일 경로가 스크립트 태그를 주입할 수 없습니다.
Usage: hotspot [-hV] [COMMAND]
Description:
Hotspot analysis CLI for Java codebases.
Options:
-h, --help Show this help message and exit.
-V, --version Print version information and exit.
Commands:
analyze Analyse a repository and emit hotspot reports.
init Generate a sample hotspot.yml configuration file.
| 옵션 | 필수 | 기본값 | 설명 |
|---|---|---|---|
[path] |
현재 디렉터리 | 분석할 저장소 루트 경로(--config와 함께 사용 불가) |
|
--config, -c <file> |
— | YAML 설정 파일 경로. 생략하면 [path](또는 현재 디렉터리)에서 자동 감지 |
|
--print-config |
off | 자동 감지된 설정을 YAML로 stdout에 출력하고 종료(분석하지 않음). zero-config 모드 전용 — --config와 함께 쓸 수 없음 |
|
--quiet, -q |
off | stdout 요약 출력 억제 | |
--strict, -s |
off | 결과가 비면 종료 코드 3 (윈도우 내 커밋 0건 또는 스코프 매칭 파일 0건). CI 게이팅용. |
| 종료 코드 | 의미 |
|---|---|
| 0 | 분석 완료; 출력 기록됨 |
| 1 | 설정 무효 / 파이프라인 실패 / 미지원 대상 |
| 2 | Picocli 사용법 오류 |
| 3 | --strict 설정 상태에서 결과가 비어 있음 |
| 옵션 | 필수 | 기본값 | 설명 |
|---|---|---|---|
--output, -o <path> |
./hotspot.yml |
샘플 설정 저장 위치 | |
--force, -f |
off | 대상이 있으면 덮어쓰기 |
| 경로 | 타입 | 비고 |
|---|---|---|
analysis.target.type |
local-git | github |
CLI는 local-git만 end-to-end 실행 |
analysis.target.path |
string | local-git에 필수 |
analysis.target.github.{owner,repo,branch,token} |
strings | github에 필수; token은 ${ENV_VAR} 치환 지원 |
analysis.window.since / analysis.window.until |
ISO date | 또는 analysis.window.days 사용 |
analysis.window.days |
integer ≥ 1 | 지금 기준 상대 윈도우 |
analysis.scope.granularity[] |
file | method |
둘 다 선택 가능 |
analysis.scope.include[] |
glob[] | 최소 한 개 필요 |
analysis.scope.exclude[] |
glob[] | 선택 |
analysis.scoring.decayHalfLifeDays |
integer ≥ 1 | 최근성 감쇠 반감기; 기본 90일 |
analysis.scoring.excludeCoverage |
boolean | true면 Composite = CC × Decay, 리포트는 배수 대신 원시 line-coverage 표시; 기본 false |
analysis.apiAnalysis.enabled |
boolean | REST API 엔드포인트 + 공유 컴포넌트 단위 활성화; 기본 false |
analysis.apiAnalysis.sharedComponentMode |
CUMULATIVE | SEPARATE | BOTH |
2개 이상 엔드포인트가 공유하는 메서드의 집계/리포트 방식; 기본 BOTH |
analysis.apiAnalysis.classpathDirectories[] |
string[] | 콜그래프 심볼 해석을 돕는 의존성 jar/클래스 디렉터리 |
analysis.jacocoReportPath |
string | JaCoCo XML 리포트 경로; 커버리지 배수 1/(coverage+0.1) 활성화. 없으면 배수 1.0 |
output.formats[] |
CSV | YAML | MD | HTML |
최소 한 개 |
output.apiLayout |
COMBINED | STANDALONE | BOTH |
API/공유 테이블 위치: hotspots.*에 통합, 독립 api_report.*, 또는 둘 다; 기본 BOTH |
output.coverageBreakdown |
boolean | true면(+JaCoCo 제공 시) 모든 커버리지 수치의 계산 근거 — 파일별 counted 라인, 엔드포인트별 메서드 기여(covered/실행가능 라인) — 를 coverage_breakdown.yml로 출력; 기본 false |
output.path |
string | 출력 디렉터리 |
output.topN |
integer ≥ 0 | 0은 "모든 행" |
문자열 값의 환경변수는 ${VAR_NAME} 구문으로 치환됩니다. #로 시작하는 줄
(YAML 주석)은 그대로 두어 문서 예시가 오류를 일으키지 않습니다.
모든 리포트는 이제 두 개의 점수와 네 개의 입력 요인을 다음 표준 순서로 나란히 담습니다:
| 열 | 의미 |
|---|---|
| LOC | 아티팩트의 현재 라인 수 |
| Revisions | 윈도우 내에서 해당 항목을 건드린 커밋 수 |
| Simple Score | Revisions × LOC — Adam Tornhill의 원래 신호 |
| Recency Decay | 같은 커밋들에 대한 Σ exp(-ln(2) × Δt / halfLife) |
| Cognitive Complexity | SonarQube에서 영감을 받은 AST 순회 점수 |
| Coverage Multiplier | JaCoCo XML 기준 1 / (line_coverage + 0.1); 리포트 없으면 1.0 |
| Composite Score | Cognitive Complexity × Recency Decay × Coverage Multiplier |
행은 Composite Score 내림차순으로 정렬됩니다(동점은 경로 / 표준 시그니처로 처리).
CSV는 단위별로 분리(파일 10열, 메서드 15열); YAML/MD/HTML은 모든 단위를 한 문서에 묶습니다.
CSV는 위 표의 요인 앞에 simple_rank, composite_rank 두 열이 먼저 옵니다 — 즉
file_hotspots.csv 헤더는 simple_rank,composite_rank,path,loc,revisions,simple_score,recency_decay,cognitive_complexity,coverage_multiplier,composite_score이며,
method_hotspots.csv는 그 사이에 fqcn,method,parameters,file,start_line,end_line을 더한 15열입니다.
(excludeCoverage: true면 마지막 두 열이 composite_score,line_coverage로 바뀝니다.)
단위별 산출 방식 — 파일 / 메서드 / REST API 엔드포인트 / 공유 컴포넌트마다 Revisions / Recency Decay / Cognitive Complexity / Coverage를 어떻게 측정하고 결합하는지 — 는
docs/scoring/에 소스 근거와 워크드 예시까지 정리돼 있습니다.
docs/
phase1-design.md ← 아키텍처 & 결정
reports/T1…T11-*.md ← 태스크별 완료 리포트
src/main/java/io/github/baekchangjoon/hotspotanalysis/
HotspotApplication.java ← Spring Boot 진입점
cli/ ← Picocli 서브커맨드
config/ ← YAML 스키마 record + 로더
vcs/ ← VcsProvider + LocalGit / GitHub 구현
parser/ ← JavaParser 기반 메서드 추출
analysis/ ← Revisions / LOC / Score / Orchestrator
coverage/ ← JaCoCo XML 파서
output/ ← CSV / YAML / MD / HTML 라이터
src/main/resources/
application.yml ← Spring Boot 로깅 설정
templates/hotspot.example.yml ← `hotspot init`용 번들 샘플
첫 실행이 "비어" 보인다면 거의 확실히 아래 셋 중 하나입니다.
Java NIO의 ** glob은 빈 경로 세그먼트를 매칭하지 않습니다. 그래서 단일
모듈과 멀티 모듈 레이아웃 사이에 미묘한 비대칭이 생깁니다:
| 저장소 형태 | 첫 Java 경로 예시 | 동작하는 패턴 |
|---|---|---|
| 단일 모듈 | src/main/java/com/foo/Hot.java |
src/main/java/**/*.java |
| 멀티 모듈 | service-a/src/main/java/com/foo/Hot.java |
**/src/main/java/**/*.java |
대상의 형태를 미리 모르면 두 패턴을 모두 나열하세요. 수집기가 매칭을 중복 제거합니다:
scope:
include:
- "src/main/java/**/*.java" # 단일 모듈 저장소 포착
- "**/src/main/java/**/*.java" # 멀티 모듈 저장소 포착빠른 점검(Bash):
git -C <repo> ls-files | grep -E 'src/main/java/.*\.java$' | head윈도우는 지금 기준으로는 맞지만, 저장소가 수년간 비활성이었을 수 있습니다 (동결된 참조 구현, 아카이브된 데모 등). 실제 활동과 겹치는 절대 범위로 바꾸세요:
window:
since: "2017-01-01"
until: "2026-12-31"커맨드라인에서 교차 확인:
git -C <repo> log --since=<since> --until=<until> --name-only \
--pretty=format: -- '*.java' | sort -u | headCI 팁:
hotspot analyze호출에--strict를 추가하세요. 커밋이나 파일이 비어 돌아오면 종료 코드 3으로 끝나, 잘못 설정된 CI 파이프라인이 빈 리포트를 만드는 대신 큰 소리로 실패합니다.java -jar hotspot.jar analyze --config hotspot.yml --strict
번들 jar는 Java 21로 컴파일됩니다. Gradle 툴체인이 빌드에 21을 자동
프로비저닝했더라도, 실행에는 여전히 PATH에 JDK 21+가 필요합니다:
java -version # 반드시 21 이상 보고
# macOS:
export JAVA_HOME=$(/usr/libexec/java_home -v 21)
# Linux (sdkman):
sdk use java 21.0.4-temtarget.type: github에는 토큰이 필요합니다. 둘 중 하나:
target:
type: github
github:
owner: my-org
repo: my-repo
branch: main
token: ${GITHUB_TOKEN} # 로드 시 환경에서 해석…그리고 실행 전에 export GITHUB_TOKEN=…. CLI는 local-git에만
end-to-end로 연결되어 있고, GitHub 프로바이더는 WireMock 계약 테스트로
검증됩니다. GitHub 저장소를 분석하려면 로컬에 클론하고
target.type: local-git을 작업 트리에 가리키세요.
- GitHub 대상 end-to-end: API 클라이언트는 WireMock으로 검증되지만 CLI의
analyze는local-git대상을 요구합니다. GitHub 저장소는 로컬에 클론하고target.type=local-git으로 실행하세요. - 작업 트리 LOC: LOC는 각 역사적 커밋이 아니라 HEAD에서 읽습니다 (Tornhill이 책에서 하는 동일한 단순화).
- Merge 커밋: 변경 이력은 각 커밋의 첫 번째 부모와의 diff로 계산합니다. 따라서 fast-forward 없이 머지된 브랜치 쪽 변경은 revision/recency 집계에서 누락될 수 있습니다.
- 소스 파싱 실패: 스코프 내
.java파일 하나라도 파싱에 실패하면 분석 전체가 중단됩니다(부분 결과를 내지 않음). 손상·비표준 소스는scope.exclude로 제외하세요.
이 결정들과 대안의 태스크별 분석은 docs/reports/*를 참고하세요.
./gradlew test # 포괄적 테스트 스위트, ~10초
./gradlew build # 전체 어셈블리
./gradlew check # 테스트 + 정적 분석CI는 매 푸시마다 그리고 매일 스케줄(09:00 KST)로 실행됩니다. 각 실행은 다음 아티팩트를 업로드합니다(Actions 탭 참고):
| 아티팩트 | 내용 |
|---|---|
hotspot-jar-<N> |
어셈블된 fat jar (hotspot-*.jar) |
test-results-<N> |
모든 테스트 클래스의 JUnit XML |
test-report-<N> |
Gradle의 전체 HTML 테스트 리포트 |
test-summary-<N> |
GitHub Step Summary 패널에 표시되는 Markdown 요약 |
hotspot-demo-report-<N> |
자체 분석 출력 — file_hotspots.csv, method_hotspots.csv, hotspots.yml, hotspots.md, hotspots.html, 그리고 이를 만든 hotspot.yml. 다운로드해 hotspots.html을 브라우저에서 바로 여세요. |
hotspot-analysis는 전적으로 당신의 머신에서 실행됩니다. 텔레메트리도, 애널리틱스도, "phone home"도 없습니다.
local-git대상 (기본값): git 히스토리와 Java 소스를 지정한 작업 트리에서 읽고, 점수를 로컬에서 계산해, 리포트를 로컬output.path디렉터리에 씁니다. 아무것도 컴퓨터를 떠나지 않습니다.github대상: 유일한 외부 네트워크 트래픽은 GitHub REST API로 향하며, 당신이${GITHUB_TOKEN}으로 제공한 토큰으로 인증합니다. 토큰은 로드 시 환경에서 읽히고 리포트에는 절대 기록되지 않습니다.- 리포트(
hotspots.html등)는 코드의 파일 경로·클래스/메서드 이름·라인 수를 담습니다. 소스와 동일하게 취급하세요 — HTML 리포트는 완전히 자체 완결형이고 (CDN/원격 호출 없음) 모든 값을 HTML 이스케이프하지만, 여전히 코드 구조를 담으므로 의도적으로만 공유하세요. - 스킬 / 플러그인은 이 로컬 CLI를 구동할 뿐입니다. 설치해도 어떤 코드나 분석도 제3자에게 전송하지 않습니다.
MIT © 2026 baekchangjoon