본문 바로가기
Programming/13. Book

리팩터링 - 5, 6장

by @sangseophwang 2024. 3. 2.

Chapter 05. 리팩터링 카탈로그 보는 법

❗️ 리팩터링 설명 형식

이름

리팩토링 용어 지칭

개요

리팩터링의 핵심 개념을 간략히 표현

개념도 + 코드 예시

배경

해당 기법이 왜 필요한지, 또는 적용하면 안되는 상황이 어떤건지에 대해 설명

절차

리팩터링 과정을 단계별로 제시

예시

리팩터링 실제 적용 예시 & 효과

Chapter 06. 기본적인 리팩터링

✏️ 함수 추출하기

배경

  • 코드 조각을 찾아 무슨 일을 하는지 파악한 다음, 독립된 함수로 추출하고 목적에 맞는 이름을 붙인다.
  • 이렇게 했을 때 함수 이름만으로 목적을 빠르게 파악할 수 있고, 본문 코드에 크게 신경쓰지 않아도 되기 때문이다.
  • 이 방식으로 여러 짧은 함수가 만들어져 호출량의 증가로 성능 저하가 있다고 생각할 수 있지만, 함수가 짧으면 캐싱하기 더 쉽기 때문에 컴파일러가 최적화하는데 유리하다는 장점이 있다.

절차

  1. 함수의 목적을 드러내는 이름을 붙인다. ('어떻게' 가 아닌 ‘무엇을’ 하는지 드러나도록)
    1. 이름이 떠오르지 않는다면 함수로 추출하면 안 된다는 신호다.
  2. 추출할 코드를 새 함수에 복사한다.
  3. 참조하는 지역변수는 매개변수로 전달
    • 새 함수에서만 사용되는 변수는 지역변수로
    • 지역변수의 값을 변경할 경우 새 함수의 결과로 전달
  4. 새로 만든 함수를 호출하는 문으로 수정한다.

예시

Before

function printOwing(invoice) {
  let outstanding = 0;

  // 배너 출력
  console.log('***********************');
  console.log('******* 고객 채무 *******');
  console.log('***********************');

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

  // 마감일 기록
  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 outstanding = 0;
  for (const o of invoice.orders) {
    outstanding += o.amount;
  }
  return outstanding;
}

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

✏️ 함수 인라인하기

배경

  • 함수 본문이 이름만큼 명확한 경우, 또는 함수 본문 코드를 이름만큼 깔끔하게 리팩터링할 수 있는 경우
  • 쓸데없는 간접 호출은 오히려 불필요함을 야기할 수 있다.

절차

  1. 다형 메서드인지 체크
    • 서브클래스에서 오버라이딩된 메서드는 인라인 금지
  2. 모든 호출문을 (점진적으로) 인라인으로 교체

예시

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

✏️ 변수 추출하기

배경

  • 표현식이 너무 복잡해 이해하기 어려울 때
  • 지역 변수를 활용해 표현식을 쪼개 관리하기 쉽도록 만든다.
    • 이를 통해 코드의 목적을 훨씬 명확하게 드러낼 수 있다.
    • 이 과정에서 추가한 변수는 디버깅 중단점(breakpoint)으로 도움이 될 수 있다.
  • 문맥을 고려해 현재 선언된 함수보다 더 넓은 문맥에서까지 쓰이는 역할과 이름을 갖게 된다면 함수로 추출하는 것을 권장

절차

  1. 추출하려는 표현식에 부작용(side effect)이 없는지 확인한다.
  2. 상수를 선언하고 표현식을 대입한다.
  3. 원본 표현식을 새로 만든 상수로 교체한다.

예시

Before

// price(가격) = 기본 가격 - 수량 할인 + 배송비

