本文へ移動
cccskills
無料GitHub で公開

extract-method-object

지역 변수가 서로 얽혀 Extract Method가 불가능한 거대 메서드를 별도 클래스(Method Object)로 추출. "긴 메서드를 클래스로", "지역 변수 때문에 추출 불가", "method object", "/extract-method-object" 요청 시 사용. 단, 지역 변수 얽힘 없이 잘라낼 수 있는 긴 메서드는 /intent-revealing-names(grouping→extract)가 적합. /extract-method-object [commit-ref]로 호출.

インストール方法を見る

含まれるファイル(1)

  • SKILL.md10.6 KB

SKILL.md(原文)

インストールする前に、エージェントに与えられる指示の中身を確認できます。

Extract Method Object Skill

지역 변수가 상호 의존해 Extract Method가 어려운 거대 메서드를 별도 클래스로 추출.

GOAL

  • 성공 = 지역 변수가 상호 의존하는 긴 메서드가 Method Object로 추출되어 커밋 완료됨
  • 50줄 이상의 복잡한 메서드가 식별됨
  • 지역 변수가 메서드 전역에 걸쳐 상호 의존해 Extract Method 불가
  • 후보 보고 후 Method Object 패턴 적용 (Tidy 계열 — 승인 없이)
  • 모든 테스트 통과

CONSTRAINTS

  • 계열: Tidy — 후보 보고 후 승인 없이 적용 (../../references/refactoring-procedure.md §0·§3-A)

Hard Rules

  • 동작 변경 금지 — 구조 개선만 수행
  • 테스트 수정 금지 — 구조 변경이 테스트를 실패시키면 되돌리기
  • 커밋 단위 — 1파일 x 1기법 = 1커밋 (논리적으로 연결된 파일은 함께)
  • git add -A 금지 — 변경된 파일만 명시적으로 추가

적용 패턴

Extract Method Object 리팩토링 단계:

  1. 메서드를 클래스로 변환

    • 새 클래스 생성 (메서드명 + "er" 또는 명사형)
    • 원본 메서드를 invoke() 또는 execute() 메서드로 이동
  2. 지역 변수를 필드로 변환

    • 메서드 내 지역 변수들을 클래스 필드로 변환
    • 읽기 전용 변수는 final 필드로
  3. 외부 의존성을 생성자 파라미터로 전달

    • 원본 클래스의 필드 참조를 생성자 파라미터로
    • 필요한 최소한의 의존성만 전달
  4. 작은 메서드로 분해

    • 큰 invoke() 메서드를 의미 있는 작은 private 메서드들로 분해
    • 각 메서드는 단일 책임 수행

Before/After 예시

// Before: 지역 변수가 상호 의존하는 거대 메서드 (50+ lines)
public class RefundService {
    public List<RefundDiff> refundDiff() {
        // 지역 변수 선언
        Map<String, Cost> costMap = new HashMap<>();
        Map<String, AcmeCost> acmeCostMap = new HashMap<>();
        List<RefundDiff> differences = new ArrayList<>();
        
        // 복잡한 로직이 50줄 이상...
        for (Cost cost : costs) {
            costMap.put(cost.getItemId(), cost);
        }
        
        for (AcmeCost acmeCost : acmeCosts) {
            acmeCostMap.put(acmeCost.getItemId(), acmeCost);
        }
        
        for (OrderItem item : orderItems) {
            Cost cost = costMap.get(item.getItemId());
            AcmeCost acmeCost = acmeCostMap.get(item.getItemId());
            
            if (cost != null && acmeCost != null) {
                double diff = cost.getAmount() - acmeCost.getAmount();
                if (Math.abs(diff) > 0.01) {
                    differences.add(new RefundDiff(item, diff));
                }
            }
        }
        
        return differences;
    }
}

// After: Method Object로 추출
public class RefundService {
    public List<RefundDiff> refundDiff() {
        return new RefundDifferenceCalculator(costs, acmeCosts).invoke(orderItems);
    }
}

