본문 바로가기
Programming/13. Book

리팩터링 - 11장

by @sangseophwang 2024. 3. 2.

 

Chapter 11. API 리팩터링

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

배경

  • 우리는 외부에서 관찰할 수 있는 겉보기 부수효과(Observable Side Effect)가 전혀 없이 값을 반환해주는 함수를 추구해야 한다.

순수 함수: 같은 입력에 대해서 같은 결과를 반환하며 함수의 바깥 영역에 side effect를 초래하지 않는 함수

  • 겉보기 부수효과가 있는 함수와 없는 함수는 명확히 구분하는 것이 좋은데, 이를 위한 한 가지 방법은 ‘질의 함수(읽기 함수)는 모든 부수효과가 없어야 한다’ 는 규칙을 따르는 것이다.
  • 이를 명령-질의 분리라 한다.

명령-질의 분리 원칙

- 어떤 메서드는 명령을 실행(부작용을 생성하는 어떤 작업을 함)하거나 질의에 대답(어떤 값 반환)할 수 있으며, 두 작업을 모두하면 안된다.

- 즉 질의형 함수를 작성할 때는 요청한 값을 리턴하기만 하고, 데이터에 상태를 바꾸는 일은 하지 않는다. 반대로 데이터의 상태를 변경하는 함수는 값을 리턴하는 일도 하지 말아야 한다.

절차

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

예시

Before

// 함수 내용: 이름을 조회하며 악당을 찾으면 그 사람의 이름을 반환하는 경고를 울림.
// 목표 : 질의 함수에 부수효과를 낳는 부분 제거하기

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

const found = alertForVillian(people) // ⭐️ 악당을 찾고 알림을 하는 두 가지 역할 수행중

After

  1. 함수를 복제해 질의 목적에 맞는 이름을 짓는다. (alertForVillian => findVillian)
  2. 질의 함수에서 부수효과를 낳는 부분을 제거한다. (setOffAlarms())
  3. 새로운 질의 함수를 호출하도록 바꾼다.
function findVillian(people){ // ⭐️ 악당 색출 역할만 분리
  for (const p of people) {
    if (p === '조커') {
      return '조커';
    }
    if (p === '사루만') {
      return '사루만';
    }
  }
  return '';
}

...

function alertForVillian(people) {
  const villian = findVillian(people); // ⭐️ 악당 색출
  setOffAlarms(alarm, villian);        // ⭐️ 색출한 악당 알림
}

✏️ 함수 매개변수화하기

배경

  • 두 함수의 로직이 비슷하고 리터럴 값만 다르다면 매개변수로 받아 함수를 하나로 합쳐 중복을 없앨 수 있다.

절차

  1. 함수 선언 바꾸기로 리터럴들을 매개변수로 추가한다.
  2. 매개변수로 받을 값을 사용하도록 함수 본문을 수정한다.

예시

Before

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

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

After

function salaryRaise(person, factor) {
  person.salary = person.salary.multiply(1 + factor);
}

✏️ 플래그 인수 제거하기

배경

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

function bookConcert(customer, isPremium) {
  if (isPremium) { ... 프리미엄 예약용 로직 }
  else {  ... 일반 예약용 로직 }
}

bookConcert(customer, true);
bookConcert(customer, CustomerType.PREMIUM);
  • 플래그 인수는 호출할 수 있는 함수들이 무엇이고 어떻게 호출해야 하는지를 이해하기 어렵게 만들 수 있다.
  • 플래그 인수를 제거하면 코드가 깔끔해짐은 물론 프로그래밍 도구에도 도움을 준다.
  • 다만 플래그 인수 없이 구현하려면 플래그 인수들의 가능한 조합 수만큼의 함수를 만들어야 한다.
    • 다른 의미로 함수 하나가 많은 일을 처리하고 있다는 신호이다.
    • 이럴 때는 더 간단한 함수를 만들 방법을 고민해봐야 한다.

절차

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

예시

Before

// 함수 내용 : isRush 매개변수에 따라 배송일자를 다르게 계산
// 목표 : 플래그 인수 제거

// 아래 함수를 보기 전 두번째 인자만 봐서는 어떤 역할을 하는지 알 수 없다!
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);
  } 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);
  }
}

After

  1. 일반 배송 상태일 때, 배송이 밀릴 때의 조건에 따라 함수를 둘로 나누고, 기존 인수를 받아 분기 처리하는 함수를 만든다.
  2. 분리가 잘 됐다면 분기 처리 함수를 제거하고 각각 함수를 사용한다.
blueShip.deliveryDate = rushDeliveryDate(order);
redShip.deliveryDate = regularDeliveryDate(order);

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

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

