跳到正文
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(原文)

安装前可以查看交给 AI 代理的指令内容。

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 생성 후 작은 메서드로 분해하지 않음 (단순히 코드만 옮김)
  • 필요 이상의 의존성을 생성자로 전달 (원본 클래스 전체 전달 등)

评价

还没有评价。欢迎分享你的使用感受。

评论

无需登录即可发表。欢迎分享使用感受、提问,或说明在哪些环境下无法运行。

还没有评论。来写第一条吧。

提交后会立即公开。请勿填写邮箱、电话等个人信息。URL 最多 2 个。

0 / 1000 字

此表单受 reCAPTCHA 保护,适用 Google 的隐私权政策和服务条款。

同一仓库的技能

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

中文简介正在准备中。当前显示原文说明。

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

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

中文简介正在准备中。当前显示原文说明。

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

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

中文简介正在准备中。当前显示原文说明。

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

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月16日 更新

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

中文简介正在准备中。当前显示原文说明。

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

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

中文简介正在准备中。当前显示原文说明。

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

查看 msbaek 的全部技能

报告此技能的问题