class RefundDifferenceCalculator {
    private final List<Cost> costs;
    private final List<AcmeCost> acmeCosts;
    private Map<String, Cost> costMap;
    private Map<String, AcmeCost> acmeCostMap;
    private List<RefundDiff> differences;
    
    RefundDifferenceCalculator(List<Cost> costs, List<AcmeCost> acmeCosts) {
        this.costs = costs;
        this.acmeCosts = acmeCosts;
    }
    
    List<RefundDiff> invoke(List<OrderItem> orderItems) {
        buildCostMaps();
        calculateDifferences(orderItems);
        return differences;
    }
    
    private void buildCostMaps() {
        costMap = costs.stream()
            .collect(Collectors.toMap(Cost::getItemId, c -> c));
        acmeCostMap = acmeCosts.stream()
            .collect(Collectors.toMap(AcmeCost::getItemId, c -> c));
    }
    
    private void calculateDifferences(List<OrderItem> orderItems) {
        differences = new ArrayList<>();
        for (OrderItem item : orderItems) {
            findAndAddDifference(item);
        }
    }
    
    private void findAndAddDifference(OrderItem item) {
        Cost cost = costMap.get(item.getItemId());
        AcmeCost acmeCost = acmeCostMap.get(item.getItemId());
        
        if (cost != null && acmeCost != null) {
            addDifferenceIfSignificant(item, cost, acmeCost);
        }
    }
    
    private void addDifferenceIfSignificant(OrderItem item, Cost cost, AcmeCost acmeCost) {
        double diff = cost.getAmount() - acmeCost.getAmount();
        if (Math.abs(diff) > 0.01) {
            differences.add(new RefundDiff(item, diff));
        }
    }
}

적용 기준

Extract Method Object를 적용해야 하는 경우:

  1. 메서드 길이: 50줄 이상의 긴 메서드
  2. 지역 변수 상호 의존(entanglement): 여러 지역 변수가 메서드 전체에 걸쳐 상호작용
  3. Extract Method 불가: 파라미터가 너무 많아져 Extract Method로 분해 불가
  4. 임시 변수 과다: 중간 결과를 저장하는 임시 변수가 많음
  5. 단계적 계산: 여러 단계의 계산이 순차적으로 진행됨

신규 기능 추가 경로 — 계획된 목표 구조로서의 Method Object

Method Object는 레거시 개선용만이 아니다. 새 기능을 추가할 때 처음부터 목표 구조로 계획하면 다음 3단계 경로가 효과적이다:

  1. 검증 조건을 테스트로 먼저 확정 — 정확한 기대값을 가진 인수 테스트를 구현 전에 작성한다.
  2. 새 클래스 하나에 절차적으로 구현 — WELC(Working Effectively with Legacy Code)의 Sprout Class처럼 정적 메서드 하나를 진입점으로 노출해 기존 호출부 변경을 한 줄로 최소화한다. 이 단계는 절차적으로 구현한다 (make it work).
  3. 절차를 Method Object로 변환 — 상태가 없는 순수 계산이라도, 메서드 간 인자 전달을 최소화하기 위해 입력을 final 필드로 받는 객체로 전환한다. 정적 진입점은 유지한다:
// public API는 처음부터 끝까지 정적 메서드 하나 — 내부가 절차→객체로 바뀌어도 호출부 불변
public static BigDecimal calculate(final Line[] lines) {
    return new TaxCalculator(List.of(lines)).total();
}

이 경로에서는 내부 구조 변화(절차 → 객체, 메서드 분해, 인자 제거)가 호출부에 전혀 전파되지 않아, 각 단계를 독립적인 작은 refactor: 커밋으로 안전하게 진행할 수 있다.

추출 후 데이터 결정 — 무엇을 필드로, 무엇을 파라미터로

