Intent-Revealing Names — 이름 주도 관통 리팩토링
이름 없는 긴 메서드 하나를 대상으로, grouping → comment → 정직한 이름 → extract → 의도 드러내는 이름 → 도메인 추상화를 하나의 연속 흐름으로 관통한다.
이름을 신뢰한다면 어떨까요? 이름을 읽어도 메서드를 읽을 필요가 없는 세상을 상상해 보세요.
— Arlo Belshee
핵심 명제
- intent(의도) = why(왜). 좋은 이름은 코드가 *무엇을 하는가(what)*가 아니라 *왜 부르는가(why)*를 드러낸다.
- 이름은 리팩터링의 결과가 아니라 진단 수단이다. 이름을 정직하게 만들려는 시도 자체가 구조적 문제(SRP 위반, 원시 집착)를 드러내고, 그 진단이 Extract Method·값 객체 도입으로 이어진다.
- comment는 extract될 메서드 이름의 초안이다 (Kent Beck: "좋은 이름이 떠오르지 않으면 먼저 주석으로 의도를 표현하라 — Extract Method의 전 단계").
- 전환점은 3 → 5단계: 3단계까지 "무엇을(what)"을 정직하게 누적하고, 5단계에서 "왜(why)"로 전환한다. 이 전환이 있어야 "이름만 읽으면 본문을 읽을 필요가 없는" 상태가 된다.
GOAL
- 성공 = 대상 긴 메서드가 목차(table of contents)로 축약되고, 각 이름이 why를 드러내며, 모든 테스트가 통과하고, 되돌리기 쉬운 단위 커밋으로 남는다.
- 상위 메서드의 이름이 what 나열이 아니라 why(호출 이유)를 드러냄
- 추출된 각 private helper가 하나의 식별 가능한 책임만 수행 (Composed Method)
- 남은 원시 집착(Primitive Obsession)이 식별되어 후속 스킬로 인계됨 (승격 자체는 이 스킬 밖)
- 동작 변경 없이 구조만 개선됨
CONSTRAINTS
- 계열: Tidy — 후보 보고 후 승인 없이 적용 (
../../references/refactoring-procedure.md §0·§3-A)
Hard Rules (공유)
- 동작 변경 금지 — 구조·이름만 개선, 기능 변경 없음
- 테스트 수정 금지 — 변경이 테스트를 실패시키면 즉시 되돌리기
- 작은 단계 + 매 단계 테스트 — 한 번에 하나의 변형만 적용하고 green 확인
- 명시적 git add —
git add -A 금지, 변경 파일만 명시
- 사용자 확인 — 대상 메서드 확정과 5·6단계 진입은 사용자와 합의
이 스킬 고유 규칙
- 대상은 하나의 긴 메서드 — 파일 전체 검토가 아니라, 지정된(또는 가장 냄새나는) 긴 메서드 하나에 집중
- rename을 마지막에 일괄 수행하지 않는다 — 이름 진화(3단계)가 extract(4단계)를 유발하고, extract 후 다시 rename(5단계)한다. 이 인터리빙이 이 스킬의 본질이다
- Extract는 국소(local)에 한정 — 대상 메서드를 같은 클래스 내 private helper로 분할하는 것까지. 클래스 분리·도메인 이동 등 대규모 구조 변경은
/system-wide-refactoring 전담 (이 스킬은 self-contained 연속 흐름)
- 6단계는 가르치되 인계 — 운영상 이 스킬은 5단계(Intent Revealing)까지 실행하고, 6단계(Domain Abstraction)는 방향만 보여준 뒤 원시 집착을 지목해 값 객체·도메인 이동을 후속 스킬(
/discover-value-object 등)로 인계한다. 승격을 이 스킬 흐름 안에서 실행하지 않는다
6단계 관통 프로세스 — "주문 처리" 예제
e-commerce "주문 처리" 예제로 6단계를 처음부터 끝까지 진행한다. 각 단계는 목적 + 코드 상태 + 다음 단계로 넘어가게 만드는 트리거(냄새) 로 구성된다.
출발점 — 이름 없는 긴 메서드
OrderHelper.process(cart)가 재고 확인 → 재고 차감 → 결제 → 확정 메일을 한 메서드에서 처리한다. -Helper 접미사와 process는 정보가 없으면서 정보가 있는 것처럼 보이는, 가장 위험한 종류의 이름이다.
public class OrderHelper {
public void process(Cart cart) {
for (CartItem item : cart.getItems()) {
int stock = stockDao.getStock(item.getProductId());
if (stock < item.getQuantity()) throw new RuntimeException("out of stock");
}
for (CartItem item : cart.getItems())
stockDao.decrease(item.getProductId(), item.getQuantity());
BigDecimal total = BigDecimal.ZERO;
for (CartItem item : cart.getItems())
total = total.add(item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity())));
paymentGateway.charge(cart.getUserId(), total);
emailSender.send(cart.getUserEmail(), "주문이 완료되었습니다");
}
}
단계 A — 본문 정돈으로 책임 가시화 (grouping + comment)
사용자 요청 1·2번. 기법 정의는 /tdd-tidy(tdd-blue의 Local Tidying)에서 인용 — 여기서는 대상 메서드에 국소 적용한다.
A-1. Reorder (Reading/Cohesion Order): 변수 선언을 사용 위치 가까이로(Reading Order), 관련 로직끼리 인접하게(Cohesion Order, Step Down Rule).
A-2. Chunk: 빈 줄로 논리 블록을 분리한다.
A-3. Explaining Comment: 각 블록에 "무엇을/왜"를 한 줄로 붙인다. 이 주석이 곧 추출될 메서드 이름의 초안이다.
public void process(Cart cart) {
// 재고 확인 — 부족하면 주문 거절
for (CartItem item : cart.getItems()) {
int stock = stockDao.getStock(item.getProductId());
if (stock < item.getQuantity()) throw new RuntimeException("out of stock");
}
// 재고 차감
for (CartItem item : cart.getItems())
stockDao.decrease(item.getProductId(), item.getQuantity());
// 결제 — 합계 계산 후 청구
BigDecimal total = BigDecimal.ZERO;
for (CartItem item : cart.getItems())
total = total.add(item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity())));
paymentGateway.charge(cart.getUserId(), total);
// 확정 메일 발송
emailSender.send(cart.getUserEmail(), "주문이 완료되었습니다");
}
트리거: 주석 붙은 책임 블록이 4개다 — 이 메서드가 여러 일을 한다는 사실이 이제 눈에 보인다.
단계 B — 상위 메서드 이름을 정직하게 진화 (6단계 1→2→3)
이름 축을 정직하게 단계적으로 바꿔, 이름 스스로가 SRP 위반을 드러내게 만든다.
1. Obvious Nonsense: process() → applesauce(). 그럴듯하지만 비어 있는 이름을, 일부러 명백히 틀린 이름으로 바꿔 "여기 이름이 필요하다"를 코드가 명시하게 한다. -er/-Utils/라이프사이클(onCreate) 이름을 제거하는 단계이기도 하다.
2. Honest: applesauce() → probably_updateStockAndCharge_andStuff(). 3부 규칙 — probably_(불확실) + 파악한 동작 + _andStuff(아직 못 파악). 완벽함이 아니라 오해를 주지 않는 것이 목표.
3. Completely Honest: checkStockAndDeductStockAndChargePaymentAndSendConfirmationEmail(). 단계 A의 comment들을 그대로 이어 붙인 이름. probably_·_andStuff를 모두 제거하고 하는 일 전부(what)를 담는다. 길고 어색해도 상관없다.
트리거: 이름에 And가 네 번 등장한다 — 책임이 네 개, 즉 SRP 위반을 이름이 드러낸다. 이것이 다음 단계(extract)의 근거다.
팀 합의 노트: applesauce·probably_..._andStuff 같은 중간 이름을 실제 커밋할지는 팀 합의 사항이다. "아직 이해하지 못했다"를 정직하게 남기는 편이 그럴듯한 거짓 이름보다 낫다는 입장(Emily Bache 데모)이 있으나, 개인 작업이면 3단계에서 한 번에 커밋해도 된다.
단계 C — 드러난 책임대로 Extract (6단계 4: Does the Right Thing, composed method)
완전정직 이름의 각 And 조각 = 단계 A의 각 comment 블록. 그 블록들을 comment를 이름으로 승격하여 private helper로 추출한다. 상위 메서드는 4줄짜리 목차가 된다. (Arlo Belshee 정본 4단계 "Does the Right Thing"을 여기서 Extract로 흡수한다 — 각 조각이 비로소 제 일만 하게 되므로.)
public void checkStockAndDeductStockAndChargePaymentAndSendConfirmationEmail(Cart cart) {
ensureStockAvailable(cart);
deductStock(cart);
chargePayment(cart);
sendConfirmationEmail(cart);
}
private void ensureStockAvailable(Cart cart) { /* 재고 확인 블록 */ }
private void deductStock(Cart cart) { /* 재고 차감 블록 */ }
private void chargePayment(Cart cart) { /* 결제 블록 */ }
private void sendConfirmationEmail(Cart cart) { /* 확정 메일 블록 */ }
지역 변수가 상호 의존해 추출이 불가능하면 먼저 /replace-temp-with-query·/extract-method-object를 적용한다(이름으로 인용).
트리거: 구조는 정직해졌지만 상위 이름은 여전히 "무엇을 하는지"의 나열이다. "왜 이 메서드를 부르는지"는 아직 드러나지 않는다.
단계 D — 의도를 드러내기 (6단계 5) ★핵심 전환점
호출하는 쪽(예: CheckoutController의 "주문하기" 버튼 처리)을 살펴보면, 이 메서드의 진짜 목적은 "고객이 주문을 낸다"임을 알 수 있다. what이 아니라 why로 이름을 바꾼다.
// 호출부: checkStockAndDeductStockAndChargePaymentAndSendConfirmationEmail(cart);
// ↓
// placeOrder(cart);
public class OrderService { // OrderHelper → OrderService
public void placeOrder(Cart cart) {
ensureStockAvailable(cart);
deductStock(cart);
chargePayment(cart);
sendConfirmationEmail(cart);
}
}
이것이 "intent = why"의 실체다. 이름은 더 짧아지고, 호출 코드를 읽을 때 자연스럽게 이해된다.
트리거: 이름은 좋아졌지만 내부에 String productId, int quantity, BigDecimal total 같은 원시 타입이 분산되어 있다 — 원시 집착(Primitive Obsession).
단계 E — 도메인 추상화 (6단계 6)
여기서부터는 무거운 구조 변경이라 이 스킬의 범위를 벗어난다 — 이 스킬은 5단계까지 실행하고 6단계는 인계한다. 값 객체·First Class Collection·Functional Core 분리는 각각 /discover-value-object, /introduce-parameter-object, /first-class-collection, /segregate-functional-core로 인계한다.
아래 코드는 후속 스킬들로 도달하는 목표 end-state(aspirational)이며, 이 스킬에서 전부 실행하지 않는다. 이 스킬의 역할은 5단계까지 완성한 뒤 원시 집착을 가리키고, 실제 승격은 인용된 후속 스킬로 넘기는 것이다 (클래스 분리·도메인 이동은 이 스킬 범위 밖 — FAILURE CONDITIONS 참조).
public Order placeOrder(Cart cart) {
Order order = cart.toOrder(); // Order 애그리거트
inventory.reserve(order.lines()); // 재고 확인+차감 → Inventory 도메인
payment.charge(order.total()); // total() → Money 값 객체
notifier.notifyOrderPlaced(order);
return order;
}
| 이전 (원시 집착) | 이후 (도메인 개념) |
|---|
String productId + int quantity 반복 | OrderLine 값 객체 |
BigDecimal total 산재 | Money 값 객체 (order.total()) |
stockDao.getStock()/decrease() 절차 호출 | Inventory.reserve(lines) |
Cart를 절차적으로 순회·변환 | Order 애그리거트 (cart.toOrder()) |
이제 placeOrder의 본문은 하나의 유비쿼터스 언어 안에서 비즈니스 규칙을 읽는 산문이 된다.
6단계 요약
| 단계 | 이 스킬의 활동 | 초점 | 예제 이름 |
|---|
| A. Tidy-prep (6단계 이전 준비) | Reorder + Chunk + Comment | 책임 가시화 | // 재고 확인 등 4개 블록 |
| B1. Obvious Nonsense | 나쁜 이름 제거 | — | applesauce() |
| B2. Honest | 파악한 사실만 정직하게 | what(부분) | probably_updateStockAndCharge_andStuff() |
| B3. Completely Honest | 하는 일 전부 | what(전부) | checkStock...AndSendConfirmationEmail() |
| C. Extract | SRP 위반대로 분리 | Composed Method | ensureStockAvailable() + 3개 |
| D. Intent Revealing ★ | 무엇이 아니라 왜 | why | placeOrder(cart) |
| E. Domain Abstraction (범위 밖 — 인계) | 원시 집착 → 도메인 언어 | 도메인 | 후속 스킬로 인계 (Order, Money, Inventory) |
다른 스킬과의 경계
이 스킬은 self-contained하지만, 각 기법의 정의는 재기술하지 않고 이름으로 인용한다.
- grouping/comment 기법 상세 →
/tdd-tidy(tdd-blue): Reorder, Chunk, Explaining Comment, Normalize Symmetries, Split Loop 등
- 대규모 Extract / 클래스 분리 / 도메인 이동 →
/system-wide-refactoring. 이 스킬은 국소 private helper 추출까지만
- 개별 이름 rename 카탈로그(Naming Smells 탐지) →
/naming-process. 이 스킬은 "하나의 메서드를 6단계로 관통"이 목적 (개별 이름 스캔이 아님)
- 값 객체/컬렉션/함수형 코어 승격 상세 →
/discover-value-object, /introduce-parameter-object, /first-class-collection, /segregate-functional-core
OUTPUT FORMAT
실행 절차
1. 대상 메서드 확정
- 인자로
파일:메서드가 오면 그 메서드를, commit-ref가 오면 그 diff의 Java 파일에서 가장 긴/냄새나는 메서드를 후보로 제시
- 인자가 없으면
git diff --name-only -- '*.java'(+--cached)의 변경 파일에서 20줄 이상 메서드를 찾아 후보 제시
- 테스트 파일(
*Test.java 등)은 제외
- 후보를 보여주고 대상 하나를 사용자와 확정
관통 리팩토링 대상 후보:
- OrderHelper.process(Cart) — 15줄, 책임 4개 추정 (재고확인/차감/결제/메일)
이 메서드로 진행할까요?
2. 단계 A~E를 순서대로, 작은 단계로 적용
- A: Reorder → Chunk → Comment (책임 블록 가시화)
- B: 상위 메서드 이름을 1→2→3단계로 진화 (완전정직까지)
- C: comment 블록을 이름으로 승격해 private helper 추출 (IDE Extract Method)
- D: 호출부를 근거로 상위 메서드를 intent-revealing rename (IDE Rename — 참조 자동 갱신). 클래스명
-Helper/-er도 이때 함께
- E: (실행 안 함 — 인계) 5단계 완성 후 남은 원시 집착을 지목하고, 값 객체·도메인 이동을 후속 스킬(
/discover-value-object 등)로 제안한다. 승격은 이 스킬에서 수행하지 않는다
각 단계마다: 변경 → 테스트 실행(./gradlew test 또는 mvn test) → green이면 진행, red면 즉시 되돌리기.
3. 커밋 — 되돌리기 쉬운 단위로
의미 단위로 나눠 커밋하는 것을 권장한다 (각 단계가 독립적으로 되돌려짐):
refactor: tidy-prep OrderHelper.process — group & comment responsibilities (단계 A)
refactor: extract composed methods from OrderHelper.process (단계 C, B의 이름 진화 포함)
refactor: reveal intent — process → placeOrder (단계 D)
(단계 E의 도메인 승격은 이 스킬에서 커밋하지 않는다 — 후속 스킬로 인계.)
- 커밋 메시지는
docs/reviewable-commits.md(없으면 ${CLAUDE_PLUGIN_ROOT}/references/reviewable-commits.md) 표준을 따른다. subject는 refactor: 접두사, body에 Why(이름이 무엇을 드러냈고 왜 이렇게 분리/명명했는지)를 담는다. 형식은 그 표준이 유일한 출처이므로 여기서 재기술하지 않는다. 길이는 ../../references/commit-style.md의 간결성 규칙(제목 + 핵심 bullet 2~4줄)을 따른다.
- 한글 메시지는 임시 파일 +
git commit -F (-m "한글" 금지 — 깨짐).
- 최소 요건이 급하면 전체를 단일
refactor: 커밋으로 마무리해도 되나, 단계별 분리가 리뷰·롤백에 유리하다.
4. 완료 보고
- 이름 진화 경로 요약 (
process → applesauce → … → placeOrder)
- 추출된 helper 목록과 각 책임
- 테스트 결과
- 남은 원시 집착과 인계할 후속 스킬 제안 (
/discover-value-object 등)
FAILURE CONDITIONS
이 중 하나라도 발생하면 작업 실패로 간주:
더 읽을거리 (비-load-bearing)
이 스킬은 아래 vault 문서를 소스로 작성되었다. 런타임 의존성이 아니며, 없어도 스킬은 완전하게 동작한다.
- vault:
Intent Revealing Names.md — Arlo Belshee 6단계 프로세스를 "주문 처리" 예제로 관통한 원 노트
- Emily Bache, Trustworthy Code with Naming as a Process — Arlo Belshee 기법 라이브 데모
- Robert C. Martin, Clean Code Ch.4 Meaningful Names — Intention-Revealing Names 원개념