Introduce Parameter Object / Preserve Whole Object
상황에 따라 두 가지 기법 중 적합한 것을 선택:
- Introduce Parameter Object (IPO): 파라미터에 행위를 추가해야 할 때 → 새 객체 생성 → 행위 이동 → Value Object 발견
- Preserve Whole Object (PWO): 기존 객체에서 값을 꺼내 전달할 때 → 객체 자체를 전달하여 파라미터 수 감소
GOAL
- 성공 = 반복 파라미터가 IPO 또는 PWO로 정리되어 커밋 완료됨
- IPO: 파라미터 그룹이 Parameter Object로 치환되고 관련 행위가 이동됨
- PWO: 객체에서 꺼낸 값들이 객체 자체 전달로 대체됨
- 사용자 확인 후 적용
- 모든 테스트 통과
기법 선택 기준
| 상황 | 적용 기법 | 이유 |
|---|
| 파라미터들이 이미 하나의 객체에서 꺼내져 전달됨 | Preserve Whole Object | 객체가 이미 존재 — 꺼내지 말고 그대로 전달 |
| 파라미터에 대한 행위를 추가해야 하는 경우 | Introduce Parameter Object | 새 객체 생성 → 행위 이동 → Value Object 발견 |
IPO의 진짜 목적
"더 큰 혜택은 파라미터 그룹에 대해서 동작하는 행위들을 파라미터 객체로 이동시킬 수 있는 것" — Fowler, Ch10
IPO는 파라미터 묶기 자체가 목적이 아니라, Value Object를 발견하는 시작점:
Introduce Parameter Object
→ Move Instance Method (관련 로직을 Parameter Object로 이동)
→ Value Object 탄생 (데이터 + 행위를 가진 객체)
적용하면 안 되는 경우
| 상황 | 이유 |
|---|
| 파라미터 간에 논리적 관계가 없음 | 논리 관계 없이 그룹화하면 의미 없는 객체가 생성됨 |
| 한 메서드에서만 사용되는 파라미터 조합 | 반복이 없으면 추상화의 가치 없음 |
| 파라미터 객체에 이동할 행위가 없음 | 단순 DTO가 되어 Anemic Domain Model 유발 (이 경우 PWO가 더 적합할 수 있음) |
CONSTRAINTS
- 계열: System-wide — 후보 제시 후 승인 받고 적용 (
../../references/refactoring-procedure.md §0·§3-B)
Hard Rules
- 동작 변경 금지 — 구조 개선만 수행
- 테스트 수정 금지 — 구조 변경이 테스트를 실패시키면 되돌리기
- 사용자 확인 없이 리팩토링 금지 — 모든 후보는 사용자 승인 후 실행
- 커밋 단위 — 1파일 x 1기법 = 1커밋 (논리적으로 연결된 파일은 함께)
- git add -A 금지 — 변경된 파일만 명시적으로 추가
적용 패턴
Introduce Parameter Object 리팩토링 단계:
-
반복 파라미터 그룹 식별
- 3개 이상의 파라미터가 함께 전달되는 패턴
- 2곳 이상에서 동일한 파라미터 조합 반복
-
Parameter Object 클래스 생성
- 논리적으로 관련된 파라미터를 하나의 클래스로
- record 또는 immutable class 사용 (Java 14+)
- 의미 있는 이름 부여
-
메서드 시그니처 변경
- 개별 파라미터를 Parameter Object로 치환
- 호출자 코드 업데이트
-
관련 동작 이동 (선택적)
- Parameter Object와 관련된 로직을 객체 내부로 이동
- Value Object로 발전 가능
Before/After 예시
// Before: 반복되는 파라미터 그룹
public class PriceService {
public void processPrice(int originalPrice, int discountedPrice, String currency) {
// 처리 로직
validatePrice(originalPrice, discountedPrice, currency);
formatPrice(originalPrice, discountedPrice, currency);
}
private void validatePrice(int originalPrice, int discountedPrice, String currency) {
if (originalPrice < 0 || discountedPrice < 0) {
throw new IllegalArgumentException("Price cannot be negative");
}
if (discountedPrice > originalPrice) {
throw new IllegalArgumentException("Discounted price cannot exceed original");
}
}
private String formatPrice(int originalPrice, int discountedPrice, String currency) {
double discount = (originalPrice - discountedPrice) * 100.0 / originalPrice;
return String.format("%s %.0f%% off: %d -> %d",
currency, discount, originalPrice, discountedPrice);
}
}
// After: Parameter Object 적용
record Price(int original, int discounted, String currency) {
// 생성자에서 검증
public Price {
if (original < 0 || discounted < 0) {
throw new IllegalArgumentException("Price cannot be negative");
}
if (discounted > original) {
throw new IllegalArgumentException("Discounted price cannot exceed original");
}
}
// 관련 동작을 객체 내부로 이동
public double discountPercentage() {
return (original - discounted) * 100.0 / original;
}
public String formatted() {
return String.format("%s %.0f%% off: %d -> %d",
currency, discountPercentage(), original, discounted);
}
}
public class PriceService {
public void processPrice(Price price) {
// Parameter Object 하나만 전달
// 검증은 Price 생성자에서 자동 수행
String formatted = price.formatted();
}
}
Preserve Whole Object 패턴
// Before: 객체에서 값을 꺼내서 전달
int low = temperatureRange.getLow();
int high = temperatureRange.getHigh();
boolean withinRange = plan.isWithinRange(low, high);
// After: 객체 자체를 전달
boolean withinRange = plan.isWithinRange(temperatureRange);
// Before: DTO에서 여러 값을 꺼내서 전달
String name = request.getName();
String email = request.getEmail();
int age = request.getAge();
User user = createUser(name, email, age);
// After: 객체 자체를 전달
User user = createUser(request);
적용 기준
Introduce Parameter Object를 적용해야 하는 경우:
- 파라미터 수: 3개 이상의 파라미터가 함께 전달
- 반복 패턴: 동일한 파라미터 조합이 2회 이상 반복
- 논리적 관계: 파라미터들이 논리적으로 관련 있음
- 함께 검증: 파라미터들이 함께 검증되어야 함
- 동시 변경: 파라미터들이 함께 변경되는 경향
OUTPUT FORMAT
실행 절차
공통 골격(대상 파일 수집 → 후보 제시(계열별 승인 규칙) → 적용 → 테스트 → 커밋/되돌리기, 브랜치·PR이
필요한 조건)은 이 스킬 디렉터리 기준 ../../references/refactoring-procedure.md가 정본이다.
아래는 이 기법에 고유한 부분만 규정한다.
반복 파라미터 그룹 후보 식별 (공통 절차 2단계)
대상 파일에서 다음 패턴을 찾는다:
패턴 A: Introduce Parameter Object
- 3개 이상의 파라미터를 받는 메서드
- 동일한 파라미터 조합이 2곳 이상에서 반복
- 파라미터들이 논리적으로 관련 있음 (같은 도메인 개념)
- 파라미터에 대한 계산/검증 로직이 여러 곳에서 중복
- Move Method의 전 단계로 파라미터를 묶어야 하는 경우
패턴 B: Preserve Whole Object
- 하나의 객체에서 여러 값을 꺼내어 다른 메서드에 전달
obj.getX(), obj.getY(), obj.getZ()를 꺼낸 뒤 method(x, y, z) 호출
- 객체 자체를 전달하면 파라미터 수가 줄어드는 경우
후보 제시 예시 (공통 절차 3단계)
후보를 하나씩 제시하고 사용자 확인:
## 리팩토링 후보 1: Introduce Parameter Object
**파일**: PriceService.java
**반복 파라미터**: (int originalPrice, int discountedPrice, String currency)
**반복 위치** (3곳):
1. processPrice() 메서드
2. validatePrice() 메서드
3. formatPrice() 메서드
**현재 시그니처**:
void processPrice(int originalPrice, int discountedPrice, String currency)
void validatePrice(int originalPrice, int discountedPrice, String currency)
String formatPrice(int originalPrice, int discountedPrice, String currency)
**제안 변경**:
1. Price record 생성 (original, discounted, currency)
2. 검증 로직을 Price 생성자로 이동
3. 포맷팅 로직을 Price.formatted()로 이동
4. 모든 메서드 시그니처를 Price로 변경
**적용할까요?** (yes / no / 수정 요청)
## 리팩토링 후보 2: Preserve Whole Object
**파일**: HeatingPlan.java
**현재**: temperatureRange에서 low, high를 꺼내어 isWithinRange(low, high) 호출
**제안**: isWithinRange(temperatureRange)로 변경
적용할까요? (yes / no / 수정 요청)
- 사용자가 yes → 실행 목록에 추가
- 사용자가 no → 스킵
- 사용자가 수정 요청 → 요청 반영 후 재제시
모든 후보 확인 후 최종 실행 목록을 보여주고 진행 여부 확인.
Parameter Object 도입 실행 (공통 절차 4단계)
확정된 리팩토링을 하나씩 수행:
- Parameter Object 클래스(record) 생성
- 검증 로직을 생성자로 이동
- 관련 동작을 객체 메서드로 이동
- 메서드 시그니처 변경
- 호출자 코드 업데이트
커밋 메시지 형식:
refactor: introduce parameter object [객체명] in [클래스명]
결과 보고
사용자에게 보고:
실전 리팩토링 경로 (Vault 사례)
사례 1: SequenceKey
Extract Method → Introduce Parameter Object → Move Instance Method → Convert Record to Class
효과: sequenceKey가 String → Value Object로 승격. format, increase 로직이 Value Object로 이동.
사례 2: ProductPrice
Split Conditional → Extract Method → Introduce Parameter Object → Move Instance Method
효과: isDiscounted, getFinalPrice가 ProductPrice로 이동. 독립 테스트 가능.
사례 3: StockChecker (빵속빵 패턴)
빵속빵(FCIS) 구조 만들기 → DTO 의존성을 파라미터로 전환 → Parameter Object 생성 → 관련 메서드 이동
패턴: Move Method 전 단계로 파라미터를 먼저 묶음.
공통 패턴
IPO → Move Instance Method → Value Object 탄생
FAILURE CONDITIONS
공통 실패 조건(계열별 승인 규칙 위반, 테스트 실패 방치, 테스트 수정, 커밋 단위, git add -A, heredoc
한글 메시지)은 ../../references/refactoring-procedure.md에 있다. 아래는 이 기법에 고유한 것만.
- 논리적 관계 없는 파라미터를 억지로 묶음 (불필요한 결합)
- 1곳에만 사용되는 파라미터 그룹에 적용 (과도한 추상화)
- mutable Parameter Object 생성 (불변성 보장 필요)