6장, 기본적인 리팩터링

2021.10.18

1. Extract Function, 함수 추출하기

  • 함수를 만드는 합리적인 기준은 목적과 구현을 분리하는 것.
  • 함수의 길이는 중요하지 않다.

절차

  1. 함수를 새로 만들고 목적을 잘 드러내는 이름을 붙인다. ( 어떻게가 아닌 무엇을 하는지가 드러나야 한다 )
  2. 추출할 코드를 원본 함수에서 복사하여 새 함수에 붙여넣는다.
  3. 추출한 코드 중 원본 함수의 지역 변수를 참조하거나 추출한 함수의 유효범위를 벗어나는 변수는 없는지 검사한다. 있다면 매개변수로.
  4. 변수를 다 처리했다면 컴파일한다.
  5. 원본 함수에서 추출한 코드 부분을 함수 호출하는 문장으로 변경.
  6. 테스트.
  7. 다른 코드에 유사한 코드가 있다면, 방금 작성한 함수를 호출하는 것으로 변경.

예시

before

function printOwing(invoice) {
  let outstanding = 0;

  console.log('*****************');
  console.log('**** 고객 채무 ****');
  console.log('*****************');

  // 미해결 채무(outstanding)를 계산합니다.
  for (const o of invoice.orders) {
    outstanding += o.amount;
  }

  // 마감일(dueDate)을 기록합니다.
  const today = Clock.today;
  invoice.dueDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 30);

  // 세부 사항을 출력합니다.
  console.log(`고객명: ${invoice.customer}`);
  console.log(`채무액: ${outstanding}`);
  console.log(`마감일: ${invoice.dueDate.toLocaleDateString()}`);
}

after

function printOwing(invoice) {
  printBanner();

  let outstanding = calculateOutStanding(invoice);

  recordDueDate(invoice);
  printDetails(invoice, outstanding);
}

function printBanner() {
  console.log('*****************');
  console.log('**** 고객 채무 ****');
  console.log('*****************');
}

function calculateOutStanding(invoice) {
  let result = 0;
  for (const o of invoice.orders) {
    result += o.amount;
  }
  return result;
}

function recordDueDate(invoice) {
  // 마감일(dueDate)을 기록합니다.
  const today = Clock.today;
  invoice.dueDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 30);
}

function printDetails(invoice, outstanding) {
  // 세부 사항을 출력합니다.
  console.log(`고객명: ${invoice.customer}`);
  console.log(`채무액: ${outstanding}`);
  console.log(`마감일: ${invoice.dueDate.toLocaleDateString()}`);
}

2. Inline Function, 함수 인라인하기

  • 함수 본문의 코드가 이름보다 명확하다면 함수를 제거하여 간접호출을 피한다.
  • 다른 함수로 단순히 위임하기만 하는 함수가 많고 위임 관계가 복잡하면 인라인 처리.

절차

  1. 다형성을 가지고 있는 메서드인지 확인한다. ( 다형성이 있다면 인라인 X )
  2. 인라인할 함수를 호출하는 곳을 모두 찾는다.
  3. 각 호출문을 함수 본문의 코드로 교체.
  4. 하나씩 교체할때 마다 테스트.
  5. 함수 삭

예시

before

function getRating(driver) {
  return moreThanFiveLateDeliveries(driver) ? 2 : 1;
}

function moreThanFiveLateDeliveries(driver) {
  return driver.numberOfLateDeliveries > 5;
}

after

function getRating(driver) {
  return driver.numberOfLateDeliveries > 5 ? 2 : 1;
}

3. Extract Variable, 변수 추출하기

  • 복잡한 로직을 구성하는 단계마다 변수를 생성하여 적절한 네이밍.
  • breakpoint를 걸어 디버깅 용이.
  • 현재 함수 안에서만 의미가 있다면 변수로 추출하는 것이 좋다.
  • 함수를 벗어나 넓은 문맥에서 까지 의미가 있다면 함수로 추출. ( 넓은 범위에서 통용될만한 네이밍. )

절차

  1. 추출하려는 표현식에 부작용은 없는지 확인.
  2. 불변 변수를 하나 선언하고 일므을 붙일 표현식의 복제본을 대입.
  3. 원본 표현식을 새로 만든 변수로 교체.
  4. 테스트
  5. 표현식을 여러 곳에서 사용한다면, 각각 새로 만든 변수로 교체.
  6. 하나씩 교체할때 마다 테스트.

예시

before

return (
  order.quantity * order.itemPrice -
  Math.max(0, order.quantity - 500) * order.itemPrice * 0.05 +
  Math.min(order.quantity * order.itemPrice * 0.1, 100)
);

after

const basePrice = order.quantity * order.itemPrice;
const quantityDiscount = Math.max(0, order.quantity - 500) * order.itemPrice * 0.05;
const shipping = Math.min(order.quantity * order.itemPrice * 0.1, 100);
return basePrice - quantityDiscount + shipping;

4. Inline Variable, 변수 인라인하기

  • 변수명이 원래 표현식과 다를 바 없을 때