function price(order) {
  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(basePrice * 0.1, 100);

return basePrice - quantityDiscount + shipping;

✏️ 변수 인라인하기

배경

  • 변수명이 원래 표현식과 다를 바 없을 때
  • 오히려 변수가 주변 코드를 리팩터링하는 데 방해가 되기도 한다.

절차

  1. 인라인할 표현식에 부작용(side effect)이 없는지 확인한다.
  2. 상수인지 확인하고, 아니라면 상수로 변경 후 테스트한다.
  3. 테스트 과정에서 변수에 값이 단 한번만 대입되는지 확인한다.
  4. 변수를 표현식으로 교체한다.

예시

Before

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

After

return anOrder.basePrice > 1000;

✏️ 함수 선언 바꾸기

다른 이름

  • 함수 이름 바꾸기
  • 시그니처 바꾸기

배경

  • 함수는 소프트웨어 시스템의 구성 요소를 조립하는 연결부 역할을 한다.
  • 연결부를 잘 정의하면 시스템에 새로운 부분을 추가하기 쉬워지는 반면, 잘못 정의하면 지속적인 방해 요인으로 작용해 소프트웨어 동작을 파악하기 어려워지고 요구사항에 대한 유연성이 떨어지게 된다.
  • 이러한 연결부에서 가장 중요한 요소는 함수의 이름이다.

함수의 이름이 떠오르지 않을 때 ‘주석을 이용해 함수의 목적을 설명’ 해보는 방식이 도움이 된다.

절차

마이그레이션 절차의 복잡도에 따라 간단한 절차마이그레이션 절차로 구분지어 따름

간단한 절차

  1. 매개변수 제거 시 참조하는 곳이 있는지 확인한다.
  2. 메서드 선언을 원하는 형태로 바꾼다.

마이그레이션 절차

  1. 함수 본문을 새로운 함수로 추출한다.
  2. 추출한 함수에 매개변수를 추가해야 한다면 간단한 절차를 따라 추가한다.
  3. 기존 함수를 인라인한다.
  4. 임시 이름을 붙인 새 함수를 원래 이름으로 수정한다.

예시 1. 간단한 절차

Before

// 함수 이름을 너무 축약한 예

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

After

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

예시 2. 마이그레이션 절차

Before

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

After

// 본문 전체를 새로운 함수로 추출
// 테스트 후 기존 함수 제거
// 이 때 제거될 함수에는 'deprecated' 표시

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

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

예시 3. 매개변수를 속성으로 바꾸기

Before

function inNewEngland(aCustomer) {
  return ['MA', 'CT', 'ME', 'VT', 'NH', 'RI'].includes(aCustomer.address.state);
}

const newEnglanders = someCustomers.filter(c => inNewEngland(c));

After

const newEnglanders = someCustomers.filter(c => inNewEngland(c.address.state));

function inNewEngland(stateCode) {
  return ['MA', 'CT', 'ME', 'VT', 'NH', 'RI'].includes(stateCode);
}

✏️ 변수 캡슐화하기

캡슐화: 객체, 즉 클래스 안에 서로 연관있는 속성과 기능들을 하나의 캡슐(capsule)로 만들어 데이터를 외부로부터 보호하는 것.

배경

  • 함수는 대체로 호출하는데 사용되고, 함수의 이름을 바꾸거나 다른 모듈로 옮기는 건 어렵지 않다.
  • 반면 데이터는 유효범위가 넓을수록 참조하는 모든 부분을 바꿔야 코드가 제대로 동작하기 때문에 쉽지 않을 수 있다.
  • 그래서 접근 범위가 넓은 데이터를 그 데이터로의 접근을 독점하는 함수를 만드는 식으로 캡슐화하는 것이 좋다.
  • 데이터 캡슐화는 데이터를 변경하고 사용하는 코드를 감시할 수 있는 확실한 통로가 되어주기 때문에 데이터 변경 전 검증, 변경 후 추가 로직 추가가 용이하다.
  • 불변 데이터는 변경될 일이 없기 때문에 캡슐화할 이유가 적다.

절차

  1. 변수로의 접근과 갱신을 전담하는 캡슐화 함수를 만든다.
  2. 변수를 직접 참조하던 부분을 모두 적절한 캡슐화 함수 호출로 수정한다.
  3. 변수의 접근 번위를 제한한다.

예시

Before

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

spaceship.owner = defaultOwner;

defaultOwner = {firstName: "레베카", lastName: "파슨스"};

After

function getDefaultOwner() {
  return defaultOwner;
}

function setDefaultOwner(arg) {
  defaultOwner = arg;
}

// 게터 함수 호출
spaceship.owner = getDefaultOwner();

// 세터 함수 호출
setDefaultOwner({firstName: "레베카", lastName: "파슨스"});

✏️ 변수 이름 바꾸기

배경

  • 변수는 프로그래머가 하려는 일에 관해 많은 것을 설명해준다.
  • 특히 사용 범위가 넓은 변수라면 변수 캡슐화를 고려한다.

절차

  1. 이름을 바꿀 변수를 참조하는 곳을 찾아 하나씩 변경한다.

예시

Before

const a = width * height;

const cpyNm = 'Coloso.';

After

const area = width * height;

const companyName = 'Coloso.';

✏️ 매개변수 객체 만들기

배경

  • 데이터 뭉치를 데이터 구조로 묶으면 데이터 사이의 관게가 명확해진다.
  • 또한 함수가 이 데이터 구조를 받게 하면 매개변수 수가 줄어든다.
  • 일관성 또한 높일 수 있다.

절차

  1. 데이터 구조를 생성한다.
  2. 함수 선언 바꾸기로 새 데이터 구조를 매개변수로 추가한다.
  3. 함수 호출 시 새로운 데이터 구조 인스턴스를 넘기도록 수정한다.
  4. 기존 매개변수를 제거하고 테스트한다.

예시

Before

const station = {
    name: 'ZB1',
    readings: [
        {temp: 47, time: "2016-11-10 09:10"},
        {temp: 53, time: "2016-11-10 09:20"},
        {temp: 58, time: "2016-11-10 09:30"},
        {temp: 53, time: "2016-11-10 09:40"},
        {temp: 51, time: "2016-11-10 09:50"}
    ]
};

const operationPlan = {
    temperatureFloor: 50,
    temperatureCeiling: 55
};

const readingsOutsideRange = (station, min, max) => {
    return station.readings.filter(r => r.temp < min || r.temp > max)
};

const alerts = readingsOutsideRange(
                    station, 
                    operationPlan.temperatureFloor, 
                    operationPlan.temperatureCeiling
);

After

class NumberRange {
    constructor(min, max) {
        this._data = {min, max}
    }

    get min() {return this._data.min}
    get max() {return this._data.max}
    
    contains(arg) {return arg >= this.min && arg <= this.max}
};

const range = new NumberRange(50, 55);

const readingsOutsideRange = (station, range) => {
    return station.readings.filter(r => !range.contains(r.temp))
};

const alerts = readingsOutsideRange(station, range);

✏️ 여러 함수를 클래스로 묶기

배경

  • 클래스는 데이터와 함수를 하나의 공유 환경으로 묶은 후, 다른 프로그램 요소와 어우러질 수 있도록 그 중 일부를 외부에 제공한다.
  • 클래스로 함수를 묶으면 함수들이 공유하는 공통 환경을 더 명확하게 표현할 수 있고, 각 함수에 전달되는 인수를 줄여 객체 안에서의 함수 호출을 간결하게 만들 수 있다.

절차

  1. 함수들이 공유하는 공통 데이터를 먼저 캡슐화한다.
  2. 공통 데이터를 사용하는 함수들을 클래스로 옮긴다.

예시

Before

const reading = {customer: "Ivan", quantity: 10, month: 3, year: 2023};

const aReading = acquireReading();
const baseCharge = baseRate(aReading.month, aReading.year) + aReading.quantity;
const taxableCharge = Math.max(0, baseCharge - taxThreshold(aReading.year));

function calculateBaseCharge(aReading) {
  return baseRate(aReading.month, aReading.year) * aReading.quantity;
}

After

class Reading {
  constructor(data) {
    this._customer = data.customer;
    this._quantity = data.quantity;
    this._month = data.month;
    this.year = data.year;
  }
  
  get customer() {return this._customer};
  get quantity() {return this._quantity};
  get month() {return this._month};
  get year() {return this.year};
  get baseCharge() {
    return baseRate(this.month, this.year) * this.quantity;
  }
  get taxableCharge() {
    return Math.max(this.baseCharge - taxThreshold(this.year));
  }
};

const rawReading = acquireReading();
const aReading = new Reading(rawReading);
const basicChargeAmount = aReading.baseCharge;
const taxableCharge = aReading.taxableCharge;

✏️ 여러 함수를 변환 함수로 묶기

배경

  • 여러 도출 로직을 일관된 장소(함수)에서 처리하면 로직 중복을 막고 검색과 갱신을 빠르게 처리할 수 있다.
  • 이러한 역할을 하는 함수를 변환 함수라 하며, 원본 데이터를 입력받아 필요한 정보를 모두 도출한 뒤, 각각 출력 데이터의 필드에 넣어 반환한다.
  • 이렇게 해두면 도출 과정을 검토할 일이 생겼을 때 변환 함수만 살펴보면 된다.

절차

  1. 데이터를 입력받아 값을 그대로 반환하는 변환 함수를 만든다.
  2. 묶을 함수 중 하나를 골라 본문 코드를 변환 함수로 옮기고 처리 결과를 레코드에 새 필드로 기록한다.
  3. 나머지 함수들도 위 과정을 따라 처리한다.

예시

Before

const reading = {customer: "Ivan", quantity: 10, month: 3, year: 2023};

const aReading = acquireReading();
const baseCharge = baseRate(aReading.month, aReading.year) + aReading.quantity;
const taxableCharge = Math.max(0, baseCharge - taxThreshold(aReading.year));

function calculateBaseCharge(aReading) {
  return baseRate(aReading.month, aReading.year) * aReading.quantity;
}

After

function enrichReading(original) {
  const result = _.cloneDeep(original);
  result.baseCharge = calculateBaseCharge(result);
  result.taxableCharge = Math.max(0, result.baseCharge - taxThreshold(result.year));
  return result;
}

const rawReading = acquireReading();
const aReading = enrichReading(rawReading);
const baseChargeAmount = aReading.baseCharge;
const taxablecharge = aReading.taxableCharge;

✏️ 단계 쪼개기

배경

  • 서로 다른 두 대상을 한꺼번에 다루는 코드는 각각 별개 모듈로 나누어 하나에만 집중하도록 한다.
  • 간편한 방법은 하나의 동작을 연이은 두 단계로 쪼개는 것이다.

절차

  1. 분리한 단계에 해당하는 코드를 독립 함수로 추출한다.
  2. 중간 데이터 구조를 만들어 추출한 함수의 매개변수로 추가한다.
  3. 이전 단계에서 사용하는 매개변수가 있다면 중간 데이터 구조로 옮기고 동일한 결과를 반환하도록 나머지 단계도 수정한다.

예시

Before

function priceOrder(product, quantity, shippingMethod) {
  const basePrice = product.basePrice * quantity;
  const discount =
    Math.max(quantity - product.discountThreshold, 0) *
    product.basePrice *
    product.discountRate;
  const shippingPerCase =
    basePrice > shippingMethod.discountThreshold
      ? shippingMethod.discountedFee
      : shippingMethod.feePerCase;
  const shippingCost = quantity * shippingPerCase;
  const price = basePrice - discount + shippingCost;
  return price;
}

After

function calculateDiscount(product, quantity) {
	return Math.max(quantity - product.discountThreshold, 0) * product.basePrice * product.discountRate;
}

function calculateBasePrice(product, quantity) {
	return product.basePrice * quantity;
}

function calculateShippingCost(basePrice, quantity, shippingMethod) {
	const shippingPerCase = basePrice > shippingMethod.discountThreshold ? shippingMethod.discountedFee : shippingMethod.feePerCase;
	return quantity * shippingPerCase;
}

function priceOrder(product, quantity, shippingMethod) {
	const basePrice = calculateBasePrice(product, quantity);
	const discount = calculateDiscount(product, quantity);
	const shippingCost = calculateShippingCost(basePrice, quantity, shippingMethod);
	return basePrice - discount + shippingCost;
}
반응형

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

리팩터링 - 10장  (0) 2024.03.02
리팩터링 - 8, 9장  (0) 2024.03.02
리팩터링 - 2장  (0) 2024.03.02
모던 리액트 Deep Dive - 11장  (0) 2024.03.02
모던 리액트 Deep Dive - 10장  (0) 2024.03.02

댓글