Deep dive

  • 모든 플래그 인수를 가진 함수를 나눌 수는 없을 것이다. 분명 분기 처리를 요하는 함수도 있을텐데, 이럴 때는 어떻게 접근하는게 좋을까?
    • 책에는 플래그 인수 자체를 싫어한다기보다는 리터럴로 받아 처리하기에 문제가 되며, isRush와 같은 값을 사용하면 괜찮았을 수도 있다고 한다. 그렇다면 리터럴 값을 사용하는 함수 작성법만 지양해야 할까? 혹은 플래그 인수 함수 자체를 지양해야 할까?

✏️ 객체 통째로 넘기기

배경

  • 레코드를 통째로 넘기면 변화에 대응하기 쉽다.
    • 예컨대 그 함수가 더 다양한 데이터를 사용하도록 바뀌어도 매개변수 목록을 수정할 필요가 없다.
    • 그리고 매개변수 목록이 짧아져 함수 사용법을 이해하기 쉬워진다.
  • 이 때 적용하면 좋은 방법이 매개변수 객체 만들기다.
    • 즉, 산재한 수많은 데이터 더미를 새로운 객체로 묶은 후 적용하는 것이다.

절차

  1. 매개변수를 원하는 형태로 받는 빈 함수를 만든다.
  2. 새 함수의 본문에서는 원래 함수를 호출하도록 하고, 새 매개변수와 원래 매개변수를 매핑한다.
  3. 모든 호출자가 새 함수를 사용하게 수정한다.
  4. 모두 수정했다면 원래 함수를 인라인한다.

예시

Before

// 코드 내용 : withinRange에 매개변수를 전달하기 위해 low, high 상수를 생성 후 전달

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

const low = room.daysTempRange.low;
const high = room.daysTempRange.high;

if (!plan.withinRange(low, high)) {
  alerts.push('room temperature went outside range');
}

After

  1. 값을 그대로 전달해 함수 내에서 처리한다.
class HeatingPlan {
  ...

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

if (!plan.withinRange(room.daysTempRange)) {
  alerts.push('room temperature went outside range');
}

Deep dive

  • 개인적으로 좋은 방식이라고 생각합니다. 이번 상세 페이지 서버 컴포넌트에서 페칭된 데이터를 props로 전달할 때 위 방식을 사용했는데, 해당 컴포넌트에 어떤 데이터가 전달되는지 명시적으로 알 수 있기도 하고 추후 데이터 추가 및 제거 시 관리 포인트를 줄일 수 있어 좋았습니다.
  • 객체의 크기를 생각해 봐야 할 거 같아요. (커봐야 큰 영향 없겠지만..) 개인적으로 한두 개 정도는 그냥 분해해서 넘기는 편인 것 같습니다.

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

배경

  • 매개변수 목록은 함수의 변동 요인을 모아놓은 곳이다.
    • 즉, 함수의 동작에 변화를 줄 수 있는 일차적인 수단이다.
  • 피호출 함수가 내부에서 값의 사용을 결정하도록 주체를 변경하게 되면 매개변수를 줄이며 함수의 책임 범위가 명확해질 수 있다.
  • 주의사항이 하나 있다.
    • 매개변수를 함수로 변경했을 때 대상 함수가 참조 투명(referential transparency)해야 한다는 것이다.
    • 참조 투명이란 ‘함수가 똑같은 값을 건네 호출하면 항상 똑같이 동작한다’는 뜻이다.
    • 이런 특성이 있어야 동작을 예측하고 테스트하기 쉬워진다.

절차

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

예시

Before

// 클래스 내용 : discountedPrice 함수에 매개변수를 두 개 전달
// 목표 : 매개변수를 질의함수로 바꾸기

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

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

After

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

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

  // 📍 basePrice 변수 분리
  get basePrice() {
    return this.quantity * this.itemPrice;
  }

  // 📍 discountLevel 변수 분리
  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;
    }
  }
}

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

배경

  • 코드를 읽다보면 거북한 참조를 발견할 때가 있다.
    • 전역 변수를 참조하거나
    • 제거하길 원하는 원소를 참조하거나
  • 이 문제는 해당 참조를 매개변수로 바꿔 해결할 수 있다.
    • 참조를 풀어내는 책임을 호출자로 옮기는 것이다.
  • 이런 상황 대부분은 코드의 의존 관계를 바꾸려 할 때 벌어지며, 해당 코드를 더 잘 이해하게 됐을 때 질의함수와 매개변수 중 선택해 개선하는 것이 중요하다.
  • 또 다른 케이스는 참조 투명하지 않은 원소에 접근해 참조 투명성을 잃게 됐을 때 해당 원소를 매개변수로 바꾸는 것이다.
  • 이 리팩터링에도 단점이 있다.
    • 질의 함수를 매개변수로 바꾸면 어떤 값을 제공할지를 호출자가 알아내야 한다.
    • 결국 호출자가 복잡해지는 결과를 낳는다.
    • 이 문제는 결국 책임 소재를 프로그램의 어디에 배정하느냐의 문제로 귀결된다.