절차

  1. 대입문의 표현식에서 부작용이 없는지 확인.
  2. 변수가 불변으로 선언되지 않았다면 불변으로 만든 후 테스트.
  3. 변수를 가장 처음으로 사용하는 코드를 표현식 코드로 변경.
  4. 테스트
  5. 변수를 사용하는 부분을 모두 교체할 때까지 과정 반복.
  6. 변수 선언문과 대입문 삭제.
  7. 테스트

예시

before

let basePrice = anOrder.basePrice;
return basePrice > 1000;

after

return anOrder.basePrice > 1000;

5. Change Function Declaration, 함수 선언 바꾸기

  • 좋지 않은 함수명이 있고, 더 나은 이름이 떠오른다면 즉시 바꿔주자.
  • 함수의 매개변수는 함수와 외부 모듈과의 결합을 낮추는 방식도 좋다. ( 매개변수가 자체적인 데이터 구조를 가지는 방식 )
    • 외부 모듈과의 결합이 낮아진다면 활용도가 넓어진다.
    • 하지만 모듈과의 결합이 있다고 나쁜 것은 아니다.
    • 매개변수로 전달받은 모듈 내부의 참조가 가능하여 때에 따라 복잡한 로직 처리를 이 함수내에서만 할 수 있을 수도 있다. ( 정답은 없다 )

절차

간단한 절차

호출하는 곳이 많거나, 호출 과정이 복잡하거나, 호출 대상이 다형 메서드거나 선언을 복잡하게 변경할 때 사용

  1. 매개변수를 제거하려거든 먼저 함수 본문에서 제거 대상 매개변수를 참조하는 곳이 없는지 확인한다.
  2. 메서드 선언을 원하는 형태로 바꾼다.
  3. 기존 메서드 선언을 참조하는 부분을 모두 찾아서 바뀐 형태로 수정한다.
  4. 테스트.

마이그레이션 절차

변경할 것이 둘 이상이면 나눠서 처리할 때가 나을 때가 있으며, 이름 변경과 매개변수 추가를 모두 하고 싶다면 각각을 독립적으로 처리. (이후 문제가 생길 시 돌리기 위해서)

  1. 이어지는 추출 단계를 수월하게 만들어야 한다면 함수의 본문을 적절히 리팩터링한다.
  2. 함수 본문을 새로운 함수로 추출한다.
  3. 추출한 함수에 매개변수를 추가해야 한다면 간단한 절차를 따라 추가한다.
  4. 테스트.
  5. 기존 함수를 인라인한다.
  6. 테스트.

예시

before

function circum(radius) {
  return 2 * Math.PI * radius;
}

after

function circumference(radius) {
  return 2 * Math.PI * radius;
}

6. Encapsulate Variable, 변수 캡슐화하기

  • 접근할 수 있는 범위가 넓은 데이터를 옮길 때는 먼저 그 데이터로의 접근을 독점하는 함수를 만드는식으로 캡슐화하는게 좋다.
  • 데이터를 변경하고 사용하는 코드를 감시할 수 있는 통로가 되기 때문에, 데이터 변경 전 검증이나 변경 후 추가 로직을 쉽게 넣을 수 있다.
  • 데이터의 유효범위가 넓을수록 캡슐화하는 것이 좋다.
  • 불변 데이터는 가변 데이터보다 캡슐화할 필요가 없다.

절차

  1. 변수로의 접근과 갱신을 전담하는 캡슐화 함수들을 만든다.
  2. 정적 검사를 수행한다.
  3. 변수를 직접 참조하던 부분을 모두 적절한 캡슐화 함수 호출로 바꾼다. ( 하나씩 바꿀때마다 테스트. )
  4. 변수의 접근 범위를 제한한다.
  5. 테스트.
  6. 변수 값이 레코드라면 레코드 캡슐화하기를 적용할지 고려해본다.

예시

before

let defaultOwner = { firstName: '마틴', lastName: '파울러' };

after

class Person {
  constructor(data) {
    this._lastName = data.lastName;
    this._firstName = data.firstName;
  }

  get lastName() {
    return this._lastName;
  }
  get firstName() {
    return this._firstName;
  }
}

7. Rename Variable, 변수 이름 바꾸기

  • 명확한 프로그래밍의 핵심은 이름짓기이다.
  • 함수 호출 한 번으로 끝나지 않고 값이 영속되는 필드라면 이름에 더 신경 써야 한다.

절차

  1. 폭넓게 쓰이는 변수라면 캡슐화하기를 고려한다.
  2. 이름을 바꿀 변수를 참조하는 곳을 모두 찾아서 하나씩 변경한다.
  3. 테스트.

예시

before

let a = height * width;

after

let area = height * width;

