[리팩터링 2판] - API 리팩터링

Lee Jeong Min·2022년 9월 19일
1

리팩터링 2판

목록 보기
11/12
post-thumbnail

리팩터링 2판의 Chatper 11를 보고 정리한 글입니다.

모듈과 함수는 소프트웨어를 구성하는 빌딩 블록이며, API는 이 블록들을 끼워 맞추는 연결부이다. 따라서 API를 이해하기 쉽고 사용하기 쉽게 만드는 일은 중요한 일이다. 이러한 API관련 리팩터링 기법들을 알아보자.

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

배경

값을 반환하면서도 부수효과가 있는 함수를 발견하면 상태를 변경하는 부분과 질의하는 부분을 분리해보자.

절차

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

예시

리팩터링 전

이름 목록을 훑어 악당을 찾아 악당을 찾으면 그 사람의 이름을 반환하고 경고를 울리는 함수

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

// 호출하는 쪽 코드
const found = alertForMiscreant(people);

리팩터링 후

함수를 복제하고 질의 목적에 맞는 이름을 지은후, 부수효과를 낳는 부분을 제거한다. 원래 함수를 호출하는 곳을 모두 찾아서 새로운 질의 함수로 바꾸고 변경 함수 호출 코드를 바로 아래에 삽입하자.

function findMiscreant(people) {
  for (const p of people) {
    if (p === '조커') {
      return '조커';
    }
    if (p === '사루만') {
      return '사루만';
    }
  }
  return '';
}

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

// 호출하는 쪽 코드
const found = findMiscreant(people);
alertForMiscreant(people);


// 여기서 alertForMiscreant 함수를 더 가다듬으면 다음과 같이 만들 수 있다.
function alertForMiscreant(people) {
  if (findMiscreant(people) !== '') setOffAlarams();
}

함수 매개변수화하기

배경

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

절차

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

예시

직관적인 경우

리팩터링 전

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

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

리팩터링 후

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

덜 직관적인 경우

리팩터링 전

function 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);
}

function bottomBand(usage) {
  return Math.min(usage, 100);
}

function middleBand(usage) {
  return usage > 100 ? Math.min(usage, 200) - 100 : 0;
}

function topBand(usage) {
  return usage > 200 ? usage - 200 : 0;
}

리팩터링 후

// withinBand라는 3가지의 매개변수를 받는 함수를 만들어 사용!
function withinBand(usage, bottom, top) {
  return usage > bottom ? Math.min(usage, top) - bottom : 0;
}

// 하나의 함수로 리팩터링 완료
function baseCharge(usage) {
    if (usage < 0) return usd(0);
    const amount = withinBand(usage, 0, 100) * 0.03
        + withinBand(usage, 100, 200) * 0.05
        + withinBand(usage, 200, Infinity) * 0.07;
    return usd(amount);
}

플래그 인수 제거하기

배경

플래그 인수란 호출되는 함수가 실행한 로직을 호출하는 쪽에서 선택하기 위해 전달하는 인수다. 그러나 이러한 인수가 있으면 함수들의 기능 차이가 함수 목록에서 드러나지 않는다.

특정한 기능 하나만 수행하는 명시적인 함수를 제공하는 편이 훨씬 깔끔하다.

절차

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

예시

리팩터링 전

배송일자를 계산하는 코드

// 호출하는 쪽
// boolean 값이 뭘 의미하는지라는 의문이 떠오름
aShipment.deliveryDate = deliveryDate(anOrder, true);
aShipment.deliveryDate = deliveryDate(anOrder, false);



function 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);
  }
  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.placeOn.plusDays(2 + deliveryTime);
}

전형적인 플래그 인수를 사용하고 있는 코드

리팩터링 후

// 명시적인 함수가 호출자의 의도를 더 잘 드러낸다.
aShipment.deliveryDate = rushDeliveryDate(anOrder);
aShipment.deliveryDate = regularDeliveryDate(anOrder);

function 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);
}

function 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.placeOn.plusDays(2 + deliveryTime);
}