절차

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

예시

Before

thermostat : 온도 조절 장치

// 클래스 내용 : 온도 조절 장치의 목표 온도와 현재 온도를 비교해 난방을 조절
// 문제 : targetTemperature() 메서드가 전역 객체인 thermostat에 의존중
// 목표 : 질의함수를 매개변수로 바꾸기

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

// 호출자
if (thePlan.targetTemperature > thermostat.currentTemperature) setToHeat();
else if (thePlan.targetTemperature < thermostat.currentTemperature) setToCool();
else setOff();

After

// 📍 각 조건별로 thermostat.selectedTemperature 전달
if (
  thePlan.targetTemperature(thermostat.selectedTemperature) >
  thermostat.currentTemperature
)
  setToHeat();
else if (
  thePlan.targetTemperature(thermostat.selectedTemperature) <
  thermostat.currentTemperature
)
  setToCool();
else setOff();

// 📍 selectedTemperature 매개변수 생성
class HeatingPlan {
  targetTemperature(selectedTemperature) {
    if (selectedTemperature > this._max) return this._max;
    else if (selectedTemperature < this._min) return this._min;
    else return selectedTemperature;
  }
}

✏️ 세터 제거하기

배경

  • 세터 메서드가 있다고 함은 필드가 수정될 수 있다는 뜻이다.
  • 세터 제거하기가 필요한 상황은 주로 두 가지다.
    • 사람들이 무조건 접근자 메서드를 통해서만 필드를 다루려 할 때
    • 클라이언트에서 생성 스크립트를 사용해 객체를 생성할 때

생성 스크립트 : 생성자를 호출한 후 일련의 세터를 호출해 객체를 완성하는 형태의 코드

  • 가장 좋은건 이런 세터들을 제거해 값이 바뀌지 않게 하며 의도를 더 정확히 전달하는 것이다.

절차

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

예시

Before

// 클래스 내용 : 이름과 id를 가져오고 변경하는 게터, 세터 함수 포함
// 목표 : 세터 제거하기

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

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

After

class Person {
  constructor(name, id) {
    this._name = name;
    this._id = id;
  }
  
  get name() {return this._name;}
  get id() {return this._id;}
}

const martin = new Person('마틴', '1234');

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

배경

  • 많은 객체 지향 언어에서 제공하는 생성자는 객체를 초기화하는 특별한 용도의 함수다.
    • 실제로 새로운 객체를 생성할 때면 주로 생성자를 호출한다.
  • 이런 생성자 호출 방식은 몇몇 제약이 있는데, 팩토리 함수는 제약 없이 대체해 사용할 수 있다.

절차

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

예시

Before

// 

class Employee {
  constructor(name, code) {
    this._name = name;
    this._code = code;
  }
  
  get name() {return this._name;}
  get code() {
    return Employee.legalCode[this._code];
  }
  
  static get legalCode() {
    return ["E": "Engineer", "M": "Manager"];
  }
}

const candidate = new Employee(document.name, document.code);

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

After

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

// 📍 직원 유형 리터럴을 팩터리 함수에 적용
function createEngineer(name) {
  return new Employee(name, "E");
}

const candidate = createEmployee(document.name, document.code);
const leadEngineer = createEngineer(document.leadEngineer);

✏️ 함수를 명령으로 바꾸기

배경

  • 함수는 프로그래밍의 기본적인 빌딩 블록 중 하나다.
  • 그런데 함수를 그 함수만을 위한 객체 안으로 캡슐화하면 더 유용해지는 상황이 있다.
    • 이런 객체를 가리켜 ‘명령 객체’ 또는 ‘명령(command)’이라 한다.
    • 명령 객체 대부분은 메서드 하나로 구성되며, 이 메서드를 요청해 실행하는 것이 이 객체의 목적이다.
  • 명령은 평범한 함수 메커니즘보다 훨씬 유연하게 함수를 제어하고 표현할 수 있다.
    • 되돌리기같은 보조 연산 제공
    • 수명주기를 더 정밀하게 제어하는 데 필요한 매개변수를 만들어주는 메서드 제공
    • 상속과 훅을 이용해 사용자 맞춤형 제작 가능
  • 이처럼 명령을 사용해 얻는 이점이 많으므로 함수를 명령으로 리팩터링할 채비를 갖춰야 할 것이다.
  • 하지만 유연성은 복잡성을 키우고 얻는 대가이므로, 가능하면 명령보다는 일급 함수를 선택하는 쪽이 좋다.

