읽을거리/리팩터링

06 기본적인 리팩터링 - 변수 추출하기

씨씨상 2024. 11. 5. 18:49

 

 

변수 추출하기

 

 

 

 

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

 

 

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

 

 

배경

표현식이 너무 복잡해서 이해하기 어려울 때가 있다. 이럴 때 지역 변수를 활용하면 표현식을 쪼개 관리하기 더 쉽게 만들 수 있다. 그러면 복잡한 로직을 구성하는 단계마다 이름을 붙일 수 있어서 코드의 목적을 훨씬 명확하게 드러낼 수 있다.

 

이 과정에서 추가한 변수는 디버깅에도 도움된다. 디버거에 중단점breakpoint을 지정하거나 상태를 출력하는 문장을 추가할 수 있기 때문이다.

 

변수 추출을 고려한다고 함은 표현식에 이름을 붙이고 싶다는 뜻이다. 이름을 붙이기로 했다면 그 이름이 들어갈 문맥도 살펴야 한다. 현재 함수 안에서만 의미가 있다면 변수로 추출하는 것이 좋다. 그러나 함수를 벗어난 넓은 문맥에서까지 의미가 된다면 그 넓은 범위에서 동용되는 이름을 생각해야 한다. 다시 말해 변수가 아닌 (주로) 함수로 추출해야 한다. 이름이 통용되는 문맥을 넓히면 다른 코드에서 사용할 수 있기 때문에 같은 표현식을 중복해서 작성하지 않아도 된다. 그래서 중복이 적으면서 의도가 잘 드러나는 코드를 작성할 수 있다.

 

이름이 통용되는 문맥을 넓힐 때 생기는 단점은 할 일이 늘어난다는 것이다. 많이 늘어날 것 같다면 임시 변수를 함수로 바꾸기를 적용할 수 있을 때까지 일단 놔둔다. 간단히 처리할 수 있다면 즉시 넓혀서 다른 코드에서도 사용할 수 있게 한다. 가령 클래스 안의 코드를 다룰 때는 함수 추출하기를 아주 쉽게 적용할 수 있다.

 

 

절차

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

 

 

예시

간단한 계산식에서 시작해보자.

 

function price(order) {
  // 가격(price) = 기본 가격 - 수량 할인 + 배송비
  return order.quantity * order.itemPrice -
    Math.max(0, order.quantity - 500) * order.itemPrice * 0.05 +
    Math.min(order.quantity * order.itemPrice * 0.1, 100);
}

 

간단한 코드지만 더 쉽게 만들 수 있다. 먼저 기본 가격은 상품 가격(itemPrice)에 수량(quantity)를 곱한 값임을 파악해내야 한다.

 

이 로직을 이해했다면 기본 가격을 담을 변수를 만들고 적절한 이름을 지어준다.

 

function price(order) {
  // 가격(price) = 기본 가격 - 수량 할인 + 배송비
  const basePrice = order.quantity * order.itemPrice;
  return order.quantity * order.itemPrice -
    Math.max(0, order.quantity - 500) * order.itemPrice * 0.05 +
    Math.min(order.quantity * order.itemPrice * 0.1, 100);
}

 

물론 이렇게 변수 하나를 선언하고 초기화한다고 해서 달라지는 건 없다. 이 변수를 실제로 사용해야 하므로 원래 표현식에서 새로 만든 변수에 해당하는 부분을 교체한다.

 

function price(order) {
  // 가격(price) = 기본 가격 - 수량 할인 + 배송비
  const basePrice = order.quantity * order.itemPrice;
  return basePrice -
    Math.max(0, order.quantity - 500) * order.itemPrice * 0.05 +
    Math.min(order.quantity * order.itemPrice * 0.1, 100);
}

 

방금 교체한 표현식이 쓰이는 부분이 더 있다면 마찬가지로 새 변수를 사용하도록 수정한다.

 

function price(order) {
  // 가격(price) = 기본 가격 - 수량 할인 + 배송비
  const basePrice = order.quantity * order.itemPrice;
  return basePrice -
    Math.max(0, order.quantity - 500) * order.itemPrice * 0.05 +
    Math.min(basePrice * 0.1, 100);
}

 

그 다음 줄은 수량 할인이다. 수량 할인도 다음과 같이 추출하고 교체한다.

 

function price(order) {
  // 가격(price) = 기본 가격 - 수량 할인 + 배송비
  const basePrice = order.quantity * order.itemPrice;
  const quantityDiscount = Math.max(0, order.quantity - 500) * order.itemPrice * 0.05;
  return basePrice - quantityDiscount + Math.min(basePrice * 0.1, 100);
}

 

마지막으로 배송비도 똑같이 처리한다. 다 수정했다면 주석은 지워도 된다. 주석에서 한 말이 코드에 그대로 드러나기 때문이다.

 

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

 

 

예시: 클래스 안에서

똑같은 코드를 클래스 문맥 안에서 처리하는 방법을 살펴보자.

 

class Order {
  constructor(aRecord) {
    this._data = aRecord;
  }

  get quantity() {
    return this._data.quantity;
  }
  get itemPrice() {
    return this._data.itemPrice;
  }

  get price() {
    return this.quantity * this.itemPrice -
      Math.max(0, this.quantity - 500) * this.itemPrice * 0.05 +
      Math.min(this.quantity * this.itemPrice * 0.1, 100);
  }
}

 

이번에도 추출하려는 이름은 같다. 하지만 그 이름이 가격을 계산하는 price() 메서드의 범위를 넘어, 주문을 표현하는 Order 클래스 전체에 적용된다. 이처럼 클래스 전체에 영향을 줄 때는 나는 변수가 아닌 메서드로 추출하는 편이다.

 

class Order {
  constructor(aRecord) {
    this._data = aRecord;
  }

  get quantity() {
    return this._data.quantity;
  }
  get itemPrice() {
    return this._data.itemPrice;
  }

  get price() {
    return this.basePrice - this.quantityDiscount + this.shipping;
  }
  get basePrice() {
    return this.quantity * this.itemPrice;
  }
  get quantityDiscount() {
    return Math.max(0, this.quantity - 500) * this.itemPrice * 0.05;
  }
  get shipping() {
    return Math.min(this.basePrice * 0.1, 100);
  }
}

 

여기서 객체의 엄청난 장점을 볼 수 있다. 객체는 특정 로직과 데이터를 외부와 공유하려 할 때 공유할 정보를 설명해주는 적당한 크기의 문맥이 되어준다. 이 예처럼 간단한 경우라면 효과가 크지 않지만, 덩치가 큰 클래스에서 공통 동작을 별도 이름으로 뽑아내서 추상화해두면 그 객체를 다룰 때 쉽게 활용할 수 있어서 매우 유용하다.

 

 

 

 

 

[출처]

리팩터링 2판 - 마틴 파울러

 

[정리]

변수를 추출하는 방법을 설명한다. 함수 안에서 읽기 어려운 코드가 있을 경우 변수로 추출하며, 클래스 안에서 변수로 추출할 때는 전체 값에 영향을 주는지 살펴보고 메서드로 추출한다.