[리팩터링] 7. API 리팩터링

안광의·2022년 10월 19일
1
post-thumbnail

API 리팩터링

질의 함수와 변경 함수 분리하기

//리팩토링 전
function totalOutstandingAndSendBill() {
  const result = customer.invoices.reduce(
    (total, each) => each.amount + total,
    0
  );
  sendBill();
  return result;
}
//리팩토링 후
function getTotalOutstanding() {
  return customer.invoices.reduce(
    (total, each) => each.amount + total,
    0
  );
}

function sendBill() {
  ...
}

외부에서 관찰할 수 있는 겉보기 부수효과가 전혀 없이 값을 반환해주는 함수를 추구해야 하기 때문에 겉보기 부수효과가 있는 함수와 없는 함수는 명확히 구분하는 것이 좋다. 값을 반환하면 서 부수효과도 있는 함수를 발견하면 상태를 변경하는 부분과 질의하는 부분을 분리해야 한다.



함수 매개변수화하기

//리팩토링 전
function tenPercentRaise(person) {
  person.salary = person.salary.multiply(1.1);
}

function fivePercentRaise(person) {
  person.salary = person.salary.multiply(1.05);
}
//리팩토링 후
function raise(person, factor) {
  person.salary = person.salary.multiply(1 + factor);
}

두 함수의 로직이 아주 비슷하고 단지 리터럴 값만 다르다면, 그 다른 값만 매개변수로 받아 처리하는 함수 하나로 합쳐서 중복을 없앨 수 있다.이렇게 하면 매개변수 값만 바꿔서 여러 곳에서 쓸 수 있의니 함수의 유용성이 커진다.



플래그 인수 제거하기

//리팩토링 전
function setDimension(name, value) {
  if (name === 'height') {
    this._height = value;
    return;
  }
  if (name === 'width') {
    this._width = value;
    return;
  }
}
//리팩토링 후
function setWidth(value) {
  this._width = value;
}

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

플래그 인수란 호출되는 함수가 실행할 로직을 호출하는 쪽에서 선택하기위해 전달하는 인수를 말하는데, 호출할 수 있는 함수들이 무엇이고 어떻게 호출해야 하는지르 이해하기가 어려워지기 때문에 사용하지 않는 것이 좋다. 플래그 인수를 제거하면 코드가 깔끔해짐은 뭄ㄹ론 프로그래밍 도구에도 도움을 준다.

플래그 인수가 둘 이상이면 함수 하나가 너무 많은 일을 처리하고 있다는 신호이므로 같은 로직을 조합해내는 더 간단한 함수를 만들 방법을 고민해봐야 한다.



객체 통째로 넘기기

//리팩토링 전
export function temperatureAlerts(room, plan) {
  const alerts = [];
  const low = room.daysTempRange.low;
  const high = room.daysTempRange.high;
  if (!plan.withinRange(low, high)) {
    alerts.push('room temperature went outside range');
  }

  return alerts;
}

export class HeatingPlan {
  constructor(temperatureRange) {
    this._temperatureRange = temperatureRange;
  }

  withinRange(bottom, top) {
    return (
      bottom >= this._temperatureRange.low && top <= this._temperatureRange.high
    );
  }
}
//리팩토링 후
export function temperatureAlerts(room, plan) {
  const alerts = [];
  if (!plan.withinRange(room.daysTempRange)) {
    alerts.push('room temperature went outside range');
  }

  return alerts;
}

export class HeatingPlan {
  constructor(temperatureRange) {
    this._temperatureRange = temperatureRange;
  }

  withinRange(range) {
    return (
      range.bottom >= this._temperatureRange.low && range.top <= this._temperatureRange.high
    );
  }
}

하나의 레코드에서 값 두어개를 가져와 인수로 넘기는 대신, 레코드를 통째로 넘기고 함수 본문에서 필요한 값들을 꺼내 쓰도록 수정하면 변화에 대응하기 쉬워진다. 하지만 함수가 레코드 자체에 의존하기를 원치 않을 때, 특히 레코드와 함수가 서로 다른 모듈에 속한 상황이라면 이 리팩터링을 적용하지 말아야 한다.



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

//리팩토링 전
export 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;
    }
  }
}
//리팩토링 후
export class Order {
  constructor(quantity, itemPrice) {
    this.quantity = quantity;
    this.itemPrice = itemPrice;
  }

  get basePrice() {
    return this.quantity * this.itemPrice;
  }

  get finalPrice() {
    return this.discountedPrice();
  }

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

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

매개변수 목록은 함수의 변동 요인을 모아놓은 곳으로 중복은 피하는 게 좋으며 짧을 수록 이해하기 쉽다. 피호출 함수가 스스로 쉽게 결정할 수 있는 값을 매개변수로 건네는 것도 일종의 중복이므로 제거해주는 것이 좋다. 단, 매개변수를 제거했을때 피호출 함수에 원치 않는 의존성이 생긴다면 매개변수를 질의함수로 바꾸지 말아야 한다.



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

//리팩토링 전
targetTemperature(aPlan);

function targetTemperature(aPlan) {
  currentTemperature = thermostat.currentTemperature;
  ...
}
//리팩토링 후
targetTemperature(aPlan, thermostat.currentTemperature);

function targetTemperature(aPlan, currentTemperature) {
  ...
}

전역 변수를 참조한다거나 제거하길 원하는 원소를 참조하는 경우에는 매개변수로 바꿔서 참조를 풀어내는 책임을 호출자로 옮기는 것이 좋다.



세터 제거하기

//리팩토링 전
class Person {
  get name() {}
  set name(value) {}
}
//리팩토링 후
class Person {
  #name;
  constructor(name) {
    this.#name = name;
  }
  get name() {
    return this.#name;
  }
}

세터 메서드가 있다고 함은 필드가 수정될 수 있다는 뜻이다. 객체 생성 후에는 수정되지 않길 원하는 필드라면 세터를 제공하지 말아야 한다. 사람들이 무조건 접근자 메서드를 통해서만 필드를 다루려 할때나 클라이언트에서 생성 스크립트를 사용해 객체를 생성할 때 등의 경우에도 세터들을 제거하여 의도를 더 정확하게 전달하는 게 좋다.



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

//리팩토링 전
export 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: 'Salesman' };
  }
}
//리팩토링 후
export 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: 'Salesman' };
  }

  static createEngineer (name) {
    return new Employee(name, 'E');
  }

  static createManager (name) {
    return new Employee(name, 'M');
  }

  static createSalesman (name) {
    return new Employee(name, 'S');
  }
}