8. Introduce Parameter Object, 매개변수 객체 만들기

  • 데이터 뭉치를 데이터 구조로 묶으면 데이터 사이의 관계가 명확해진다.
  • 같은 데이터 구조를 사용하는 모든 함수가 원소를 참조할 때 똑같은 이름을 사용하기 때문에 일관성이 높아진다.
  • 데이터 구조에 담길 데이터에 공통으로 적용되는 동작을 추출해서 함수로 만들 수 있다.
  • 이 함수들과 데이터를 합쳐 클래스로도 만들 수 있다.
  • 새로 만든 데이터 구조가 문제 영역을 훨씬 간결하게 표현하는 새로운 추상개념으로 격상되면서, 코드의 개념적인 그림을 다시 그릴 수 있다.

절차

  1. 적당한 데이터 구조가 아직 마련되어 있지 않다면 새로 만든다.
  2. 테스트.
  3. 함수 선언 바꾸기로 새 데이터 구조를 매개변수로 추가한다.
  4. 테스트.
  5. 함수 호출 시 새로운 데이터 구조 인스턴스를 넘기도록 수정한다. ( 하나씩 테스트 )
  6. 기존 매개변수를 사용하던 코드를 새 데이터 구조의 원소를 사용하도록 바꾼다.
  7. 다 바꿨다면 기존 매개변수를 제거하고 테스트.

예시

before

function amountInvoiced(startDate, endDate) {}
function amountReceived(startDate, endDate) {}
function amountOverdue(startDate, endDate) {}

after

function amountInvoiced(aDateRange) {}
function amountReceived(aDateRange) {}
function amountOverdue(aDateRange) {}

9. Combine Functions to Class, 여러 함수를 클래스로 묶기

  • 공통 데이터를 중심으로 엮여 작동하는 함수 무리를 클래스로 묶는다.
  • 이 함수들이 공유하는 공통 환경을 더 명확하게 표현가능.
  • 각 함수에 전달되는 인수를 줄여서 객체 안에서의 함수 호출을 간결하게 만들 수 있다.

절차

  1. 함수들이 공유하는 공통 데이터 레코드를 캡슐화한다.
  2. 공통 레코드를 사용하는 함수 각각을 새 클래스로 옮긴다.
  3. 데이터를 조작하는 로직들은 함수로 추출해서 새 클래스로 옮긴다.

예시

before

function base(aReading) {}
function taxableCharge(aReading) {}
function calculateBaseCharge(aReading) {}

after

class Reading {
  base() {}
  taxableCharge() {}
  calculateBaseCharge() {}
}

10. Combine Functions into Transform, 여러 함수를 변환 함수로 묶기

  • 소프트웨어는 데이터를 입력받아서 여러 가지 정보를 도출하곤 한다.
  • 여러 군데에서 사용하는 도출 로직들을 한군데로 모아두면 검색과 갱신을 일관된 장소에서 처리할 수 있고 중복도 막을 수 있다.
  • 여러 함수를 하나로 묶은 하나의 변환 함수를 통해 데이터를 도출하는 로직들의 중복을 막는다.

절차

  1. 변환할 레코드를 입력받아서 값을 그대로 반환하는 변환 함수를 만든다.
  2. 묶을 함수 중 함수 하나를 골라서 본문 코드를 변환 함수로 옮기고, 처리 결과를 레코드에 새 필드로 기록한다. 그런 다음 클라이언트 코드가 이 필드를 사용하도록 수정한다.
  3. 테스트.
  4. 나머지 관련 함수도 위 과정에 따라 처리한다.

예시

before

function base(aReading) {}
function taxableCharge(aReading) {}

after

function enrichReading(argReading) {
  const aReading = _.cloneDeep(argReading);
  aReading.baseCharge = base(aReading);
  aReading.taxableCharge = taxableCharge(aReading);
  return aReading;
}

11. Split Phase, 단계 쪼개기

  • 서로 다른 두 대상을 한꺼번에 다루는 코드는 별개 모듈로 나누면 좋다.
  • 가장 간편한 방법은 하나의 동작을 연이은 두 단계로 쪼개는 것이다.

절차

  1. 두 번쨰 단계에 해당하는 코드를 독립 함수로 추출한다.
  2. 테스트.
  3. 중간 데이터 구조를 만들어서 앞에서 추출한 함수의 인수로 추가한다.
  4. 테스트.
  5. 추출한 두 번째 단계 함수의 매개변수를 하나씩 검토한다. 그중 첫 번째 단계에서 사용되는 것은 중간 데이터 구조로 옮긴다. ( 하나씩 옮길때마다 테스트 )
  6. 첫 번째 단계 코드를 함수로 추출하면서 중간 데이터 구조를 반환하도록 만든다.

예시

before

const orderDate = orderString.split(/\s+/);
const productPrice = priceList[orderDate[0].split('-')[1]];
const orderPrice = parseInt(orderDate[1]) * productPrice;

after

const orderRecord = parseOrder(order);
const orderPrice = price(orderRecord, priceList);

function parseOrder(aString) {
  const values = aString.split(/\s+/);
  return {
    productID: values[0].split('-')[1],
    quantity: parseInt(values[1]),
  };
}
function price(order, priceList) {
  return order.quantity * priceList[order.productID];
}