리팩토링 11장 7~13 정리

‘리팩터링 2판 - 마틴 파울러’를 읽고 정리한 내용입니다.

11-7. 세터 제거하기

세터 메서드가 있다는 것은 필드가 수정될 수 있다는 뜻이다.

세터 제거하기 리팩터링을 적용하는 상황은 두가지이다.

  1. 사람들이 무조건 접근자 메서드를 통해서만 필드를 다루려고 할 때
    • 세터를 제거해서 객체가 생성된 후에는 값이 바뀌면 안되도록 명시한다.
  2. 클라이언트에서 생성 스크립트를 사용해서 객체를 생성할때

생성 스크립트란? 생성자를 호출한 후 일련의 세터를 호출하여 객체를 완성하는 형태의 코드를 말한다.

해당 세터들은 처음 생성할 때만 호출되리라 가정하는 데 이런 경우에도 세터를 제거하여 의도를 분명히 하는 것이 좋다.

절차

  1. 설정해야 할 값을 생성자에서 받지 않는 다면 그 값을 받을 매개변수를 생성자에 추가 한다. (함수 선언 바꾸기) 그런 다음 생성자 안에서 적절한 세터를 호출한다.
  2. 생성자 밖에서 세터를 호출하는 곳을 찾아 제거하고, 대신 새로운 생성자를 사용하도록 한다. 하나 수정할 때마다 테스트 한다. ( 갱선하려는 대상이 공유참조 객체라서 새로운 객체를 생성하는 방식으로는 세터 호출을 대체할 수 없다면 이 리팩토링을 취소한다. )
  3. 세터 메서드를 인라인 한다. 가능하다면 해당 필드를 불변으로 만든다.
  4. 테스트 한다.

before

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 = "laura";
martin.id = "1234";

after

class Person {
  constructor(id, name){
    this.id = id;
    this._name = name;
  }
  get name() {return this._name;} 
  get id() {return this._id;} 
}
const martin = new Person("1234", "laura");

11-8. 생성자를 팩터리 함수로 바꾸기

생성자는 객체를 초기화하는 특별한 용도의 함수이다.

새로운 객체를 생성할때면 주로 생성자를 호출한다. 하지만 생성자에는 일반 함수에는 없는 이상한 제약이 붙기도 한다.

(반드시 그 생성자를 정의한 클래스의 인스턴스를 반환해야 한다던지, 생성자의 이름이 고정되어 더 나은 이름으로 변경할 수 없다던지 등등 )

절차

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

before

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 leadEngineer = new Employee(document.leadEngineer, 'E');

after - 1

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

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

const leadEngineer = createEmploye(document.leadEngineer, 'E');

after - 2

함수를 좀더 명확하게 createEngineer 엔지니어를 생성하는 함수로 나눠서 정의 한다.

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

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

const leadEngineer = createEngineer(document.leadEngineer);

11-9. 함수를 명령으로 바꾸기

함수를 그 함수만을 위한 객체 안으로 캡슐화하면 더 유용해지는 상황이 있다.

이런 객체를 가리켜 ‘명령 객체’ 혹은 명령 이라 한다. 명령 객체 대부분은 메서드 하나로 구성된다.

자바스크립트는 일급 함수이고 일급 함수와 명령 중 선택해야 한다면 일급 함수를 선택한다.

명령을 선택하는 때는 명령으로 해야 제일 간단한 방식의 기능을 얻을 수 있을 때이다.

( 일급 함수를 지원하지 않는 프로그래밍 언어에서 명령은 일급 함수를 흉내낼 수 있다. )

절차

  1. 대상 함수의 기능을 옮길 빈 클래스를 만든다. 클래스 이름은 함수 이름에 기초해서 짓는 다.
  2. 방금 생성한 빈 클래스로 함수를 옮긴다.
    • 이때 리팩토링이 끝날 때까지는 원래 함수를 전달 함수 역할로 남겨둔다.
  3. 함수의 인수들 각각은 명령의 필드로 만들어 생성자를 통해 설정할지 고민해본다.