많은 객체 지향 언어에서 제공하는 생성자는 객체를 초기화하는 특별한 용도의 함수이다. 생성자를 호출하려면 특별한 연산자(new)를 사용해야 해서 일반 함수가 오길 기대하는 자리에 쓰기 어려운데 팩터리 함수에는 이런 제약이 없다.



함수를 명령으로 바꾸기

//리팩토링 전
export function 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;
}

export class ScoringGuide {
  stateWithLowCertification(state) {
    return state < 5;
  }
}
//리팩토링 후
export class ScoringGuide {
  stateWithLowCertification(state) {
    return state < 5;
  }
}

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

  excute() {
    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;
    }
  }
}

함수를 그 함수만을 위한 객체 안으로 캡슐화하면 더 유용해지는 상황이 있는데 이런 객체를 가리켜 '명령 객체' 혹은 단순히 '명령'이라 한다. 명령 객체 대부분은 메서드 하나로 구성되며, 이 메서드를요청해 실행하는 것이 이 객체의 목적이다. 명령은 평범한 함수 메커니즘보다 훨씬 유연하게 함수를 제어하고 표현할 수 있다.



명령을 함수로 바꾸기

//리팩토링 전
export 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;
  }
}
//리팩토링 후
function charge(customer, usage, provider) {
  const baseCharge = customer.baseRate * usage;
  return baseCharge + provider.connectionCharge;
}

명령 객체는 복잡한 연산을 다룰 수 있는 강력한 메커니즘을 제공하지만, 로직이 크게 복잡하지 않다면 명령 객체는 장점보다 단점이 많으니 함수로 바꿔주는 것이 낫다.



수정된 값 반환하기

//리팩토링 전
export function ascentVelocity(points, totalMinutes) {
  function calculateAscent() {
    for (let i = 1; i < points.length; i++) {
      const verticalChange = points[i].elevation - points[i - 1].elevation;
      totalAscent += verticalChange > 0 ? verticalChange : 0;
    }
  }

  let totalAscent = 0;
  calculateAscent([{ elevation: 10 }, { elevation: 20 }]);

  return totalAscent / totalMinutes;
}
//리팩토링 후
export function ascentVelocity(points, totalMinutes) {
  function 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;
  }

  const totalAscent = calculateAscent(points);

  return totalAscent / totalMinutes;
}

데이터가 어떻게 수정되는지를 추적하는 일은 코드에서 이해하기 가장 어려운 부분 중 하나이다. 특히 같은 테이터 블록을 읽고 수정하는 코드가 여러곳이라면 데이터가 수정되는 흐름과 코드의 프름을 일치시키기 상당히 어렵기 때문에 데이터가 수정된다는 사실을 명확히 알려주어서 어느 함수가 무슨 일을 하는지 쉽게 알 수 있게 하는 일이 대단히 중요하다.



오류 코드를 예외로 바꾸기

//리팩토링 전
function localShippingRules(data) {
  if (data) return new ShippingRules(data);
  else return -23;
}
//리팩토링 후
function localShippingRules(data) {
  if (data) {
    return new ShippingRules(data);
  } else {
    throw new OrderProcessingError('No shipping rules found');
  }
}

class OrderProcessingError extends Error {
  constructor(errorCode) {
    super();
    this.errorCode = errorCode;
  }
}

try{
  const result = localShippingRules();
} catch (error) {
  if(error instanceof OrderProcessingError) {
    console.error(error);
  }
}

예외는 정교한 메커니즘이지만 대다수의 다른 정교한 메커니즘과 같이 정확하게 사용할 때만 최고의 효과를 낸다. 예외는 정확히 예상 밖의 동작일 때만 쓰여야 한다.



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

//리팩토링 전
const values = [];
function getValueForPeriod(periodNumber) {
  const value = values[periodNumber];
  if (!value) {
    throw new Error('value is undefined');
  }
  return value;
}

try {
  getValueForPeriod(-10);
} catch (error) {
  console.log('에러 발생!');
}
//리팩토링 후
const values = [];
function getValueForPeriod(periodNumber) {
  return values[periodNumber] ?? 0;
}

getValueForPeriod(-10);

예외는 뜻밖의 오류라는 말 그대로 예외적으로 동작할 때만 쓰여야 한다. 함수 수행 시 문제가 될 수 있는 조건을 함수 호출 전에 검사할 수 있다면, 예외를 던지는 대신 호출하는 곳에서 조건을 검사하도록 해야 한다.

profile
개발자로 성장하기

0개의 댓글