앞 예시와 달리 매개변수가 훨씬 까다로운 방식(if문안의 중첩 조건으로 사용)일 땐, 그 함수를 래핑하는 식으로 하여 앞에서 호출하는 코드들을 작성하고 뒤에 이어서 매개변수를 사용하는 부분만 함수를 만들어 리팩터링을 진행할 수 있다.

객체 통째로 넘기기

배경

하나의 레코드에서 값 두어개를 인수로 넘기는 경우, 그 값들 대신 레코드를 통째로 넘기고 함수 본문에서 필요한 값들을 꺼내 쓰도록 수정할 수 있다.

절차

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

예시

실내온도 모니터링 시스템(일일 최저,최고 기온이 난방 계획에서 정한 범위를 벗어나는지 확인)

리팩터링 전

const { low } = aRoom.daysTempRange;
const { high } = aRoom.daysTempRange;
if (!aPlan.withinRange(low, high)) alerts.push('room temperature went outside range');

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

리팩터링 후

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

if (!aPlan.withinRange(aRoom.daysTempRange)) alerts.push('room temperature went outside range');

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

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

배경

매개변수 목록은 함수의 변동 요인을 모아놓은 곳이기 때문에 중복은 피하는 것이 좋으며 짧을수록 이해하기 쉽다. 따라서 매개변수를 줄이고, 책임 소재를 피호출 함수로 옮겨 호출하는 쪽에서의 책임 주체를 간소하게 만들자.

→ 새로운 의존성이 생기거나 제거하고 싶은 기존 의존성을 강화하는 경우 매개변수를 질의 함수로 바꾸지 말아야 함.

절차

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

예시

리팩터링 전

class Order {
  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;
    }
  }
}

리팩터링 후

class Order {
  // 기존 로직 변경
  get finalPrice() {
    const basePrice = this.quantity * this.itemPrice;
    return this.discountedPrice(basePrice);
  }

  // 기존 매개변수 삭제
  discountedPrice(basePrice) {
    switch (this.discountLevel) {
      case 1:
        return basePrice * 0.95;
      case 2:
        return basePrice * 0.9;
    }
  }

  // 질의 함수 생성
  get discountLevel() {
    return this.quantity > 100 ? 2 : 1;
  }
}

필요할 때 직접 호출함으로써, 매개변수를 줄일 수 있다.

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

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

배경

코드를 보면 전역 변수를 참조하거나 제거하길 원하는 원소를 참조하는 경우와 같이 함수 안에 두기엔 거북한 참조를 발견할 때가 있다. 이 경우 매개변수로 바꾸어 참조를 풀어내는 책임을 호출자로 옮겨보자.

→ 질의 함수를 매개변수로 바꾸면 어떤 값을 제공할지를 호출자가 알아내야하므로 호출자가 복잡해지는데 결국 이 문제는 책임 소재를 프로그램의 어디에 배정하느냐의 문제로 귀결된다.(답이 없는 문제)

절차

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

예시

실내 온도 제어시스템으로 사용자는 온도조절기(thermostat)로 온도를 설정하지만 목표 온도는 난방 계획에서 정한 범위에서만 선택!

리팩터링 전

class HeatingPlan {
  get targetTemperature() {
    if (thermostat.selectedTemperature > this._max) return this._max;
    if (thermostat.selectedTemperature < this._min) return this._min;
    return thermostat.selectedTemperature;
  }
}

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

현재 thermostat이라는 전역 객체에 의존하고 있음. 이를 리팩터링 해보자.

리팩터링 후

class HeatingPlan {
  targetTemperature(selectedTemperature) {
    if (selectedTemperature > this._max) return this._max;
    if (selectedTemperature < this._min) return this._min;
    return selectedTemperature;
  }
}

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

매개 변수를 받아 다음과 같이 리팩터링을 할 수 있게 되었다.
또한 클래스는 불변이 되어 모든 필드가 생성자에서 설정되며 필드를 변경할 수 있는 메서드는 존재하지 않게되었다.
→ 테스트하고 다루기 쉬워진다는 장점을 가짐

세터 제거하기

배경