Method Object 내부의 데이터는 세 종류로 나뉘고, 종류마다 답이 다르다:

데이터 종류예판정
입력 (생성자로 받는 본질적 상태)주문 라인 목록final 필드 — 전체를 순회·합산하는 메서드들의 인자가 사라진다
파생값 (계산 도중의 중간 결과)과세표준, 세액파라미터 유지(데이터 흐름이 시그니처에 명시) 또는 no-arg 질의로 전환(replace-temp-with-query). 가변 필드에 저장하는 방식은 호출 순서 결합(temporal coupling)을 만들므로 피한다
loop 변수 (반복마다 다른 값)개별 라인파라미터 필수 — 호출마다 값이 달라 필드화 불가능

explicit-parameters 스킬과의 경계: explicit-parameters는 협력 객체 의존성(Singleton·전역 상태·서비스)을 파라미터로 드러내는 기법이다. Method Object의 입력·계산 상태 필드는 그 스킬의 대상이 아니다 — 방향이 반대인 두 스킬은 적용 대상이 달라 충돌하지 않는다.

OUTPUT FORMAT

실행 절차

공통 골격(대상 파일 수집 → 후보 제시(계열별 승인 규칙) → 적용 → 테스트 → 커밋/되돌리기, 브랜치·PR이 필요한 조건)은 이 스킬 디렉터리 기준 ../../references/refactoring-procedure.md가 정본이다. 아래는 이 기법에 고유한 부분만 규정한다.

Method Object 후보 식별 (공통 절차 2단계)

대상 파일에서 다음 패턴을 찾는다:

  • 50줄 이상의 긴 메서드
  • 5개 이상의 지역 변수 선언
  • 지역 변수가 메서드 전체에 걸쳐 읽기/쓰기됨
  • Extract Method 시 파라미터가 4개 이상 필요
  • 여러 단계의 계산이 순차적으로 진행

후보 제시 예시 (공통 절차 3단계)

후보를 보고하고 즉시 적용한다(Tidy 계열 — 정본 §3-A):

## 리팩토링 후보 1: Extract Method Object

**파일**: RefundService.java
**대상**: refundDiff() 메서드 (68줄)

**현재 코드**:
[해당 메서드 전체]

**지역 변수 분석**:
- costMap, acmeCostMap, differences (메서드 전체에서 사용)
- 5개 이상의 지역 변수가 상호 의존함
- Extract Method 불가 (파라미터 6개 필요)

**제안 변경**:
1. RefundDifferenceCalculator 클래스 생성
2. 지역 변수를 필드로 변환
3. costs, acmeCosts를 생성자 파라미터로
4. invoke(orderItems) 메서드로 계산 수행
5. buildCostMaps(), calculateDifferences() 등으로 분해

→ 승인 없이 적용 (Tidy 계열)

Method Object 추출 실행 (공통 절차 4단계)

확정된 리팩토링을 하나씩 수행:

  1. Method Object 클래스 생성 및 메서드 이동
  2. 지역 변수를 필드로 변환
  3. 외부 의존성을 생성자 파라미터로
  4. 작은 메서드로 분해

커밋 메시지 형식:

refactor: extract method object [클래스명] from [원본클래스명].[메서드명]

결과 보고

사용자에게 보고:

  • 적용된 Method Object 목록

FAILURE CONDITIONS

공통 실패 조건(계열별 승인 규칙 위반, 테스트 실패 방치, 테스트 수정, 커밋 단위, git add -A, heredoc 한글 메시지)은 ../../references/refactoring-procedure.md에 있다. 아래는 이 기법에 고유한 것만.

  • 지역 변수가 적은 (3개 이하) 단순 메서드에 적용 (Extract Method로 충분)
  • Method Object 생성 후 작은 메서드로 분해하지 않음 (단순히 코드만 옮김)
  • 필요 이상의 의존성을 생성자로 전달 (원본 클래스 전체 전달 등)

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

同じリポジトリのスキル

