06 기본적인 리팩터링 - 변수 추출하기
변수 추출하기
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을 지정하거나 상태를 출력하는 문장을 추가할 수 있기 때문이다.
변수 추출을 고려한다고 함은 표현식에 이름을 붙이고 싶다는 뜻이다. 이름을 붙이기로 했다면 그 이름이 들어갈 문맥도 살펴야 한다. 현재 함수 안에서만 의미가 있다면 변수로 추출하는 것이 좋다. 그러나 함수를 벗어난 넓은 문맥에서까지 의미가 된다면 그 넓은 범위에서 동용되는 이름을 생각해야 한다. 다시 말해 변수가 아닌 (주로) 함수로 추출해야 한다. 이름이 통용되는 문맥을 넓히면 다른 코드에서 사용할 수 있기 때문에 같은 표현식을 중복해서 작성하지 않아도 된다. 그래서 중복이 적으면서 의도가 잘 드러나는 코드를 작성할 수 있다.
이름이 통용되는 문맥을 넓힐 때 생기는 단점은 할 일이 늘어난다는 것이다. 많이 늘어날 것 같다면 임시 변수를 함수로 바꾸기를 적용할 수 있을 때까지 일단 놔둔다. 간단히 처리할 수 있다면 즉시 넓혀서 다른 코드에서도 사용할 수 있게 한다. 가령 클래스 안의 코드를 다룰 때는 함수 추출하기를 아주 쉽게 적용할 수 있다.
절차
- 추출하려는 표현식에 부작용은 없는지 확인한다.
- 불변 변수를 하나 선언하고 이름을 붙일 표현식의 복제본을 대입한다.
- 원본 표현식을 새로 만든 변수로 교체한다.
- 테스트한다.
- 표현식을 여러 곳에서 사용한다면 각각을 새로 만든 변수로 교체한다. 하나 교체할 때마다 테스트 한다.
예시
간단한 계산식에서 시작해보자.
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판 - 마틴 파울러
[정리]
변수를 추출하는 방법을 설명한다. 함수 안에서 읽기 어려운 코드가 있을 경우 변수로 추출하며, 클래스 안에서 변수로 추출할 때는 전체 값에 영향을 주는지 살펴보고 메서드로 추출한다.