읽을거리/리팩터링

06 기본적인 리팩터링 - 매개변수 객체 만들기

씨씨상 2024. 12. 5. 18:23

 

 

매개변수 객체 만들기

 

 

 

 

function amountInvoiced(startDate, endDate) {...}
function amountReceived(startDate, endDate) {...}
function amountOverdue(startDate, endDate) {...}

 

 

function amountInvoiced(aDateRange) {...}
function amountReceived(aDateRange) {...}
function amountOverdue(aDateRange) {...}

 

 

배경

데이터 항목 여러 개가 이 함수에서 저 함수로 함께 몰려다니는 경우를 자주 본다. 나는 이런 데이터 무리를 발견하면 데이터 구조 하나로 모아주곤 한다.

 

데이터 뭉치를 데이터 구조로 묶으면 데이터 사이의 관계가 명확해진다는 이점을 얻는다. 게다가 함수가 이 데이터 구조를 받게 하면 매개변수 수가 줄어든다. 같은 데이터 구조를 사용하는 모든 함수가 원소를 참조할 때 항상 똑같은 이름을 사용하기 때문에 일관성도 높여준다.

 

하지만 이 리팩터링의 진정한 힘은 코드를 더 근본적으로 바꿔준다는 데 있다. 나는 이런 데이터 구조를 새로 발견하면 이 데이터 구조를 활용하는 형태로 프로그램 동작을 재구성한다. 데이터 구조에 담길 데이터에 공통으로 적용되는 동작을 추출해서 함수로 만드는 것이다(공용 함수를 나열하는 식으로 작성할 수도 있고, 이 함수들과 데이터를 합쳐 클래스로 만들 수도 있다). 이 과정에서 새로 만든 데이터 구조가 문제 영역을 훨씬 간결하게 표현하는 새로운 추상 개념으로 격상되면서, 코드의 개념적인 그림을 다시 그릴 수도 있다. 그러면 놀라울 정도로 강력한 효과를 낸다. 하지만 이 모든 것의 시작은 매개변수 객체 만들기부터다.

 

 

절차

  1. 적당한 데이터 구조가 아직 마련되어 있지 않다면 새로 만든다
    • → 개인적으로 클래스로 만드는 걸 선호한다. 나중에 동작까지 함께 묶기 좋기 때문이다. 나는 주로 데이터 구조를 값 객체Value Object로 만든다.
  2. 테스트한다.
  3. 함수 선언 바꾸기로 새 데이터 구조를 매개변수로 추가한다.
  4. 테스트한다.
  5. 함수 호출 시 새로운 데이터 구조 인스턴스를 넘기도록 수정한다. 하나씩 수정할 때마다 테스트한다.
  6. 기존 매개변수를 사용하던 코드를 새 데이터 구조의 원소를 사용하도록 바꾼다.
  7. 다 바꿨다면 기존 매개변수를 제거하고 테스트한다.

 

 

예시

