API 리팩터링

질의(읽기) 함수와 변경 함수 분리하기

데이터를 갱신하는 함수와 그저 조회만 하는 함수는 명확히 구분하도록 한다. 두 기능이 섞여 있다면 갈라놔야 한다.

📜 절차

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

다음은 이름 목록을 훑어 악당을 찾는 예제다. 악당을 찾으면 그 사람의 이름을 반환하고 경고를 울린다.

const setOffAlarms = () => { ... };

const alertForMiscreant = (people) => {
  for (const p of people) {
    if (p === "조커") {
      setOffAlarms();
      return "조커";
    }
    if (p === "사루만") {
      setOffAlarms();
      return "사루만";
    }
  }
  return "";
};

질의 함수와 변경 함수를 분리해보자. 우선 함수를 복제하고 질의 목적에 맞는 이름을 짓는다. 그리고 새 질의 함수에서 부수효과를 낳는 부분을 제거한다.

const setOffAlarms = () => { ... };
                            
// 새 질의 함수 생성
const findMiscreant = (people) => {
  for (const p of people) {
    if (p === "조커") {
      return "조커";
    }
    if (p === "사루만") {
      return "사루만";
    }
  }
  return "";
};

const alertForMiscreant = (people) => { ... }

이제 원래 함수를 호출하는 곳을 찾아서 새로운 질의 함수를 호출하도록 바꾸고, 원래의 변경 함수에서 질의 관련 코드를 없앤다.

const setOffAlarms = () => { ... };

const findMiscreant = (people) => { ... }
                         
// 질의 관련 코드 제거
const alertForMiscreant = (people) => {
  for (const p of people) {
    if (p === "조커") {
      setOffAlarms();
      return;
    }
    if (p === "사루만") {
      setOffAlarms();
      return;
    }
  }
  return;
};
                                   
const found = findMiscreant(people);
alertForMiscreant(people);

추가로, 변경 함수와 새 질의 함수에 중복된 코드들은 제거한다.

const setOffAlarms = () => { ... };

const findMiscreant = (people) => { ... }

const alertForMiscreant = (people) => {
  const miscreant = findMiscreant(people);
  if (miscreant !== "") {
    setOffAlarms();
  }
};

💻 질의 함수와 변경 함수 분리하기 코드


함수 매개변수화하기

두 함수의 로직이 아주 비슷하고 단지 리터럴 값만 다르다면, 그 다른 값만 매개변수로 받아 처리하는 함수로 합쳐서 중복을 없애도록 한다.

📜 절차

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

다음의 예제를 보자. 대역을 다루는 세 함수의 로직이 비슷하다. 매개변수화 함수로 통합해보자.

const usd = (aNumber) => { ... };

const bottomBand = (usage) => Math.min(usage, 100);

const middleBand = (usage) => (usage > 100 ? Math.min(usage, 200) - 100 : 0);

const topBand = (usage) => (usage > 200 ? usage - 200 : 0);

const baseCharge = (usage) => {
  if (usage < 0) {
    return usd(0);
  }
  const amount = bottomBand(usage) * 0.03 + middleBand(usage) * 0.05 + topBand(usage) * 0.07;
  return usd(amount);
};

middleBand에 매개변수를 추가하고 다른 호출들을 여기에 맞춰보자. 이 과정에서 함수 이름도 어울리게 수정한다.

// ... 

// bottom 관련 코드 추가
const withinBand = (usage, bottom, top) => (usage > bottom ? Math.min(usage, 200) - bottom : 0);

const baseCharge = (usage) => {
  if (usage < 0) {
    return usd(0);
  }
  // middleBand -> withinBand로 변경
  const amount = bottomBand(usage) * 0.03 + withinBand(usage, 100, 200) * 0.05 + topBand(usage) * 0.07;
  return usd(amount);
};

나머지 매개변수도 대체한다. 대역의 상한 호출을 대체할 때는 Infinity를 이용한다.

// ...

// top 관련 코드 추가
const withinBand = (usage, bottom, top) => (usage > bottom ? Math.min(usage, top) - bottom : 0);

