11장, API 리팩터링

2021.11.08

1. Separate Query from Modifier, 질의 함수와 변경 함수 분리하기

  • 하나의 함수내에서 질의와 변경을 동시에 행하고 있다면 분리하자.

절차

  1. 대상 함수를 복제하고 질의 목적에 충실한 이름을 짓는다.
  2. 새 질의 함수에서 부수효과를 모두 제거한다.
  3. 정적 검사를 수행한다.
  4. 원래 함수(변경 함수)를 호출하는 곳을 모두 찾아낸다. 호출하는 곳에서 반환 값을 사용한다면 질의함수를 호출하도록 바꾸고, 원래 함수를 호출하는 코드를 바로 아래 줄에 새로 추가한다. 하나 수정할때마다 테스트한다.
  5. 원래 함수에서 질의 관련 코드를 제거한다.
  6. 테스트.

예시

before

function getTotalOutstandingAndSendBill() {
  const result = customer.invoices.reduce((total, each) => each.amount + total, 0);
  sendBill();
  return result;
}

after

function totalOutstanding() {
  return customer.invoices.reduce((total, each) => each.amount + total, 0);
}
function sendBill() {
  emailGateway.send(formatBill(customer));
}

2. Parameterize Function, 함수 매개변수화하기

  • 두 함수의 로직이 비슷하고 리터럴 값만 다르다면, 이 값을 매개변수로 받아처리한다.

절차

  1. 비슷한 함수 중 하나를 선택한다.
  2. 함수 선언 바꾸기로 리터럴들을 매개변수로 추가한다.
  3. 이 함수를 호출하는 곳 모두에 적절한 리터럴 값을 추가한다.
  4. 테스트.
  5. 매개변수로 받은 값을 사용하도록 함수 본문을 수정한다. 하나 수정할 때마다 테스트.
  6. 비슷한 다른 함수를 호출하는 코드를 찾아 매개변수화된 함수를 호출하도록 하나씩 수정한다. ( 하나씩 수정할때마다 테스트 )

예시

before

function tenPercentRaise(aPerson) {
  aPerson.salary = aPerson.salary.multiply(1.1);
}
function fivePercentRaise(aPerson) {
  aPerson.salary = aPerson.salary.multiply(1.05);
}

after

function raise(aPerson, factor) {
  aPerson.salary = aPerson.salary.multiply(1 + factory);
}

3. Remove Flag Argument, 플래그 인수 제거하기

  • 플래그 인수란 호출되는 함수가 실행할 로직을 호출하는 쪽에서 선택하기 위해 전달하는 인수이다.
  • 불리언 타입의 플래그는 코드를 읽는 이에게 뜻을 온전히 전달하지 못하기 때문에 더욱 좋지 못하다.
  • 기능 하나를 제공하는 명시적인 함수를 작성하는 것이 깔끔하다.
  • 함수 내부에서 매개변수를 분배로직이 까다로울때는 원본 함수를 래핑하는 별도의 함수를 만들 수 도 있다.

절차

  1. 매개변수로 주어질 수 있는 값 각각에 대응하는 명시적 함수들을 생성한다.
  2. 원래 함수를 호출하는 코드들을 모두 찾아서 각 리터럴 값에 대응되는 명시적 함수를 호출하도록 수정한다.

예시

before

function setDimension(name, value) {
  if (name === 'height') {
    this._height = value;
    return;
  }
  if (name === 'width') {
    this._width = value;
    return;
  }
}

after

function setHeight(value) {
  this._height = value;
}
function setWidth(value) {
  this._width = value;
}

4. Preserve Whole Object,객체 통째로 넘기기

  • 하나의 객체에서 값 두 개 이상을 가져와서 인수로 넘기는 코드를 보면, 그 값들 대신 객체 자체를 통째로 넘기자.
  • 함수가 객체 자체에 의존하기를 원치 않을 때는 이 리팩터링을 수행하지 않는다.
    • 함수와 객체가 서로 다른 모듈에 속한 상황일 때

