매개변수 객체 만들기
배경
코드를 보다 보면, 여러 개의 데이터 항목이 항상 같이 다니는 경우가 많다. 한 함수에서 다른 함수로 이동할 때 꼭 함께 전달되는 값들 말이다. 마틴 파울러는 이런 상황을 발견하면, 그 데이터들을 하나의 구조로 묶어주는 리팩터링을 권장한다. 바로 "매개변수 객체 만들기"다.
이 작업의 핵심은 단순히 코드를 깔끔하게 정리하는 걸 넘어서, 도메인을 더 명확하게 표현하는 추상화를 도입하는 데 있다. 별 생각 없이 전달하던 값들이 실제로는 하나의 개념으로 묶일 수 있다는 걸 코드 구조로 드러내는 것이다.
이 리팩터링이 주는 이점은 다음과 같다.
- 관계 명확화: 따로 놀던 값들이 하나의 구조로 묶이면서, 이 값들 사이의 관계가 더 뚜렷해진다.
- 매개변수 정리: 함수를 호출할 때 전달해야 할 인자가 줄어든다.
- 일관성 향상: 값을 꺼낼 때 공통된 이름을 쓰기 때문에 코드의 통일성이 높아진다.
- 추상화 격상: 단순히 값을 묶는 걸 넘어서, 하나의 새로운 도메인 개념으로 격상시킬 수 있다. 이건 진짜 강력한 효과다.
절차
- 데이터 구조 정의: 적절한 데이터 구조(예: 클래스를 하나 만든다)가 없다면 먼저 만든다.
- 테스트: 항상 그렇듯, 리팩터링 전에는 테스트부터 돌려서 현재 상태를 확인한다.
- 함수 선언 수정: 함수에 새로 만든 구조를 매개변수로 추가한다.
- 다시 테스트
- 함수 호출부 수정: 함수를 호출하는 곳에서 새 객체를 만들어 넘기도록 수정한다. 하나씩 바꾸면서 그때마다 테스트를 돌린다.
- 내부 코드 수정: 함수 내부에서 기존 개별 매개변수 대신 객체의 필드를 참조하도록 바꾼다.
- 기존 매개변수 제거: 다 바꿨다면, 이제 더는 필요 없는 기존 매개변수를 지운다. 그리고 마지막 테스트.
매개 변수 그룹을 객체로 교체하는 일은 진짜 값진 작업의 준비 단계이다. 클래스로 만들어 두면 관련 동작들을 이 클래스로 옮길 수 있다는 이점이 생긴다.
예시
💥 초기 버전
function readingsOutsideRange(station, min, max) {
return station.readings.filter((r) => r.temp < min || r.temp > max);
}
const alerts = readingsOutsideRange(
station,
operatingPlan.temperatureFloor,
operatingPlan.temperatureCeiling
);
🔧 데이터 구조 정의
class NumberRange {
constructor(min, max) {
this._data = { min: min, max: max };
}
get min() {
return this._data.min;
}
get max() {
return this._data.max;
}
}
🔧 함수 선언 수정
function readingsOutsideRange(station, min, max, range) {
return station.readings.filter((r) => r.temp < min || r.temp > max);
}
🔧 함수 호출부 수정
const range = new NumberRange(
operatingPlan.temperatureFloor,
operatingPlan.temperatureCeiling
);
const alerts = readingsOutsideRange(
station,
operatingPlan.temperatureFloor,
operatingPlan.temperatureCeiling,
range
);
🔧 내부 코드 수정
min 과 max 를 사용하는 부분을 하나씩 지워나가면서 테스트를 진행한다. 최종적인 형태는 다음과 같다.
function readingsOutsideRange(station, range) {
return station.readings.filter((r) => !range.contains(r.temp));
}
✅ 기존 매개변수 제거
const range = new NumberRange(
operatingPlan.temperatureFloor,
operatingPlan.temperatureCeiling
);
const alerts = readingsOutsideRange(
station,
range
);
여러 함수를 클래스로 묶기
배경
여러 함수가 동일한 데이터를 공유하고 있다면, 이 함수들을 하나의 클래스로 묶는 걸 고려할 수 있다. 클래스는 객체 지향 언어의 핵심이기도 하지만, 사실 패러다임에 관계없이 꽤 유용한 구조다.
리팩터링을 하다 보면, 어떤 함수들이 특정 데이터에 강하게 의존하고 있다는 걸 발견하게 된다. 이럴 때 이 함수들을 함께 묶어서 클래스를 만들면, 코드의 구조가 더 명확해지고, 관련 있는 기능들을 한곳에 모아둘 수 있다. 이 클래스는 다른 모듈에서 사용할 수 있도록 참조만 넘기면 되고, 내부 구현은 감출 수 있다.
단순히 코드를 정리하는 데 그치지 않는다. 클래스를 만들고 나면, 이전에는 놓치고 있었던 도메인 연산이 자연스럽게 드러나기도 한다. 그렇게 되면 그 연산도 새로운 클래스의 메서드로 옮겨주면 된다.
절차
- 공통 데이터 캡슐화: 여러 함수가 공유하는 데이터 구조(레코드 등)가 있다면, 이를 하나의 클래스로 감싼다.
- 함수 이동: 이 데이터를 사용하는 함수들을 하나씩 새 클래스로 옮긴다. 데이터에 직접 접근하는 로직이 있다면, 해당 클래스를 통해 접근하도록 수정한다.
- 로직 추출: 공통 데이터를 다루는 로직이 흩어져 있다면, 이걸 함수로 추출해서 새 클래스로 옮긴다. 이 클래스가 데이터와 기능을 모두 책임지도록 만드는 것이 핵심이다.
예시
💥 초기 버전
const reading = {
customer: "ivan",
quantity: 10,
month: 5,
year: 2017
};
// Client 1
const aReading = acquireReading();
const baseCharge =
baseRate(aReading.month, aReading.year) * aReading.quantity;
// Client 2
const aReading = acquireReading();
const base =
baseRate(aReading.month, aReading.year) * aReading.quantity;
const taxableCharge = Math.max(0, base - taxThreshold(aReading.year));
// Client 3
const aReading = acquireReading();
const basicChargeAmount = calculateBaseCharge(aReading);
function calculateBaseCharge(aReading) {
return baseRate(aReading.month, aReading.year) * aReading.quantity;
}
🔧 공통 데이터 캡슐화
class Reading {
constructor(data) {
this._customer = data.customer;
this._quantity = data.quantity;
this._month = data.month;
this._year = data.year;
}
get customer() {
return this._customer;
}
get quantity() {
return this._quantity;
}
get month() {
return this._month;
}
get year() {
return this._year;
}
}
🔧 함수 이동: calculateBaseCharge() 옮기기
// Reading 클래스..
get baseCharge() {
return baseRate(this.month, this.year) * this.quantity;
}
// Client 3
const rawReading = acquireReading();
const aReading = new Reading(rawReading);
const basicChargeAmount = aReading.baseCharge;
// Client 1
const rawReading = acquireReading();
const aReading = new Reading(rawReading);
const baseCharge = aReading.baseCharge;
// Client 2
const rawReading = acquireReading();
const aReading = new Reading(rawReading);
const taxableCharge = Math.max(
0,
aReading.baseCharge - taxThreshold(aReading.year)
);
✅ 로직 추출: 세금을 부과할 소비량을 계산하는 코드를 함수로 추출
// Reading 클래스...
get taxableCharge() {
return Math.max(0, this.baseCharge - taxThreshold(this.year));
}
// Client 3
const rawReading = acquireReading();
const aReading = new Reading(rawReading);
const taxableCharge = aReading.taxableCharge;
여러 함수를 변환 함수로 묶기
배경
데이터를 받아서 여러 정보를 계산해내는 코드는 정말 자주 마주친다. 문제는 이 계산 로직이 중복돼 있다는 점이다. 어떤 값은 A에서 계산하고, 또 어떤 곳에서는 똑같은 계산이 반복되고 있다. 이럴 땐 계산 로직을 하나로 묶어서 재사용할 수 있게 만들어야 한다.
이럴 때 유용한 리팩터링이 바로 변환 함수 만들기다. 변환 함수는 원본 데이터를 받아서 필요한 정보를 한 번에 계산하고, 그걸 새 레코드에 담아 반환한다. 이렇게 해두면 계산 과정을 검토할 일도, 수정할 일도 이 함수 하나만 보면 된다.
물론 비슷한 상황에서 "여러 함수를 클래스로 묶기"를 선택해도 된다. 둘 중 어떤 걸 쓰든 중요한 건 일관성과 유지보수성이다. 일반적으로 원본 데이터를 수정하지 않고 새로운 정보를 도출할 때는 변환 함수가 잘 어울린다. 반면, 원본 데이터를 갱신해야 한다면 클래스로 묶는 쪽이 더 안정적이다. 변환 함수는 불변성 기반 접근에 가깝기 때문이다.
절차
- 변환 함수 만들기: 변환할 레코드를 입력받아서 그대로 반환하는 기본 틀의 변환 함수를 만든다.
- 하나씩 옮기기: 묶을 함수 중 하나를 선택해서, 그 로직을 변환 함수 안으로 옮긴다. 처리된 값을 새 필드에 담아 반환한다.
- 클라이언트 코드 수정: 외부에서 직접 계산하던 코드를 수정해서, 변환 함수가 반환한 레코드의 필드를 참조하도록 고친다.
- 테스트: 바꾼 부분이 제대로 작동하는지 확인한다.
- 나머지 함수 반복 적용: 같은 방식으로 관련 함수들을 하나씩 변환 함수로 옮긴다.
예시
💥 초기 버전
const aReading = acquireReading();
const base = baseRate(aReading.month, aReading.year) * aReading.quantity;
const taxableCharge = Math.max(0, base - taxThreshold(aReading.year));
코드에 위와 같은 계산 코드가 여러 곳에 반복된다고 가정하자. 물론 중복 코드라면 함수 추출하기로 처리할 수도 있지만, 추출한 함수들이 프로그램 곳곳에 흩어져서 나중에 그런 함수가 있는지 조차 모르게 될 가능성이 있다.
🔧 변환 함수 만들기
function enrichReading(original) {
const result = _.cloneDeep(original);
return result;
}
🔧 하나씩 옮기기
const rawReading = acquireReading();
const aReading = enrichReading(rawReading);
const basicChargeAmount = calculateBaseCharge(aReading);
function enrichReading(original) {
const result = _.cloneDeep(original);
result.baseCharge = calculateBaseCharge(result);
return result;
}
🔧 클라이언트 코드 수정
const rawReading = acquireReading();
const aReading = enrichReading(rawReading);
const basicChargeAmount = aReading.baseCharge;
✅ 나머지 함수도 반복 적용
const rawReading = acquireReading();
const aReading = enrichReading(rawReading);
const base = aReading.baseCharge;
const taxableCharge = Math.max(0, base - taxThreshold(aReading.year));
리팩터링: 단계 쪼개기
배경
서로 다른 두 대상을 한 번에 처리하는 복잡한 코드를 보면, 각 대상을 별도의 모듈로 나누는 방법을 고려해야 한다. 잘 분리된 모듈은 서로의 세부사항에 대해 알지 못해도 독립적으로 수정할 수 있게 만들어준다.
가장 간단한 방법 중 하나는 동작을 두 단계로 나누는 것이다. 컴파일러가 대표적인 예시인데, 컴파일러는 입력된 텍스트를 실행 가능한 코드로 변환하는 작업을 한다. 이 과정에서 여러 단계로 나누어 처리하는 방식이 효과적이라는 사실이 오래전부터 알려졌다. 텍스트를 먼저 토큰화하고, 그 토큰을 파싱하여 구문 트리를 만들고, 그 후 최적화 등을 거쳐 목적 코드를 생성하는 식이다. 각 단계는 독립적인 문제에 집중하기 때문에, 다른 단계에 대한 세부적인 이해가 없어도 충분히 작업을 진행할 수 있다.
이처럼 큰 덩치의 소프트웨어에서는 각 단계를 명확히 나누는 것이 유지보수와 확장성에 유리하다. 이 기법을 사용하면 소프트웨어의 복잡성을 줄이면서도 각 기능을 명확히 구분할 수 있다.
절차
- 두 번째 단계 추출: 두 번째 단계에 해당하는 코드를 독립적인 함수로 분리한다.
- 테스트: 함수가 제대로 동작하는지 테스트한다.
- 중간 데이터 구조 도입: 앞서 추출한 함수의 인수로 사용할 중간 데이터 구조를 만든다.
- 테스트: 중간 데이터 구조와 함께 코드가 올바르게 동작하는지 다시 테스트한다.
- 매개변수 검토: 두 번째 단계 함수의 매개변수를 하나씩 검토하여, 첫 번째 단계에서 사용되는 데이터는 중간 데이터 구조로 옮긴다. 각 매개변수를 옮길 때마다 테스트한다.
- 첫 번째 단계 함수로 추출: 첫 번째 단계 코드를 별도의 함수로 추출하면서, 중간 데이터 구조를 반환하도록 만든다.
'Programming > Refactoring' 카테고리의 다른 글
기본적인 리팩터링 (2) (0) | 2025.04.10 |
---|---|
기본적인 리팩터링 (1) (0) | 2025.04.10 |
테스트 구축하기 (0) | 2025.03.20 |
코드에서 나는 악취 (1) | 2025.03.13 |
리팩터링: 첫 번째 예시 (0) | 2025.02.26 |