유지보수성 리뷰
기준은 하나다. 다음 사람(사람이든 에이전트든)이 이 코드를 고칠 때 몇 곳을 열어야 하는가. 정확성 버그는 다루지 않는다. 그건 code-review나 pr-review의 몫이다.
0. 모드
| 모드 | 대상 | 비교 기준 | blocking 대상 |
|---|---|---|---|
| 셀프 체크 | 커밋 전 내 diff | 레포 전체 | 고친다 (보고하지 않고 바로) |
| 변경 리뷰 | PR diff | 레포 전체 | 이번 변경이 만든 문제만 |
| 점검 | 모듈이나 디렉터리 전체 | 레포 전체 | 전부. 영향이 큰 순서로 |
변경 리뷰에서 원래 있던 문제는 blocking으로 올리지 않는다. "기존 부채"로 따로 적는다.
1. 프로젝트 기준 읽기
docs/review/architecture.md를 찾는다. 없으면 CLAUDE.md·AGENTS.md가 가리키는 구조 문서를 쓴다.- 프로젝트 기준은 이 스킬의 일반 기준보다 우선한다. 특히 다음을 기준 문서에서 가져온다.
- 모듈 지도와 의존 방향
- 재사용해야 하는 추상화 목록 (HTTP 클라이언트, 결과 타입, 테스트 도우미 등)
- 알려진 부채 목록. 이 목록에 있는 패턴은 새 코드가 따라 할 선례가 아니다.
- 기준 문서가 없으면 디렉터리 구조와 각 모듈의
indexexport로 모듈 지도를 직접 그리고, 보고 첫 줄에 "기준 문서 없음"을 적는다.
2. 볼 것
위에서부터 본다. 위에 있을수록 나중에 치르는 비용이 크고, 에이전트가 자주 놓친다.
2-1. 기존 추상화를 우회했나
에이전트는 이슈 하나만 보고 작업해서, 이전 이슈가 만든 모듈을 모른 채 같은 것을 또 만든다. 가장 흔하고 가장 비싼 실패다.
새로 생긴 함수, 타입, 에러 클래스, 상수, 외부 호출마다 레포에서 같은 일을 하는 코드를 찾는다.
git grep -nE "class \w+Error extends" # 에러 클래스
git grep -nE "https?://[a-z.]+\.(com|io|dev)" # 외부 API 주소와 헤더
git grep -nE "^(export )?type \w+ = \{" # 비슷한 이름의 타입 (예: XxxEntry)
git grep -nF "<새 코드의 특징적인 문자열·정규식>"
- 같은 외부 시스템을 부르는 래퍼가 둘 이상이면, 호출자가 오류를 여러 클래스로 나눠 검사하게 된다. 그 흔적(
a instanceof X || b instanceof Y)도 함께 찾는다. - 같은 목록 상수(확장자, 상태 값)가 두 곳에 있으면 한쪽만 바뀐다.
2-2. 의존 방향
- 기준 문서의 레이어 규칙을 어겼나 (예: 도메인 모듈이 UI나 외부 API를 import).
- 하위 모듈이 상위 모듈을 import하거나, 두 모듈이 서로 import하나.
2-3. 단위
크기가 아니라 변경 이유로 나눈다.
- 파일 하나에 서로 호출하지 않는 함수 묶음이 둘 이상이면, 변경 이유가 둘 이상이라는 신호다 (예: 폼 매핑 + 중첩 키 접근 + 날짜 형식).
- 화면 파일 여러 개가 같은 순서의 준비 과정(인증 → 설정 로드 → 오류 분기)을 반복하면, 그 과정이 이름을 가진 함수여야 한다.
2-4. 추상화의 질
- 빠진 API: 호출자 둘 이상이 같은 전처리·후처리를 반복하면, 그 처리를 담은 함수가 모듈에 없는 것이다. 예: 모든 호출자가
doc.items.find((s) => s.kind === "x")뒤에parseX(...)를 부른다면readX(doc)가 빠졌다. - 과한 추상화: 쓰는 곳이 하나뿐인 인터페이스, 팩토리, 옵션, 제네릭. 지금 필요 없는 확장점이다.
- 경계의 타입: 실패를 호출자가 처리해야 하면 결과 타입(판별 유니언)으로 드러나나. 호출자가 메시지 문자열을 비교해 분기하지 않나.
2-5. 패턴
- 레포가 이미 쓰는 패턴(결과 타입, 의존 주입, 오류 처리, 모듈 export 방식)을 따랐나.
- 새 패턴은 기존 패턴으로 안 되는 이유가 있을 때만 받아들인다. 이유가 PR이나 주석에 없으면 지적한다.
2-6. 주석과 이름
- 주석이 엉뚱한 대상에 붙어 있지 않나. 수정 커밋이 함수 사이에 새 함수를 끼워 넣어 JSDoc이 다른 함수 위에 남는 사고가 잦다.
- 이름이 하는 일과 맞나. 이름만 보고 다른 동작을 기대하게 되면 지적한다.
3. 판정
blocking (머지하지 않는다)
- 같은 책임의 코드가 이미 있는데 새로 만들었다. 기존 것을 쓸 수 없는 이유가 PR이나 주석에 적혀 있으면 제외한다.
- 기준 문서의 레이어 규칙을 어겼다.
- 새 API 때문에 호출자 둘 이상이 같은 처리를 반복하게 됐다.
- 주석이 다른 대상에 붙어 읽는 사람을 오도한다.
suggestion (머지는 막지 않고 후속 작업으로 남긴다)
- 파일 분리, 이름 개선, 원래 있던 중복.
적지 않는 것: 취향(함수형·명령형, 줄 길이), 린터와 포매터가 잡는 것, 이슈 범위 밖의 새 라이브러리 도입.
4. 검증
지적마다 코드로 확인한다. 틀린 지적은 구현 에이전트의 시간을 버리게 한다.
- "중복"이면 두 위치를 모두 열어 정말 같은 일을 하는지 본다. 생김새가 비슷해도 지키는 규격이 다르면 뺀다.
- "N곳"이면 grep으로 센다.
- "이번 변경이 만들었다"면
git diff <base>...HEAD에 그 줄이 있는지 본다.
5. 보고
첫 줄: 본 범위(모드, 대상 경로, 기준 문서 유무)와 건수(blocking N, suggestion N, 기존 부채 N).
항목마다:
- 제목: 무엇이 비용인지를 결과로 쓴다. ("GitHub 401을 호출자가 오류 클래스 3개로 나눠 검사한다" ○ / "에러 처리 개선" ×)
- 위치: 모든 관련 위치를
파일:줄로. 중복이면 양쪽 다. - 비용: 다음에 무엇을 바꿀 때 몇 곳을 고쳐야 하는지. ("API 버전 헤더를 바꾸면 3개 파일을 고쳐야 한다")
- 방향: 어느 쪽으로 합칠지 한두 문장.
- 등급: blocking / suggestion / 기존 부채.
지적할 것이 없으면 "없음"이라고 쓴다. 채우려고 지적하지 않는다.
하지 말 것
- 줄 수나 파일 크기만 보고 나누라고 하기.
- 방향 없는 지적 ("더 추상화하면 좋겠다").
- 생김새만 비슷한 코드를 중복이라고 하기.
- 원래 있던 문제를 이번 PR의 blocking으로 올리기.