절차

  1. 매개변수들을 원하는 형태로 받는 빈 함수를 만든다.
  2. 새 함수의 본문에서는 원래 함수를 호출하도록 하며, 새 매개변수와 원래 함수의 매개변수를 매핑한다.
  3. 정적 검사를 수행한다.
  4. 모든 호출자가 새 함수를 사용하게 수정한다. 하나씩 수정하며 테스트.
  5. 호출자를 모두 수정했다면 원래 함수를 인라인한다.
  6. 새 함수의 이름을 적절히 수정하고, 모든 호출자에 반영한다.

예시

before

const low = aRoom.daysTempRange.low
const high = aRoom.daysTempRange.high
if(aPlan.withinRange(low, high))

after

if(aPlan.withinRange(aRoom.daysTempRange))

5. Replace Parameter with Query, 매개변수를 질의 함수로 바꾸기

  • 매개변수 목록은 함수의 변동 요인을 모아놓은 곳이다. 함수의 동작에 변화를 줄 수 있는 일차적인 수단이다.
  • 피호출 함수가 스스로 쉽게 결정할수 있는 값을 매개변수로 건네는 것도 일종의 중복이다.
  • 매개변수를 제거하면서 피호출 함수에 원치 않는 의존성이 생길 때는 매개변수를 제거해서는 안된다.
  • 함수는 순수함수로 작성이 되어야한다. 따라서, 매개변수를 없애는 대신 가변 전역 변수를 이용하는 일은 하면 안된다.

절차

  1. 필요하다면 대상 매개변수의 값을 계산하는 코드를 별도 함수로 추출해놓는다.
  2. 함수 본문에서 대상 매개변수로의 참조를 모두 찾아서 그 매개변수의 값을 만들어주는 표현식을 참조하도록 바꾼다.
  3. 함수 선언 바꾸기로 대상 매개변수를 없앤다.

예시

before

availableVacation(anEmployee, anEmployee.grade);
function availableVacation(anEmployee, grade) {
  /*...*/
}

after

availableVacation(anEmployee);
function availableVacation(anEmployee) {
  const grade = anEmployee.grade;
  // ...
}

6. Replace Query with Parameter, 질의 함수를 매개변수로 바꾸기

  • 함수 안에 두기 거북한 참조를 발견할 때가 많다. ( 전역 변수를 참조한다거나, 제거하길 원하는 원소를 참조하는 경우 )
  • 순수함수를 작성하고, 프로그램의 입출력과 가변 원소들을 다루는 로직으로 순수함수를 감싸는 패턴을 많이 활용한다.
    • 결과적으로 이부분은 테스트하거나 다루기가 쉬워진다.
  • 질의 함수를 매개변수로 바꾸면 어떤 값을 제공할지를 호출자가 알아내야 한다. 이러면 호출자가 복잡해지는 경우도 발생한다.
  • 자바스크립트의 클래스 모델에서는 객체 안의 데이터를 직접 얻어낼 방법이 항상 존재하기 때문에 불변 클래스임을 보장하는 수단이 없다.
    • 불변으로 설계했음을 알리고 그렇게 사용하라고 제안하는 것만으로 충분한 값어치를 할 때가 많다.

절차

  1. 변수 추출하기로 질의 코드를 함수 본문의 나머지 코드와 분리한다.
  2. 함수 본문 중 해당 질의를 호출하지 않는 코드들을 별도 함수로 추출한다.
  3. 방금 만든 변수를 인라인하여 제거한다.
  4. 원래 함수도 인라인한다.
  5. 새 함수의 이름을 원래 함수의 이름으로 고쳐준다.

예시

before

targetTemperature(aPlan);
function targetTemperature(aPlan) {
  currentTemperature = thermostat.currentTemperature;
  // ...
}

after

targetTemperature(aPlan, thermostat.currentTemperature);
function targetTemperature(aPlan, currentTemperature) {
  // ...
}

7. Remove Setting Method, 세터 제거하기

  • 객체 생성 후 수정되지 않기를 원하는 필드라면 세터를 제공하지 말자.
  • 아래 2가지 경우에는 이 리팩터링을 진행하는게 좋다.
    • 사람들이 접근자 메서드를 통해서만 필드를 다루려 할 때. ( 심지어 생성자 안에서도 )
    • 생성 스크립트를 사용하여 객체를 생성할 때.

      생성자를 호출한 후 일련의 세터를 호출하여 객체를 완성하는 형태의 코드.