온도 측정값reading 배열에서 정상 작동 범위를 벗어난 것이 있는지 검사하는 코드를 살펴보자. 온도 측정값을 표현하는 데이터는 다음과 같다.

 

 const station = {
  name: 'ZB1',
  readings: [
    { temp: 47, time: '2016-11-10 09:10' },
    { temp: 53, time: '2016-11-10 09:20' },
    { temp: 58, time: '2016-11-10 09:30' },
    { temp: 53, time: '2016-11-10 09:40' },
    { temp: 51, time: '2016-11-10 09:50' },
  ],
 ;

 

다음은 정상 범위를 벗어난 측정값을 찾는 함수다.

 

function readingsOutsideRange(station, min, max) {
  return station.readings.filter((r) => r.temp < min || r.temp > max);
}

 

이 함수는 다음과 같이 호출될 수 있다.

 

alerts = readingOutsideRange(
  station,
  operatingPlan.temperatureFloor, // 최저 온도
  operatingPlan.temperatureCeiling  // 최고 온도
);

 

호출 코드를 보면 operatingPlan의 데이터 항목 두 개를 쌍으로 가져와서 readingsOutsideRange()로 전달한다. 그리고 operatingPlan은 범위의 시작과 끝 이름을 readingsOutsideRange()와 다르게 표현한다. 이와 같은 범위range라는 개념은 객체 하나로 묶어 표현하는 게 나은 대표적인 예다. 먼저 묶은 데이터를 표현하는 클래스부터 선언하자.

 

class NumberRange {
  constructor(min, max) {
    this._data = { min: min, max: max };
  }
  get min() {
    return this._data.min;
  }
  get max() {
    return this._data.max;
  }
}

 

여기서는 기본 자바스크립트 객체가 아닌 클래스로 선언했는데, 이 리팩터링은 새로 생성한 객체로 동작까지 옮기는 더 큰 작업의 첫 단계로 수행될 때가 많기 때문이다. 이 시나리오에는 클래스가 적합하므로 곧바로 클래스를 사용했다. 한편 값 객체로 만들 가능성이 높기 때문에 세터는 만들지 않는다. 내가 이 리팩터링을 할 때는 대부분 값 객체를 만들게 된다.

 

그런 다음 새로 만든 객체를 readingsOutsideRange()의 매개변수로 추가하도록 함수 선언을 바꾼다.

 

function readingsOutsideRange(station, min, max, range) {
  return station.readings.filter((r) => r.temp < min || r.temp > max);
}

 

자바스크립트라면 호출문을 예전 상태로 둬도 되지만, 다른 언어를 사용할 때는 다음과 같이 새 매개변수 자리에 널null을 적어둔다.

 

alerts = readingOutsideRange(
  station,
  operatingPlan.temperatureFloor,
  operatingPlan.temperatureCeiling,
  null
);

 

아직까지 동작은 하나도 바꾸지 않았으니 테스트는 문제없이 통과할 것이다. 이제 온도 범위를 객체 형태로 전달하도록 호출문을 하나씩 바꾼다.

 

const range = new NumberRange(operatingPlan.temperatureFloor, operatingPlan.temperatureCeiling);
alerts = readingOutsideRange(
  station,
  operatingPlan.temperatureFloor,
  operatingPlan.temperatureCeiling,
  range
);

 

여기서도 동작은 바뀌지 않았다. 새로 건넨 매개변수를 아직 사용하지 않기 때문이다. 따라서 이번에도 모든 테스트를 무난히 통과한다.

 

이제 기존 매개변수를 사용하는 부분을 변경한다. 최댓값부터 바꿔보자.

 

alerts = readingOutsideRange(
  station,
  operatingPlan.temperatureFloor,
  range
);
function readingsOutsideRange(station, min, range) {
  return station.readings.filter((r) => r.temp < min || r.temp > range.max);
}

 

여기서 한 번 테스트한 뒤, 다음 매개변수도 제거한다.

 

function readingsOutsideRange(station, range) {
  return station.readings.filter((r) => r.temp < range.min || r.temp > range.max);
}
const range = new NumberRange(operatingPlan.temperatureFloor, operatingPlan.temperatureCeiling);
alerts = readingOutsideRange(
  station,
  range
);

 

이상으로 매개변수 객체 만들기가 끝났다.

 

 

진정한 값 객체로 거듭나기

앞서 운을 띄웠듯이 매개변수 그룹을 객체로 교환하는 일은 진짜 값진 작업의 준비단계일 뿐이다. 앞에서처럼 클래스로 만들어두면 관련 동작들을 이 클래스로 옮길 수 있다는 이점이 생긴다. 이 예에서는 온도가 허용 범위 안에 있는지 검사하는 메서드를 클래스에 추가할 수 있다.

function readingsOutsideRange(station, range) {
  return station.readings.filter((r) => !range.contains(r.temp));
}
contains(arg) {
  return arg >= this.min && arg <= this.max;
}​

지금까지 한 작업은 여러 가지 유용한 동작을 갖춘 범위(Range) 클래스를 생성하기 위한 첫 단계다. 코드에 범위 개념이 필요함을 깨달았다면 최댓값과 최솟값 쌍을 사용하는 코드를 발견할 때마다 범위 객체로 바꾸자(당장 operatingPlan의 temperatureFloor와 temperatureCeiling을 temperatureRange로 교체할 수 있다). 이러한 값 쌍이 어떻게 사용되는지 살펴보면 다른 유용한 동작도 범위 클래스로 옮겨서 코드베이스 전반에서 값을 활용하는 방식을 간소화할 수 있다. 나라면 진정한 값 객체로 만들기 위해 값에 기반한 동치성 검사 메서드equality method부터 추가할 것이다.

 

 

 

 

 

[출처]

리팩터링 2판 - 마틴 파울러

 

[정리]

값을 하나로 묶을 수 있는 데이터 무리가 있다면 객체 형식으로 묶어준다. 데이터 무리를 객체로 묶은 후 인자로 전달하고, 매개변수를 하나 바꾼 후 테스트, 이상이 없다면 나머지도 바꿔준다. 일반 객체가 아니라 클래스 형태로 묶을 경우 관련된 동작을 클래스 안에 선언할 수 있어 활용도가 높아진다.