세터 제거하기 리팩터링이 필요한 상황 2가지

  1. 사람들이 무조건 접근자 메서드를 통해서만 필드를 다루려 할 때
  2. 클라이언트에서 생성 스크립트를 사용해 객체를 생성할 때(생성 스크립트란 생성자를 호출한 후 일련의 세터를 호출하여 객체를 완성하는 형태의 코드)

절차

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

예시

간단한 사람 클래스

리팩터링 전

class Person {
  get name() {
    return this._name;
  }

  set name(value) {
    this._name = value;
  }

  get id() {
    return this._id;
  }

  set id(value) {
    this._id = value;
  }
}

const martin = new Person();
martin.name = 'Martin';
martin.id = 1;

id는 변경되며 안된다는 의도를 명확히 알리기 위해 ID 세터를 없애보자.

리팩터링 후

최초 한번은 ID를 설정할 수 있도록 생성자에서 ID를 받도록 해보자.

이후 생성 스크립트가 이 생성자를 통해 ID를 설정하게끔 수정하고, 모두 수정했다면 세터 메서드를 인라인 한다.

class Person {
  constructor(id) {
    this._id = id;
  }

  get name() {
    return this._name;
  }

  set name(value) {
    this._name = value;
  }

  get id() {
    return this._id;
  }
}

const martin = new Person('1');
martin.name = 'Martin';

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

배경

생성자는 객체를 초기화하는 특별한 용도의 함수이다. 그러나 생성자에는 일반 함수에는 없는 이상한 제약이 따라붙기도하여 이를 사용하기보다 팩터리 함수를 사용하여 제약을 없애보자.

절차

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

예시

직원 유형을 다루는, 간단하지만 이상한 예

리팩터링 전

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' };
  }
}

// 호출자
candidate = new Employee(document.name, document.empType);

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

팩터리 함수를 만들고, 생성자를 호출하는 곳을 찾아 수정하자.

리팩터링 후

function createEmployee(name, typeCode) {
    return new Employee(name, typeCode);
}

candidate = createEmployee(document.name, document.empType);

// 호출하는 부분의 리드 엔지니어 같은 경우는 문자열 리터럴을 건네는 방식이므로 좋지않고
// 차라리 createEnginner라는 형식으로 만든다.
function createEngineer(name) {
    return new Employee(name, 'E');
}

const leadEngineer = createEngineer(document.leadEngineer);

함수를 명령으로 바꾸기

반대 리팩터링: 명령을 함수로 바꾸기

배경

함수는 프로그래밍의 기본적인 빌딩 블록 중 하나다. 그런데 함수를 그 함수만을 위한 객체 안으로 캡슐화하면 더 유용해지는 상황이 있다. 이런 객체를 가리켜 '명령 객체' 혹은 단순히 명령이라 한다.

절차

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

예시

건강보험 애플리케이션에서 사용하는 점수 계산 함수다.

리팩터링 전

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;
  }

  // lots more code like this
  result -= Math.max(healthLevel - 5, 0);
  return result;
}

리팩터링 후

빈 클래스를 만들고, 함수를 클래스로 옮겨보자.

function 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;
    }

    // lots more code like this
    result -= Math.max(healthLevel - 5, 0);
    return result;
  }
}

이후 execute() 메서드가 매개변수를 받지 않게 생성자쪽으로 옮겨보자.

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

    // lots more code like this
    result -= Math.max(healthLevel - 5, 0);
    return result;
  }
}

이후 더 가다듬기에서 지역 변수를 필드로 바꾸고, 중첩 함수로 분리하여 리팩터링이 더 진행된다.

명령을 함수로 바꾸기

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

배경

로직이 크게 복잡하지 않다면 명령 객체는 장점보다 단점이 크니 평범한 함수로 바꿔주는게 낫다.

절차

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

예시

리팩터링 전

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;
  }
}

// 호출자
monthCharge = new ChargeCalculator(customer, usage, provider).charge;

이 명령 클래스는 간단하므로 함수로 대체해보자.

리팩터링 후

이 클래스를 생성하고 호출하는 코드를 함께 함수로 추출한다. 또한 값을 반환하는 메서드라면 먼저 반환할 값을 변수로 추출한다.