概要と使いどころ

동일한 결과를 내는 여러 조건문(OR 나열·중첩 AND)을 하나로 통합하고 의미 있는 boolean 메서드로 추출. "조건문 합쳐", "같은 결과 반환하는 if 정리", "중첩 if 평탄화", "/consolidate-conditional" 요청 시 사용. 단, 여러 메서드에 흩어진 동일 조건을 호출자 쪽으로 올리는 것은 /lift-up-conditional, 복잡한 조건식·분기를 메서드로 쪼개는 것은 /decompose-conditional이 적합. /consolidate-conditional [commit-ref]로 호출.

日本語の概要は準備中です。原文の説明を表示しています。

msbaek/msbaek-claude-plugins82026年10月1日 更新

기능의 external behavior를 Cucumber 인수 테스트(주 검증층)로 구축 — .feature 실행으로 문서↔코드 드리프트를 구조적으로 차단, Four Layer(Steps→Protocol Driver→SUT), 태그 기반 가역 제외, 기존 JUnit 인수 테스트 이관. "인수 테스트 도입", "Gherkin을 실행 가능하게", "cucumber 셋업" 요청 시 사용. /cucumber-acceptance로 호출.

日本語の概要は準備中です。原文の説明を表示しています。

msbaek/msbaek-claude-plugins82026年10月1日 更新

복잡한 if/then/else의 조건식과 각 분기를 의미 있는 메서드로 추출하여 가독성 향상. "조건문 분해", "if 가독성", "복잡한 조건식에 이름 붙여", "/decompose-conditional" 요청 시 사용. 단, 같은 결과를 내는 조건문들을 하나로 합치는 것은 /consolidate-conditional, 타입별 분기를 클래스로 바꾸는 것은 /replace-conditional-with-poly가 적합. /decompose-conditional [commit-ref]로 호출.

日本語の概要は準備中です。原文の説明を表示しています。

msbaek/msbaek-claude-plugins82026年10月1日 更新

Primitive Obsession 제거 — 검증·연산이 따라다니는 primitive 필드(금액+통화, 이메일 문자열 등)를 도메인 개념을 담은 Value Object로 치환. "값 객체 도입", "primitive obsession", "Money 클래스로", "/discover-value-object" 요청 시 사용. 단, 함께 전달되는 파라미터 묶음을 객체로 바꾸는 것은 /introduce-parameter-object, 컬렉션을 감싸는 것은 /first-class-collection이 적합. /discover-value-object [commit-ref]로 호출.

日本語の概要は準備中です。原文の説明を表示しています。

msbaek/msbaek-claude-plugins82026年10月1日 更新

컬렉션 getter가 내부 List/Set을 직접 노출하는 것을 방지 — unmodifiable 반환 + add/remove 메서드 제공. "컬렉션 캡슐화", "getter가 List 그대로 노출", "unmodifiable로", "/encapsulate-collection" 요청 시 사용. 단, 컬렉션과 관련 로직을 전용 클래스로 뽑는 것은 /first-class-collection이 적합. /encapsulate-collection [commit-ref]로 호출.

日本語の概要は準備中です。原文の説明を表示しています。

msbaek/msbaek-claude-plugins82026年10月1日 更新

암묵적 의존성(전역 변수·클래스 필드·싱글턴 접근)을 명시적 파라미터로 전환하여 메서드 투명성 향상. "숨은 의존성 드러내", "필드 대신 파라미터로", "전역 참조 제거", "/explicit-parameters" 요청 시 사용. 단, 파라미터가 많아져 묶어야 하면 /introduce-parameter-object, I/O와 계산 분리는 /segregate-functional-core가 적합. /explicit-parameters [commit-ref]로 호출.

日本語の概要は準備中です。原文の説明を表示しています。

msbaek/msbaek-claude-plugins82026年10月1日 更新

msbaek のスキルをすべて見る

このスキルの問題を報告する