ex) 건강 보험 앱에서 사용하는 점수 계산 함수

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 - 1

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

    // 비슷한 코드가 한참 이어짐
    result -= Math.max(healthLevel - 5, 0);
    return result;
  }
}

after - 2

여기에 모든 지역 변수를 필드로 바꿔준다.

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() {
    this._result = 0;
    this._healthLevel = 0;
    this._highMedicalRiskFlag = false;

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

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

11-10. 명령을 함수로 바꾸기

명령 객체는 복잡한 연산을 다를 수 있는 강력한 메커니즘을 제공하지만, 주로 그저 함수를 하나 호출해 정해진 일을 수행하는 용도로 쓰인다.

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

절차

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

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

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

after

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

monthCharge = charge(customer, usage, provider);

11-11. 수정된 값 반환하기

데이터가 어떻게 수정되는 지를 추적하는 일은 코드에서 이해하기 가장 어려운 부분 중 하나다.

절차

  1. 함수가 수정된 값을 반환하게 하여 호출자가 그 값을 자신의 변수에 저장하게 한다.
  2. 테스트한다.
  3. 피호출 함수 안에 반환할 값을 가리키는 새로운 변수를 선언한다. -> 이 작업이 제대로 되었다면 초기값이 무시된다.
  4. 테스트한다.
  5. 계산이 선언과 동시에 이뤄지도록 통합한다.( 즉 선언 시점에 계산 로직을 바로 실행해 대입한다.) -> 이 변수는 불변으로 지정되야 한다.
  6. 테스트한다.
  7. 피호출 함수의 변수 이름을 새 역할에 어울리도록 바꿔준다.
  8. 테스트한다.

before

let totalAscent = 0;

calculateAscent();

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

const totalAscent = calculateAscent();

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

11-12. 오류 코드를 예외로 바꾸기

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

예외를 사용하면 오류 코드를 일일이 검사하거나 오류를 식별해 콜스택 위로 던지는 일을 신경 쓰지 않아도 된다.

예외는 예상밖의 동작일 때만, 즉 프로그램의 정상 동작 범주에 들지 않는( 개발자가 의도하지 않은 방향으로 사용 등 ) 오류를 나타낼 때만 쓰여야 한다.

예외를 던지는 코드를 프로그램을 종료시키는 코드로 대체했을 때 프로그램이 여전히 동작하지 않으면 예외를 쓰면 안된다.

절차

  1. 콜스택 상위에 해당하는 예외를 처리할 예외 핸들러를 작성한다.
    • 처음에는 모든 예외를 다시 던지게 둔다.
    • 적절한 처리를 해주는 핸들러가 이미 있다면 지금의 콜스택도 처리할 수 있도록 확장한다.
  2. 테스트한다.
  3. 해당 오류 코드를 대체할 예외와 구분할 식별 방법을 찾는 다. -> 대부분 언어에서 서브클래스를 사용하면 된다.
  4. 정적 검사를 수행한다.
  5. catch절을 수정하여 직접 처리할 수 있는 예외는 적절히 대처하고 그렇지 않은 예외는 다시 던진다.
  6. 테스트한다.
  7. 오류 코드를 반환하는 곳 모두에서 예외를 던지도록 수정한다. 하나씩 수정할때마다 테스트한다.
  8. 모두 수정했다면 그 오류 코드를 콜스택 위로 전달하는 코드를 모두 제거한다. 하나씩 수정할 때마다 테스트한다.
    • 먼저 오류코드를 검사하는 부분을 함정으로 바꾼다음 함정에 걸려들지 않는 지 테스트한 후 제거하는 권략을 쓰자

before

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

after

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

//...

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

//...

11-13. 예외를 사전확인으로 바꾸기

예외는 ‘뜻 밖의 오류’라는 뜻으로 말 그대로 예외적으로 동작할 때만 쓰여야 한다.

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

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

절차

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

Discussion and feedback