class ChargeCalculator {
  constructor(customer, usage, provider) {
    this._customer = customer;
    this._usage = usage;
    this._provider = provider;
  }

  get charge() {
    const baseCharge = this._customer.baseRate * this._usage;
    return baseCharge + this._provider.connectionCharge;
  }
}


function charge(customer, usage, provider) {
  return new ChargeCalculator(customer, usage, provider).charge;
}

monthCharge = charge(customer, usage, provider);

최종 코드는 다음과 같다.

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

// 호출자
monthCharge = charge(customer, usage, provider);

수정된 값 반환하기

배경

데이터가 어떻게 수정되는지를 추적하는 일은 코드에서 이해하기 가장 어려운 부분 중 하나다. 따라서 데이터가 수정된다면 그 사실을 명확히 알려주어서, 어느 함수가 무슨 일을 하는지 쉽게 알 수 있게 하는 일이 중요하다.

→ 좋은 방법으로, 변수를 갱신하는 함수라면 수정된 값을 반환하여 호출자가 그 값을 변수에 담도록 하는 것이다.

절차

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

예시

GPS 위치 목록으로 다양한 계산을 수행하는 코드

리팩터링 전

let totalAscent = 0;
let totalTime = 0;
let totalDistance = 0;
calculateAcent();
calculateTime();
calculateDistance();
const pace = totalTime / 60 / totalDistance;

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

calculateAscent() 안에서 totalAscent가 갱신된다는 사실이 드러나지 않음
→ 밖으로 알려보자.

리팩터링 후

totalAscent 값을 반환하고, 호출한 곳에서 변수에 대입하게 고친다.

const totalAscent = calculateAscent();
const totalTime = calculateTime();
const totalDistance = calculateDistance();
const pace = totalTime / 60 / totalDistance;

function calculateAscent() {
  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;
}
function calculateTime() {...}
function calculateDistance() {...}

위 처럼 코드가 바뀌게 된다.

오류 코드를 예외로 바꾸기

배경

예외는 프로그래밍 언어에서 제공하는 독립적인 오류 처리 메커니즘이다. 오류가 발견되면 예외를 던진다. 이러한 예외를 사용하면 오류 코드를 일일이 검사하거나 오류를 식별해 콜스택 위로 던지는 일을 신경쓰지 않아도 된다.

프로그램의 정상 동작 범주에 들지 않는 오류를 나타낼 때만 이러한 예외를 사용하자.

절차

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

예시

전역 테이블에서 배송지의 배송 규칙을 알아내는 코드

리팩터링 전

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

function calculateShippingCosts(anOrder) {
    const shippingRules = localShippingRules(anOrder.country);
    if (shippingRules < 0) return shippingRules; // 오류 전파
}

// 최상위
const status = calculateShippingCosts(orderData);
if (status < 0) errorList.push({order: orderData, errorCode: status});

이 경우 앞서 country(국가 정보)가 유효한지를 함수 호출전에 다 검증했다고 가정하였을 때, 위 코드들이 예상할 수 있는 정상 동작 범주안에 든다면 오류 코드를 예외로 바꾸는 리팩터링을 적용할 준비가 된것이다.

리팩터링 후

function localShippingRules(country) {
  const data = countryData.shippingRules[country];
  if (data) return new ShippingRules(data);
  throw new OrderProcessingError(-23);
}

function calculateShippingCosts(anOrder) {
  const shippingRules = localShippingRules(anOrder.country);
  if (shippingRules < 0) throw new Error('오류 코드가 다 사라지지 않음');
}

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

저자는 다른 예외와 구별을 위해 별도의 클래스(OrderProcessingError)를 만들어 처리하는것을 좋아하여 위와같이 코드를 작성한다.

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

배경

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

절차

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

예시

책의 예시론 자바코드로 자원들을 관리하는 과정에서 자원이 바닥나는 경우, 예외 처리로 대응하기 보다 그 상태를 확인하여 조건처리하는 식으로 문제를 해결한다.

profile
It is possible for ordinary people to choose to be extraordinary.

0개의 댓글