일급 함수 조건

1. 변수에 담을 수 있다.

2. 인자로 전달할 수 있다.

3. 반환값으로 전달할 수 있다.

절차

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

예시

Before

// 건강보험 애플리케이션


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

After

// 📍클래스 내부에 execute() 명령 함수를 생성해 결과 도출 로직 분리

function score(candidate, medicalExam, scoringGuide) {
  return new Scorer(candidate, medicalExam, scoringGuide).excute();
}

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

  excute() {
    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. 죽은 코드 제거하기로 명령 클래스를 제거한다.

예시

Before

// 클래스 내용 : 소비자의 충전량을 반환하는 클래스
// 목표 : 명령을 함수로 바꾸기

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;

After

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

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

✏️ 수정된 값 반환하기

배경

  • 데이터가 어떻게 수정되는지를 추적하는 일은 코드에서 이해하기 어려운 부분 중 하나다.
  • 특히 수정하는 코드가 여러 곳이라면 그 흐름을 파악하기 어려울 수 있다.
  • 데이터가 수정됨을 알려주는 좋은 방법이 있다.
    • 변수를 갱신하는 함수라면 수정된 값을 반환하여 호출자가 그 값을 변수에 담아두도록 하는 것이다.

절차

  1. 함수가 수정된 값을 반환하게 하여 호출자가 그 값을 자신의 변수에 저장하게 한다.
  2. 피호출 함수 안에 반환할 값을 가리키는 새로운 변수를 선언한다.
  3. 계산이 선언과 동시에 이루어지도록 통합한다.

예시

Before

// 함수 내용 : totalAscent 변수에 고도의 변화에 따라 값을 더해 변수 값을 변경
// 목표 : 수정된 값을 반환하는 불변 변수 생성

let totalAscent = 0;
let totalTime = 0;
let totalDistance = 0;
calculateAscent();
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;
  }
}

After

// totalAscent 변수 값을 변경하는 대신 calculateAscent 반환값을 담을 변수 생성
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;
}

✏️ 오류 코드를 예외로 바꾸기

배경

  • 예외는 프로그래밍 언어에서 제공하는 독립적인 오류 처리 매커니즘이다. 오류가 발견되면 예외를 던진다.
  • 그러면 적절한 예외 핸들러를 찾을 때까지 콜스택을 타고 위로 전파된다.
  • 예외를 사용하면 오류 코드를 일일이 검사하거나 오류를 식별해 콜스택 위로 던지는 일을 신경쓰지 않아도 된다.
  • 예외는 정확히 예상 밖의 동작일 때만 쓰여야 한다.
    • 즉, 정상 동작하지 않을 것 같다면 예외를 사용하지 말라는 신호다.
    • 예외 대신 오류를 검출하여 프로그램을 정상 흐름으로 되돌리게끔 처리해야 한다.

절차

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

예시

Before

function localShippingRules(data) {
  if (data) return new ShippingRules(data);
  else return -23;
}

After

// 📍 에러 출력 클래스 추가
function localShippingRules(data) {
  if (data) return new ShippingRules(data);
  else throw new OrderProcessingError(-23);
}

// 📍 에러 코드에 따라 메세지 출력
class OrderProcessingError extends Error {
  constructor(errorCode) {
    super(errorCode);
    this.code = errorCode;
  }
  get name() {
    return 'OrderProcessingError';
  }
}

// 사용할 때
try {
  localShippingRules();
} catch (error) {
  if (error instanceof OrderProcessingError) {
    console.log(error);
  }
}


- 에러 메세지로 다이얼로그 컨텐트를 제어하는 로직이랑 비슷하네요.

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

배경

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

절차

  1. 예외를 유발하는 상황을 검사할 수 있는 조건문을 추가한다. (try-catch)
  2. 예외 조건(catch 블록)에 어서션을 추가하고 테스트한다.
  3. try-catch문을 제거한다.

예시

Before

// 함수 내용 : values 배열에 periodNumber번째 값이 있으면 해당 값을, 없으면 에러를 출력

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('에러 발생!');
}

After

// 예외 처리 값을 만든 후 예외 출력 구간 제거

const values = [];
function getValueForPeriod(periodNumber) {
  return values[periodNumber] ?? 0;
}

getValueForPeriod(-10);
반응형

'Programming > 13. Book' 카테고리의 다른 글

리팩터링 - 7장  (1) 2024.03.02
리팩터링 - 12장  (0) 2024.03.02
리팩터링 - 10장  (0) 2024.03.02
리팩터링 - 8, 9장  (0) 2024.03.02
리팩터링 - 5, 6장  (0) 2024.03.02

댓글