Refactoring
리팩터링 전에 테스트 구축하기
- 모든 테스트를 완전히 자동화하고 그 결과까지 스스로 검사하게 만들자.
- 실패해야 할 상황에서는 반드시 실패하게 만들자.
- 문제가 생길 가능성이 있는 경계 조건을 생각해보고 그 부분을 집중적으로 테스트하자.
- 어차피 모든 버그를 잡아낼 수는 없다고 생각하여 테스트를 작성하지 않는다면 대다수의 버그를 잡을 수 있는 기회를 날리는 셈이다.
- 너무 많은 테스트는 의욕을 떨어지게 만들 수 있다. 따라서 위험한 부분에 집중하자. 처리 과정이 복잡하거나 함수에서 오류가 생길만한 부분을 찾아보자.
- 버그 리포트를 받으면 가장 먼저 그 버그를 드러내는 단위 테스트부터 작성하자.
- 누군가 결함을 심으면 테스트가 발견할 수 있다는 믿음을 기준으로 테스트 코드를 작성하자.
Chapter 6. 기본적인 리팩터링
함수 추출하기
목적과 구현을 분리하는 방식
- 반대 리팩터링 : 함수 인라인하기
개요
Before
function printOwing(invoice) {
printBanner();
let outstanding = calculatorOutstanding();
// 세부 사항 출력
console.log(`고객명: ${invoice.customer}`);
console.log(`채무액: ${outstanding}`);
}
After
function printOwing(invoice) {
printBanner();
let outstanding = calculatorOutstanding();
printDetails(outstanding);
function printDetails(outstanding) {
console.log(`고객명: ${invoice.customer}`);
console.log(`채무액: ${outstanding}`);
}
}
절차
- 함수를 새로 만들고 목적(무엇을 하는지)을 잘 드러내는 이름 붙이기.
- 추출할 코드를 새 함수에 복사
- 참조하는 변수는 매개변수로 전달
- 원본 함수의 지역 변수
- 추출한 함수의 유효범위를 벗어나는 변수
- 추출한 코드 부분을 새로 만든 함수를 호출하는 문으로 수정
- 테스트
- 유사한 코드 확인
함수 인라인하기
함수 본문이 이름만큼 명확한 경우, 함수 코드를 이름만큼 깔끔하게 리팩터링할 경우
- 반대 리팩터링 : 함수 추출하기
개요
Before
function raiting(aDriver) { return moreThanFiveLateDeliveries(aDriver) ? 2 : 1; }
function moreThanFiveLateDeliveries(aDriver) { return aDriver.numberOfLaterDeliveries > 5; }
After
function raiting(aDriver) { return aDriver.numberOfLaterDeliveries > 5 ? 2 : 1; }
절차
- 다형 메서드인지 확인
- 서브클래스에서 오버라이드하는 메서드는 인라인 금지
- 인라인할 함수를 호출하는 곳을 모두 찾기
- 각 호출문을 함수 본문으로 교체
- 하나씩 교체할 때마다 테스트
- 함수 정의 삭제
변수 추출하기
표현식이 복잡하여 이해하기 어려울 경우 지역 변수를 활용하여 표현식을 관리
- 디버거 breakpoint를 지정하거나 상태 출력 문장을 추가하면 디버깅에 도움이 된다.
문맥을 고려하여 현재 선언된 함수보다 더 넓은 문맥에서까지의 의미가 된다면 함수로 추출 권장
- 반대 리팩터링 : 변수 인라인하기
개요
Before
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, 0.05)
);
}
After
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;
}
절차
- 추출할 표현식에 사이드이펙트가 없는지 확인
- 불변 변수를 하나 선언 후 이름을 붙일 표현식의 복제본 대입해보기
- 함수가 클래스 전체에 영향을 줄 떄는 변수가 아닌 메서드로 추출
- 원본 표현식을 새로 만든 변수로 교체
- 테스트
- 표현식을 여러 곳에서 사용할 경우 각각을 새로 만든 변수로 교체
변수 인라인하기
변수명이 원래 표현식과 다를 바 없을 때
- 반대 리팩터링 : 변수 추출하기
개요
Before
((anOrder) => {
let basePrice = anOrder.basePrice;
return basePrice > 1000;
})(anOrder);
After
((anOrder) => {
return anOrder.basePrice > 1000;
})(anOrder);
절차
- 인라인할 표현식에 사이드 이펙트가 없는지 확인
- 상수인지 확인하고 상수로 수정 후 테스트
- 이 경우 변수에 값이 단 한 번만 대입되는지 확인 가능
- 변수를 표현식으로 교체
- 테스트
- 변수를 사용하는 부분 모두 교체 시까지 이 과정 반복
- 변수 선언문과 대입문 지우기
- 테스트
함수 선언 바꾸기
호출문만 보고도 무슨 일을 하는지 파악할 수 있도록 함수 이름을 좋게 만들자.
- 주석을 이용해 함수의 목적을 설명해보면 좋은 이름이 떠오를 것이다.
다른 이름 : 함수 이름 바꾸기, 매개변수 바꾸기
개요
간단한 절차
// Before
function circum(radius) {}
// After
function circumference(radius) {}
마이그레이션 절차
// Before
function circum(radius) {
return 2 * Math.PI * radius;
}
// After
function circum(radius) {
return circumference(radius);
}
function circumference(radius) {
return 2 * Math.PI * radius;
}
절차
함수 이름 바꾸기, 매개변수 바꾸기에 모두 적용
마이그레이션 절차의 복잡도에 따라 간단한 절차와 마이그레이션 절차로 구분지어 따름
간단한 절차
- 함수 본문에 제거 대상 매개변수를 참조하는 곳이 없는지 확인
- 메서드 선언 형태 변경
- 기존 메서드 선언을 참조하는 부분을 바뀐 형태로 수정
- 테스트
마이그레이션 절차
(간단한 절차 적용 문제 발생 시)
- 함수 본문 리팩터링
- 함수/변수 추출 등
- 함수 본문에서 새로운 함수로 추출
- 추출한 함수에 매개변수 추가 시 간단한 절차를 따라 추가
- 어서션을 추가하여 새로 추가한 매개변수를 실제 사용하는지 확인 (javascript)
- 테스트
- 기존 함수 인라인 처리
- 임시 이름을 붙인 새 함수를 원래 이름으로 수정
- 테스트
변수 캡슐화하기
자주 사용하는 가변 데이터에 대한 결합도가 높아지는 막기 위해, 데이터의 유효범위가 넓을수록 캡슐화하자.
- 데이터 변경 전 검증이나 변경 후 추가 로직을 쉽게 끼워넣을 수 있다.
개요
Before
let defaultOwner = { firstName: '마틴', lastName: '파울러'};
After
let defaultOwnerData = { firstName: '마틴', lastName: '파울러' };
export function defaultOwner() { return defaultOwnerData; }
export function setDefaultOwner(arg) { defaultOwnerData = arg; }
절차
- 변수 접근/갱신을 전담하는 Getter/Setter 만들기
- 정적 검사 수행
- 변수를 직접 참조하던 부분을 적절한 캡슐화 함수 호출로 변경 (테스트 병행)
- 변수의 접근 범위 제한
- 테스트
- 변수 값이 레코드일 경우 레코드 캡슐화 적용 고려
캡슐화할 데이터를 사용하는 방식과 어떻게 변경할지에 따라 캡슐화의 구체적인 대상과 방법을 정하자.
let defaultOwnerData = { firstName: '마틴', lastName: '파울러' };
export function defaultOwner() { return new Person(defaultOwnerData); }
export function setDefaultOwner(arg) { defaultOwnerData = arg; }
class Person {
constructor(data) {
this._lastName = data.lastName;
this._firstName = data.firstName;
}
get lastName() { return this._lastName; }
get firstName() { return this._firstName; }
}
변수 이름 바꾸기
변수는 프로그래머가 하려는 일에 관해 많은 것을 설명해준다.
개요
Before
let a = height * width;
After
let area = height * width;
절차
- 폭넓게 쓰이는 변수라면 변수 캡슐화 고려하기
- 변경할 변수를 참조하는 곳을 하나씩 변경하기
- 해당 함수에서만 유효한 변수
- 테스트
매개변수 객체 만들기
데이터 뭉치를 데이터 구조로 묶으면 데이터 사이의 관계가 명확해진다.
- +, 매개변수 수 감소, 원소 참조의 일관성
- 데이터 구조를 클래스로 만들어두면 관련 동작들을 해당 클래스로 옮길 수 있다.
개요
Before
function amountInvoiced(startDate, endDate) {}
function amountReceived(startDate, endDate) {}
function amountOverdue(startDate, endDate) {}
After
function amountInvoiced(aDateRange) {}
function amountReceived(aDateRange) {}
function amountOverdue(aDateRange) {}
절차
- 데이터 구조 만들기
- 함수 선언 바꾸기로 매개변수에 새 데이터 구조 추가
- 함수 호출 시 인스턴스 수정
- 기존 매개변수를 사용하던 코드를 새 데이터 구조를 사용하도록 수정
- 기존 매개변수 제거
- 각 단계별 테스트
여러 함수를 클래스로 묶기
클래스로 묶으면 함수들이 공유하는 공통 환경을 더 명확하게 표현
각 함수에 전달되는 인수를 줄여 객체 안에서의 함수 호출을 간결하게
개요
Before
function base(aReading) {}
function taxableCharge(aReading) {}
function calculateBaseChange(aReading) {}
After
class Reading {
base() {}
taxableCharge() {}
calculateBaseChange() {}
}
절차
- 함수들이 공유하는 공통 데이터 레코드 캡슐화
- 공통 레코드를 사용하는 함수 각각을 새 클래스로 옮기기
- 멤버 변수는 함수 호출문의 인수에서 제거
- 데이터를 조작하는 로직들은 함수로 추출하여 새 클래스로 옮기기
여러 함수를 변환 함수로 묶기
변환 함수는 원본 데이터를 입력받아서 필요 정보를 모두 두출한 후, 각 출력 데이터 필드에 넣어 반환한다.
검색과 갱신을 일관된 장소에서 처리할 수 있고, 로직 중복도 막을 수 있다.
원본 데이터가 코드 안에서 갱신될 때는 클래스로 묶기를 적용하자.
- 데이터 일관성을 지켜야 한다면(원본 데이터 변경을 막기 위한 목적이 있다면) 클래스 묶기를 적용하자.
개요
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;
}
절차
- 입력된 값을 그대로 반환하는 변환 함수 생성
- 깊은 복사 처리
- 묶을 함수의 본문 코드를 변환 함수로 옮기고, 처리 결과를 새 필드로 기록
- 로직이 복잡하면 함수 추출 먼저 적용
- 테스트
- 나머지 관련 함수도 적용
단계 쪼개기
서로 다른 두 대상을 한꺼번에 다루는 경우 각각을 별개의 모듈로 나누자.
동작을 두 단계로 쪼개는 방법
개요
Before
const orderData = orderString.split(/\s+/);
const productPrice = priceList[orderData[0].split('-')[1]];
const orderPrice = parseInt(orderData[1]) * productPrice;
After
const orderRecord = parseOrder(order)
const orderPrice = parseInt(orderData[1]) * productPrice;
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]
}
절차
- 두 번째 단계 처리 코드를 독립 함수로 추출
- 테스트
- 중간 데이터 구조(ex. Class)를 만들어 전 단계에서 추출한 함수의 인수로 추가
- 테스트
- 추출한 두 번째 단계 함수의 매개변수를 검토
- 첫 번째 단계에 사용된 매개변수는 중간 데이터 구조(ex. Class)로 옮기기
- 첫 번째 단계 코드를 함수로 추출하면서 중간 데이터 구조 반환
Example
function priceOrder(product, quantity, shippingMethod) {
const priceData = calculatePricingData(product, quantity);
return applyShipping(priceData, shippingMethod, discount);
}
function calculatePricingData(product, quantity) {
const basePrice = product.basePrice * quantity;
const discount = Math.max(quantity - product.discountThreshold, 0)
* product.basePrice * product.discountRate;
return { basePrice: basePrice, quantity: quantity, discount: discount };
}
function applyShipping(priceData, shippingMethod) {
const shippingPerCase = (priceData.basePrice > shippingMethod.discountThreshold) ?
shippingMethod.discountedFee : shippingMethod.feePerCase;
const shippingCost = priceData.quantity * shippingPerCase;
return priceData.basePrice - priceData.discount + shippingCost;
}