절차

  1. 설정해야 할 값을 생성자에서 받지 않는다면 그 값을 받을 매개변수를 생성자에 추가한다. 그런 다음 생성자 안에서 적절한 세터를 호출한다.
  2. 생성자 밖에서 세터를 호출하는 곳을 찾아 제거하고, 대신 새로운 생성자를 사용하도록 한다. 하나씩 수정할 때마다 테스트.
  3. 세터 메서드를 인라인한다. 가능하다면 해당 필드를 불변으로 만든다.
  4. 테스트.

예시

before

class Person {
  get name() {
    /*...*/
  }
  set name(aString) {
    /*...*/
  }
}

after

class person {
  get name() {
    /*...*/
  }
}

8. Replace Constructor with Factory Function, 생성자를 팩터리 함수로 바꾸기

  • 생성자에는 일반 함수에는 없는 이상한 제약이 따라붙기도 한다.
    • 자바의 경우 서브클래스의 인스턴스나 프락시를 반환할 수 없다.
    • 생성자를 호출하려면 new 연산자를 써야해서 일반 함수가 오길 기대하는 자리에는 쓰기 어렵다.

절차

  1. 팩터리 함수를 만든다. 팩터리 함수의 본문에서는 원래의 생성자를 호출한다.
  2. 생성자를 호출하던 코드를 팩터리 함수 호출로 바꾼다.
  3. 하나씩 수정할 때마다 테스트.
  4. 생성자의 가시 범위가 최소가 되도록 제한한다.

예시

before

leadEngineer = new Employee(document.leadEngineer, 'E');

after

// after
leadEngineer = createEngineer(document.leadEngineer);

9. Replace Function with Command, 함수를 명령으로 바꾸기

  • 함수를 객체 안으로 캡슐화하면 더 유용해지는 상황이 있다.
    • 되돌리기와 같은 보조 연산을 제공할 수 있으며, 수명주기를 더 정밀하게 제어하는 데 필요한 매개변수를 만들어주는 메서드도 제공할 수 있다.
    • 일급함수를 지원하지 않는 프로그래밍 언어를 사용할 때는 이를 흉내낼 수 있으며 중첩 함수들을 지원하지 않으면 메서드와 필드를 이용해 함수를 쪼갤 수 있다.
  • 자바스크립트는 언어적으로 중첩함수, 일급함수 작성이 가능하니 굳이 이러한 명령 패턴을 사용할 필요는 없을 것 같다.

절차

  1. 대상 함수의 기능을 옮길 빈 클래스를 만든다. 클래스 이름은 함수 이름에 기초해 짓는다.
  2. 방금 생성한 빈 클래스로 함수를 옮긴다.
  3. 함수의 인수들 각각은 명령의 필드로 만들어 생성자를 통해 설정할지 고민해본다.

예시

before

function score(candidate, medicalExam, scoringGuide) {
  let result = 0;
  let healthLevel = 0;
  // 긴 코드 생략
}

after

class Scorer {
  constructor(candidate, medicalExam, scoringGuide) {
    this._candidate = candidate;
    this._medicalExam = medicalExam;
    this._scoringGuide = scoringGuide;
  }

  execute() {
    this._result = 0;
    this._healthLevel = 0;
    // 긴 코드 생략
  }
}

10. Replace Command with Function, 명령을 함수로 바꾸기

  • 명령은 그저 함수를 하나 호출해 정해진 일을 수행하는 용도로 주로 쓰인다.
  • 로직이 크게 복잡하지 않다면 장점보다 단점이 크니 평범한 함수로 바꿔주는게 낫다.

절차

  1. 명령을 생성하는 코드와 명령의 실행 메서드를 호출하는 코드를 함께 함수로 추추루한다.
  2. 명령의 실행 함수가 호출하는 보조 메서드들 각각을 인라인한다.
  3. 함수 선언 바꾸기를 적용하여 생성자의 매개변수 모두를 명령의 실행 메서드로 옮긴다.
  4. 명령의 실행 메서드에서 참조하는 필드들 대신 대응하는 매개변수를 사용하게끔 바꾼다. 하나씩 수정할 때마다 테스트.
  5. 생성자 호출과 명령의 실행 메서드 호출을 호출자 안으로 인라인한다.
  6. 테스트.
  7. 죽은 코드 제거하기로 명령 클래스를 없앤다.