const baseCharge = (usage) => {
  if (usage < 0) {
    return usd(0);
  }
  // withinBand로 변경
  const amount = withinBand(usage, 0, 100) * 0.03 + withinBand(usage, 100, 200) * 0.05 + withinBand(usage, 200, Infinity) * 0.07;
  return usd(amount);
};

💻 함수 매개변수화하기 코드


플래그 인수 제거하기

그저 함수의 동작 모드를 전환하는 용도로만 쓰이는 매개변수가 있다면, 각 리터럴 값에 대응되는 명시적 함수로 바꾸도록 한다.

플래그 인수란 호출되는 함수가 실행할 로직을 호출하는 쪽에서 선택하기 위해 전달하는 인수다.

function bookConcert(aCustomer, isPremium) {
  if(isPremium) { // 프리미엄 예약용 로직 }
  else { // 일반 예약용 로직 }
}
    
bookConcert(aCustomer, true);

isPremium 같은 boolean 플래그는 코드를 읽는 이에게 뜻을 온전히 전달하지 못한다. 호출할 수 있는 함수들이 무엇이고 어떻게 호출해야 하는지를 이해하기가 어려워지기 때문이다. 또한 사용할 함수를 선택한 후에도 플래그 인수로 어떤 값을 넘겨야 하는지를 또 알아내야 한다.
이보다는 특정한 기능 하나만 수행하는 명시적인 함수를 제공하는 것이 좋다.

📜 절차

  1. 매개변수로 주어질 수 있는 값 각각에 대응하는 명시적 함수들을 생성한다. (조건문 분해하기, 래핑함수 형태로 만들기)
  2. 원래 함수를 호출하는 코드들을 찾아서 각 리터럴 값에 대응되는 명시적 함수를 호출하도록 수정한다.

다음은 배송일자를 계산하는 예제다.

const deliveryDate = (anOrder, isRush) => {
  if (isRush) {
    let deliveryTime;
    if (["MA", "CT"].includes(anOrder.deliveryState)) {
      deliveryTime = 1;
    } else if (["NY", "NH"].includes(anOrder.deliveryState)) {
      deliveryTime = 2;
    } else {
      deliveryTime = 3;
    }
    return anOrder.placedOn.plusDays(1 + deliveryTime);
  } else {
    let deliveryTime;
    if (["MA", "CT", "NY"].includes(anOrder.deliveryState)) {
      deliveryTime = 2;
    } else if (["ME", "NH"].includes(anOrder.deliveryState)) {
      deliveryTime = 3;
    } else {
      deliveryTime = 4;
    }
    return anOrder.placedOn.plusDays(2 + deliveryTime);
  }
};

aShipment.deliveryDate = deliveryDate(anOrder, true); // 호출

플래그 인수 isRush를 제거하고 조건문 분해하기를 통해 각 조건문에 대응하는 명시적인 함수로 바꾼다.

const rushDeliveryDate = (anOrder) => {
  let deliveryTime;
  if (["MA", "CT"].includes(anOrder.deliveryState)) {
    deliveryTime = 1;
  } else if (["NY", "NH"].includes(anOrder.deliveryState)) {
    deliveryTime = 2;
  } else {
    deliveryTime = 3;
  }
  return anOrder.placedOn.plusDays(1 + deliveryTime);
};

const regularDeliveryDate = (anOrder) => {
  let deliveryTime;
  if (["MA", "CT", "NY"].includes(anOrder.deliveryState)) {
    deliveryTime = 2;
  } else if (["ME", "NH"].includes(anOrder.deliveryState)) {
    deliveryTime = 3;
  } else {
    deliveryTime = 4;
  }
  return anOrder.placedOn.plusDays(2 + deliveryTime);
};

aShipment.deliveryDate = rushDeliveryDate(anOrder); // 호출

💻 플래그 인수 제거하기 코드


객체 통째로 넘기기

하나의 레코드에서 값 두어 개를 가져와 인수로 넘기는 코드를 보면 그 값들 대신 레코드를 통째로 넘기도록 한다.

레코드에 담긴 데이터 중 일부를 받는 함수가 여러 개라면 그 함수들끼리는 같은 데이터를 사용하는 부분이 있을 것이고, 그 부분의 로직이 중복될 가능성이 커진다. 레코드를 통째로 넘긴다면 이런 로직 중복도 없앨 수 있을 뿐만 아니라, 매개변수 목록이 짧아져서 일반적으로는 함수 사용법을 이해하기 쉬워진다.

📜 절차

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

다음은 실내 온도 모니터링 시스템에 대한 예제다. 일일 최저/최고 기온이 난방계획에서 정한 범위를 벗어나는지 확인한다.

class HeatingPlan {
  constructor(temperatureRange) {
    this._temperatureRange = temperatureRange;
  }
  withinRange(bottom, top) {
    return (bottom >= this._temperatureRange.low && top <= this._temperatureRange.high);
  }
}

const temperatureAlerts = (aRoom, aPlan) => {
  const alerts = [];
  const low = aRoom.daysTempRange.low;
  const high = aRoom.daysTempRange.high;
  if (!aPlan.withinRange(low, high)) {
    alerts.push("방 온도가 지정 범위를 벗어났습니다.");
  }
  return alerts;
};

최저/최고 기온을 뽑아내 인수로 건네는 대신, 범위 객체를 통째로 건네는 것으로 수정한다.

class HeatingPlan {
  // ...
  withinRange(aNumberRange) {
    return (aNumberRange.low >= this._temperatureRange.low && aNumberRange.high <= this._temperatureRange.high);
  }
}

const temperatureAlerts = (aRoom, aPlan) => {
  const alerts = [];
  if (!aPlan.withinRange(aRoom.daysTempRange)) {
    alerts.push("방 온도가 지정 범위를 벗어났습니다.");
  }
  return alerts;
};

💻 객체 통째로 넘기기 코드


매개변수를 질의 함수로 바꾸기

매개변수 목록에서도 중복은 피하는 게 좋고, 매개변수 목록은 짧을수록 이해하기가 더 쉽다.

피호출 함수가 스스로 쉽게 결정할 수 있는 값을 매개변수로 건네는 것도 일종의 중복이다. 다른 매개변수에서 얻을 수 있는 값을 별도 매개변수로 전달하는 것은 아무 의미가 없다.

📜 절차

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

다음의 예제를 보자.

class Order {
  constructor(quantity, itemPrice) {
    this.quantity = quantity;
    this.itemPrice = itemPrice;
  }

  get finalPrice() {
    const basePrice = this.quantity * this.itemPrice;
    let discountLevel;
    if (this.quantity > 100) {
      discountLevel = 2;
    } else {
      discountLevel = 1;
    }
    return this.discountedPrice(basePrice, discountLevel);
  }

  discountedPrice(basePrice, discountLevel) {
    switch (discountLevel) {
      case 1:
        return basePrice * 0.95;
      case 2:
        return basePrice * 0.9;
      default:
        return basePrice;
    }
  }
}

finalPrice 함수 내의 discountLevel의 값을 계산하는 코드를 별도의 함수로 추출한다.

class Order {
  // ...
  get finalPrice() {
    const basePrice = this.quantity * this.itemPrice;
     return this.discountedPrice(basePrice, this.discountLevel); // 함수 호출로 변경
  }

  discountLevel() { // 함수로 추출
    return this.quantity > 100 ? 2 : 1;
  }

  discountedPrice(basePrice, discountLevel) {
    switch (this.discountLevel) { // 함수 호출로 변경
      case 1:
        return basePrice * 0.95;
      case 2:
        return basePrice * 0.9;
      default:
        return basePrice;
    }
  }
}

discountedPrice 함수에 discountLevel의 반환 값을 건넬 필요가 사라졌으므로, discountedPrice의 매개변수를 제거한다.

class Order {
  // ...
  get finalPrice() {
    const basePrice = this.quantity * this.itemPrice;
    return this.discountedPrice(basePrice); // 매개변수 제거
  }

  discountLevel() {
    return this.quantity > 100 ? 2 : 1;
  }

  discountedPrice(basePrice) { // 매개변수 제거
    switch (this.discountLevel) { // 함수 호출로 변경
      case 1:
        return basePrice * 0.95;
      case 2:
        return basePrice * 0.9;
      default:
        return basePrice;
    }
  }
}

💻 매개변수를 질의 함수로 바꾸기 코드


질의 함수를 매개변수로 바꾸기

* 반대 리펙터링: 매개변수를 질의 함수로 바꾸기

대상 함수가 더 이상 특정 원소에 의존하길 원치 않는다면, 해당 참조를 매개변수로 바꾸도록 한다.

📜 절차

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

다음은 실내 온도 제어 시스템에 대한 예제다. 사용자는 온도 조절기로 온도를 설정할 수 있지만, 목표 온도는 난방 계획에서 정한 범위에서만 선택할 수 있다.

const thermostat = { selectedTemperature: 25, currentTemperature: 27 };

class HeatingPlan {
  constructor(min, max) {
    this._min = min;
    this._max = max;
  }
  get targetTemperature() {
    if (thermostat.selectedTemperature > this._max) {
      return this._max;
    } else if (thermostat.selectedTemperature < this._min) {
      return this._min;
    } else {
      return thermostat.selectedTemperature;
    }
  }
}

const temperatureController = (thePlan) => {
  const setToHeat = () => {};
  const setToCool = () => {};
  const setOff = () => {};

  if (thePlan.targetTemperature > thermostat.currentTemperature) {
    setToHeat();
  } else if (thePlan.targetTemperature < thermostat.currentTemperature) {
    setToCool();
  } else {
    setOff();
  }
};

targetTemperature 함수가 전역 객체인 thermostat에 의존하고 있다. 이 전역 객체에 건내는 질의 함수를 매개변수로 옮겨서 의존성을 없앤다.

// ...

class HeatingPlan {
  // ...
  targetTemperature(selectedTemperature) { // 매개변수로 바꾸기
    if (selectedTemperature > this._max) {
      return this._max;
    } else if (selectedTemperature < this._min) {
      return this._min;
    } else {
      return selectedTemperature;
    }
  }
}

const temperatureController = (thePlan) => {
  // ...
  if (thePlan.targetTemperature(thermostat.selectedTemperature) > thermostat.currentTemperature) { // 매개변수로 바꾸기
    setToHeat();
  } else if (thePlan.targetTemperature(thermostat.selectedTemperature) < thermostat.currentTemperature) { // 매개변수로 바꾸기
    setToCool();
  } else {
    setOff();
  }
};

이로써 전역 객체와의 의존성을 제거했을 뿐만 아니라, HeatingPlan 클래스는 불변이 되었다.


💻 질의 함수를 매개변수로 바꾸기 코드


세터 제거하기

객체 생성 후에는 수정되지 않길 원하는 필드라면, 세터를 제공하지 않도록 하여 불변성을 유지하도록 한다.

세터 메서드가 있다고 하는 것은 필드가 수정될 수 있다는 뜻이다. 세터가 없는 필드라면 해당 필드는 오직 생성자에서만 설정되며, 수정하지 않겠다는 의도가 분명해지고 변경될 가능성은 봉쇄된다.

📜 절차

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

다음의 예제를 보자. Person 클래스를 작성하고 Person 객체를 하나 생성한다.

export class Person {
  get name() {
    return this._name;
  }
  set name(arg) {
    this._name = arg;
  }
  get id() {
    return this._id;
  }
  set id(arg) {
    this._id = arg;
  }
}

const martin = new Person();
martin.name = "마틴";
martin.id = "1234";

name은 변경될 수 있겠지만, id는 변경되면 안된다. 이 의도를 명확히 하기 위해, 생성자에서 id를 받도록 하고 id 세터는 제거한다.

export class Person {
  constructor(id) { // id 추가
    this._id = id;
  }
  get name() {
    return this._name;
  }
  set name(arg) {
    this._name = arg;
  }
  get id() {
    return this._id;
  }
  // 세터 제거
}

const martin = new Person("1234"); // id 설정
martin.name = "마틴";

💻 세터 제거하기 코드


생성자를 팩터리 함수로 바꾸기

호출자에 새로운 객체를 만들어 반환하려 할 때 일반적인 생성자의 능력만으로는 부족한 경우, 팩터리 함수로 바꾸도록 한다.

📜 절차

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

다음의 예제를 보자.

class Employee {
  constructor(name, typeCode) {
    this._name = name;
    this._typeCode = typeCode;
  }

  get name() {
    return this._name;
  }

  get type() {
    return Employee.legalTypeCodes[this._typeCode];
  }
  
  static get legalTypeCodes() {
    return { E: "Engineer", M: "Manager", S: "Salesperson" };
  }
}

const client1 = (document) => {
  const candidate = new Employee(document.name, document.empType);
};

const client2 = (document) => {
  const leadEngineer = new Employee(document.leadEngineer, "E");
};

팩터리 함수를 만들고, 팩터리 본문은 단순히 생성자에 위임하도록 한다.

// ... 

const createEmployee = (name, typeCode) => {
  return new Employee(name, typeCode);
};

const client1 = (document) => {
  const candidate = createEmployee(document.name, document.empType); // 새로 생성한 함수 호출로 변경
};

const client2 = (document) => {
  const leadEngineer = createEmployee(document.leadEngineer, "E"); // 새로 생성한 함수 호출로 변경
};

함수에 문자열 리터럴을 건네는 코드에 대해 새 팩터리 함수를 만들어 typeCode을 팩터리 함수의 이름에 녹인다.

// ...

const createEngineer = (name) => {
  return new Employee(name, "E");
};

// ...

const client2 = (document) => {
  const leadEngineer = createEngineer(document.leadEngineer);
};

💻 생성자를 팩터리 함수로 바꾸기 코드


함수를 명령으로 바꾸기

함수를 그 함수만을 위한 객체 안으로 캡슐화하는 것이 더 유용하다면, 함수를 명령으로 바꾸도록 한다.

함수만을 위한 객체로 캡슐화하는 객체를 가리켜 명령 객체 혹은 단순히 명령(command)이라 한다. 명령 객체 대부분은 메서드 하나로 구성되고 이 메서드를 요청해 실행하는 것이 이 객체의 목적이다.

일급 함수와 명령 중 선택해야 한다면, 대부분의 경우에는 일급 함수를 선택한다. 명령을 선택하는 경우는 명령보다 더 간단한 방식으로는 얻을 수 없는 기능이 필요할 때 뿐이다.

📜 절차

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

다음은 건강보험 어플리케이션에 대한 점수 계산 예제다.

const score = (candidate, medicalExam, scoringGuide) => {
  let result = 0;
  let healthLevel = 0;
  let highMedicalRiskFlag = false;

  if (medicalExam.isSmoker) {
    healthLevel += 10;
    highMedicalRiskFlag = true;
  }
  let certificationGrade = "regular";
  if (scoringGuide.stateWithLowCertification(candidate.originState)) {
    certificationGrade = "low";
    result -= 5;
  }
  // 비슷한 코드가 한참 이어짐
  result -= Math.max(healthLevel - 5, 0);
  return result;
};

복잡한 함수를 잘개 쪼개서 수정하기 쉽게 만들고자 할때도 명령을 사용하는 편이 낫다.

빈 클래스를 만들고 이 함수를 그 클래스로 옮긴다.

const score = (candidate, medicalExam, scoringGuide) => {
  return new Scorer().execute(candidate, medicalExam, scoringGuide); // 함수 호출
};

class Scorer { // 클래스 생성
  execute(candidate, medicalExam, scoringGuide) {
    let result = 0;
    let healthLevel = 0;
    let highMedicalRiskFlag = false;

    if (medicalExam.isSmoker) {
      healthLevel += 10;
      highMedicalRiskFlag = true;
    }
    let certificationGrade = "regular";
    if (scoringGuide.stateWithLowCertification(candidate.originState)) {
      certificationGrade = "low";
      result -= 5;
    }
    result -= Math.max(healthLevel - 5, 0);
    return result;
}

명령이 받는 인수들을 생성자로 옮겨서 execute 함수는 매개변수를 받지 않도록 한다.

const score = (candidate, medicalExam, scoringGuide) => {
  return new Scorer(candidate, medicalExam, scoringGuide).execute(); // 매개변수 이동
};

class Scorer {
  constructor(candidate, medicalExam, scoringGuide) { // 생성자로 매개변수 이동
    this._candidate = candidate;
    this._medicalExam = medicalExam;
    this._scoringGuide = scoringGuide;
  }

  execute() { // this 참조
    let result = 0;
    let healthLevel = 0;
    let highMedicalRiskFlag = false;

    if (this._medicalExam.isSmoker) {
      healthLevel += 10;
      highMedicalRiskFlag = true;
    }
    let certificationGrade = "regular";
    if (this._scoringGuide.stateWithLowCertification(this._candidate.originState)) {
      certificationGrade = "low";
      result -= 5;
    }
    result -= Math.max(healthLevel - 5, 0);
    return result;
  }
}

더 나아가서 복잡한 함수를 잘게 나눈다. 모든 지역 변수를 필드로 바꾸고, 로직을 분리할 수 있는 부분을 찾아 함수로 추출한다.

// ...

class Scorer {
  // ...
  execute() {
    this._result = 0;
    this._healthLevel = 0;
    this._highMedicalRiskFlag = false;

    this.scoreSmoking();
    this._certificationGrade = "regular";
    if (this._scoringGuide.stateWithLowCertification(this._candidate.originState)) {
      this._certificationGrade = "low";
      this._result -= 5;
    }
    this._result -= Math.max(this._healthLevel - 5, 0);
    return this._result;
  }

  scoreSmoking() {
    if (this._medicalExam.isSmoker) {
      this._healthLevel += 10;
      this._highMedicalRiskFlag = true;
    }
  }
}

💻 함수를 명령으로 바꾸기 코드


명령을 함수로 바꾸기

* 반대 리펙터링: 함수를 명령으로 바꾸기

로직이 크게 복잡하지 않다면 평범한 함수로 바꿔주도록 한다.

📜 절차

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

다음의 예제를 보자.

class ChargeCalculator {
  constructor(customer, usage, provider) {
    this._customer = customer;
    this._usage = usage;
    this._provider = provider;
  }
  get baseCharge() {
    return this._customer.baseRate * this._usage;
  }
  get charge() {
    return this.baseCharge + this._provider.connectionCharge;
  }
}

const monthCharge = new ChargeCalculator(customer, usage, provider).charge;

이 명령 클래스는 간단하기에 함수로 대체하도록 하자. 이 클래스를 생성하고 호출하는 코드를 함수로 추출한다.

// ...

const charge = (customer, usage, provider) => {
  return new ChargeCalculator(customer, usage, provider).charge;
};

const monthCharge = charge(customer, usage, provider);

ChargeCalculator 클래스의 baseCharge 함수를 인라인한다.

class ChargeCalculator {
  // ...
  
  // baseCharge 함수 제거
  get charge() {
    const baseCharge = this._customer.baseRate * this_usage; // 인라인
    return baseCharge + this._provider.connectionCharge;
  }
}

// ...

생성자에 전달되는 모든 데이터를 charge 함수로 옮긴다.

class ChargeCalculator {
  get charge(customer, usage, provider) { // 매개변수 이동
    const baseCharge = customer.baseRate * usage;
    return baseCharge + provider.connectionCharge;
  }
}

const monthCharge = new ChargeCalculator().charge(customer, usage, provider); // 매개변수 이동

명령 클래스를 제거한다.

const charge = (customer, usage, provider) => {
  const baseCharge = customer.baseRate * usage;
  return baseCharge + provider.connectionCharge;
};

const monthCharge = charge(customer, usage, provider);

💻 명령을 함수로 바꾸기 코드


수정된 값 반환하기

함수 안에서 데이터가 수정됐음을 확실히 알리려면, 수정된 값을 변수에 담아 반환하도록 한다.

📜 절차

  1. 함수가 수정된 값을 반환하게 하여 호출자가 그 값을 자신의 변수에 저장하게 한다.
  2. 피호출 함수 안에 반환할 값을 가리키는 새로운 변수를 선언한다.
  3. 선언 시점에 계산 로직을 바로 실행해 대입한다.
  4. 피호출 함수의 변수 이름을 새 역할에 어울리도록 바꿔준다.

다음은 GPS 위치 목록으로 고도 상승분 계산을 수행하는 예제다.

let totalAscent = 0;

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

calculateAscent();

calculateAscent 함수에서 totalAscent가 갱신되는 사실이 잘 드러나지 않는다.

calculateAscent 함수 안에 반환할 값을 담을 변수를 선언하고, totalAscent를 변수 선언과 동시에 수행되도록 하여, 갱신 사실이 명확히 드러나도록 수정한다.

const totalAscent = calculateAscent();

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

💻 수정된 값 반환하기 코드


오류 코드를 예외로 바꾸기

예외는 정확히 예상 밖의 동작일 때만 쓰여야 한다.

📜 절차

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

다음은 전역 테이블에서 배송지의 배송 규칙을 알아내는 예제다. 오류가 발생할 경우, localShippingRulescalculateShippingCostsexecute 함수 순으로 전파된다.

const localShippingRules = (countryData, country) => {
  const data = countryData.shippingRules[country];
  if (data) {
    return new ShippingRules(data);
  } else {
    return -23;
  }
};

const calculateShippingCosts = (order) => {
  // 관련 없는 코드
  const shippingRules = localShippingRules(order.country);
  if (shippingRules < 0) {
    return shippingRules; // 오류 전파
  }
  // 더 관련 없는 코드
};

const execute = (orderData, errorList) => {
  const status = calculateShippingCosts(orderData);
  if (status < 0) {
    errorList.push({ order: orderData, errorCode: status });
  }
};

오류 코드를 예외로 바꿔보자. execute 함수 안에 try/catch 블록을 추가한다.

// ...

const execute = (orderData, errorList) => {
 let status;
  try {
    status = calculateShippingCosts(orderData);
  } catch (e) {
    throw e;
  }
  if (status < 0) {
    errorList.push({ orderData, errorCode: status });
  }
};

예외 처리를 위해 예외 클래스를 추가하고, 예외 클래스를 처리하는 로직을 추가한다. 그 이후에 예외 전파 코드는 제거한다.

// ...

class OrderProcessingError extends Error {
  constructor(errorCode) {
    super(`주문 처리 오류: ${errorCode}`);
    this.code = errorCode;
  }
  get name() {
    return "OrderProcessingError";
  }
}

// ... 

const calculateShippingCosts = (order) => {
  const shippingRules = localShippingRules(order.country);
  // 오류 전파 코드 제거
  // ...
};

const execute = (orderData, errorList) => {
  try {
    calculateShippingCosts(orderData);
  } catch (e) {
    if (e instanceof OrderProcessingError) {
      errorList.push({ order: orderData, errorCode: e.code });
    } else {
      throw e;
    }
  }
};

💻 오류 코드를 예외로 바꾸기 코드


예외를 사전확인으로 바꾸기

문제가 되는 조건을 함수 호출 전에 검사해, 예외 남용을 줄이도록 한다.

📜 절차

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

데이터베이스 연결 같은 자원들을 관리하는 자원 풀 클래스가 있다. 자원이 필요한 코드는 풀에서 하나씩 꺼내 사용한다. 풀은 어떤 자원이 할당되었고 가용한 지를 추적하고 자원이 바닥나면 새로 생성한다.

class ResourcePool {
  get(resource) {
    let result;
    try {
      result = this.available.pop();
      this.allocated.add(result);
    } catch (e) {
      result = resource.create();
      this.allocated.add(result);
    }
    return result;
  }
}

풀에서 자원이 고갈되는 건 예상치 못한 조건이 아니기 때문에, 예외 처리로 대응하는 것은 바람직하지 않다. 예외를 제거하고 그 대신에 조건절로 대체한다.

class ResourcePool {
  get(resource) {
    let result;
    // 조건절로 대체
    if (this.available.isEmpty()) { 
      result = resource.create();
      this.allocated.add(result);
    } else {
      result = this.available.pop();
      this.allocated.add(result);
    }
    return result;
  }
}

더 나아가서, 중복되는 코드는 제거한다.

class ResourcePool {
  get(resource) {
    const result = this.available.isEmpty() ? resource.create() : this.available.pop();
    this.allocated.add(result);
    return result;
  }
}

💻 예외를 사전확인으로 바꾸기 코드

좋은 웹페이지 즐겨찾기