예시

before

class ChargeCalculator {
  constructor(customer, usage) {
    this._customer = customer;
    this._usage = usage;
  }
  execute() {
    return this._customer.rate * this._usage;
  }
}

after

function charge(customer, usage) {
  return customer.rate * usage;
}

11. Return Modified Value, 수정된 값 반환하기

  • 데이터가 수정된다면 그 사실을 명확히 알려주어서 함수가 무슨 일을 하는지 쉽게 알 수 있게 하는 것이 중요하다.
  • 변수를 갱신하는 함수라면 수정된 값을 반환하여 호출자가 그 값을 변수에 담아두도록 하는 것이다.

절차

  1. 함수가 수정된 값을 반환하게 하여 호출자가 그 값을 자신의 변수에 저장하게 한다.
  2. 테스트.
  3. 피호출 함수 안에 반환할 값을 가리키는 새로운 변수를 선언한다.
  4. 테스트.
  5. 계산이 선언과 동시에 이뤄지도록 통합한다.
  6. 테스트.
  7. 피호출 함수의 변수 이름을 새 역할에 어울리도록 바꿔준다.
  8. 테스트.

예시

before

let totalAscent = 0;
calculateAscent();
function calculateAscent() {
  for (let i = 1; i < points.length; i++) {
    const verticalCharge = points[i].elevation - points[i - 1].elevation;
    totalAscent += verticalChange > 0 ? verticalCharge : 0;
  }
}

after

const totalAscent = calculateAscent();

function calculateAscent() {
  let result = 0;
  for (let i = 1; i < points.length; i++) {
    const verticalCharge = points[i].elevation - points[i - 1].elevation;
    result += verticalChange > 0 ? verticalCharge : 0;
  }
  return result;
}

12. Replace Error Code with Exception, 오류 코드를 예외로 바꾸기

  • 예외는 프로그래밍 언어에서 제공하는 독립적인 오류 처리 메커니즘이다.

절차

  1. 콜스택 상위에 해당 예외를 처리할 예외 핸들러를 작성한다.
  2. 테스트.
  3. 해당 오류 코드를 대체할 예외와 그 밖의 예외를 구분할 식별 방법을 찾는다.
  4. 정적 검사를 수행한다.
  5. catch절을 수정하여 직접 처리할 수 있는 예외는 적절히 대처하고 그렇지 않은 예외는 다시 던진다.
  6. 테스트.
  7. 오류 코드를 반환하는 곳 모두에서 예외를 던지도록 수정한다. 하나씩 수정할 때마다 테스트한다.
  8. 모두 수정했다면 그 오류 코드를 콜스택 위로 전달하는 코드를 모두 제거한다. 하나씩 수정할 때마다 테스트한다.

예시

before

if (data) return new ShippingRules(data);
else return -23;

after

if (data) return new ShippingRules(data);
else return new OrderProcessingError(-23);

13. Replace Exception with Precheck, 예외를 사전확인으로 바꾸기

  • 함수 수행 시 문제가 될 수 있는 조건을 함수 호출 전에 검사할 수 있다면 예외를 던지는 대신 호출하는 곳에서 조건을 검사해야 한다.

절차

  1. 예외를 유발하는 상황을 검사할 수 있는 조건문을 추가한다. catch 블록의 코드를 조건문의 조건절 중 하나로 옮기고남은 try 블록의 코드를 다른 조건절로 옮긴다.
  2. catch 블록에 어서션을 추가하고 테스트.
  3. try문과 catch 블록을 제거.
  4. 테스트.

예시

before

double getValueForPeriod(int periodNumber) {
  try {
    return values[periodNumber]
  } catch (ArrayIndexOutOfBoundsException e) {
    return 0
  }
}

after

double getValueForPeriod(int periodNumber) {
  return periodNumber >= values.length ? 0 : values[periodNumber]
}