기본적인 리팩토링
1. 기본적인 리팩토링
이번 시간부터 기본적이고 자주 사용하는 리팩토링에 대한 학습을 본격적으로 시작하였다. 가장 기본적인 리팩토링의 주요 핵심 개념은 추출, 인라인, 그룹화, 그리고 네이밍이다. 추출을 통해 동일한 로직을 반복해서 사용하는 문제를 정리하고 재사용성을 높이고 인라인을 통해 짧은 로직들을 정리한다. 또한 객체나 클래스를 통한 그룹화로 동일한 로직을 한 곳에 모아 데이터 관리와 코드 구조 파악을 용이하게 만든다. 마지막으로 네이밍을 통해 로직에 이름을 붙여 코드 파악을 원활하게 수행할 수 있도록 진행한다. 네이밍은 이름뿐만 아니라 매개변수를 변경하는 것과 같이 넓은 의미로 해석한다.
2. 추출
2-1. 함수 추출하기
가장 기본적으로 많이 사용하는 리팩토링 기법이다. 주로 코드에 과도한 주석을 작성해야 이해하는 로직이 있다면 함수 추출하기를 적용하는 것을 고려해본다. 함수를 추출할 때에는 함수의 유효범위를 신경써서 추출해야한다. 함수의 유효범위를 벗어나는 데이터에 접근해야한다면 매개변수를 적극 활용하자.
만약 중첩 함수를 지원하는 언어라면 함수 추출하기를 활용할 때 먼저 중첩 함수로 빼서 동작을 확인한 다음 외부로 빼놓는 방식을 추천한다. 중첩 함수를 활용하면 함수의 유효범위를 파악하기 용이하기 때문이다. 중첩 함수를 지원하지 않는 언어라면 추출한 부모 함수와 동일한 레벨에 위치시켜야한다.
✅ 함수를 추출하면 함수가 과도하게 생성되어 성능이 저하될 수 있다는 우려를 할 수 있지만 함수가 짧으면 캐싱하기가 더 쉬워 컴파일러가 최적화하는데 더 유리할 수도 있다!
예시
// Before
function printOwing(invoice){
let outstanding = calculateOutstanding();
console.log(`고객명: ${invoice.customer}`);
console.log(`채무액: ${outstanding}`);
}
// After
function printOwing(invoice){
let outstanding = calculateOutstanding();
printInfo(invoice);
function printInfo(invoice){
console.log(`고객명: ${invoice.customer}`);
console.log(`채무액: ${outstanding}`); // 이 함수를 외부로 빼게 되면 outstanding도 매개변수로 받아야한다.
}
}
만약 매개변수로 넘긴 데이터를 수정해야한다면 return으로 반환해서 사용한다.
2-2. 변수 추출하기
표현식이 너무 복잡해서 이해하기 어렵다면 임시 변수를 생성하여 표현식을 추출한다. 변수 추출은 표현식에 이름을 붙이는 네이밍 기법으로 생각할 수 있다. 특정 로직을 변수로 추출하게 되면 표현식을 한 눈에 파악할 수 있는 것 이외에 디버깅에도 큰 도움이 된다.
변수 추출에는 우선
const
변수를 사용해서 혹시 모를 변경을 막고, 이후에 변경을 해야하는 경우let
변수로 변경한다.
예시
// Before
return order.quantity * order.itemPrice - Math.max(0, order.quantity - 500) * 0.05 + Math.min(order.quantity * order.itemPrice * 0.1, 100);
// After
const basePrice = order.quantity * order.itemPrice;
const discount = Math.max(0, order.quantity - 500) * 0.05;
const shipping = Math.min(order.quantity * order.itemPrice * 0.1, 100);
return basePrice - discount + shipping;
3. 인라인
3-1. 함수 인라인하기
함수 인라인은 단순하고 지엽적인 로직이나 간접 호출을 과하게 사용하는 코드를 대상으로 수행한다. 다른 함수를 위임하기만 하는 함수가 너무 많아 위임 관계가 복잡하게 얽혀있다면 인라인을 수행한다.
⚠️ 단, 서브클래스에서 활용해야하는 함수는 인라인해서는 안된다.
예시
// Before
function getRating(driver){
return moreThanFiveDriver(driver) ? 2 : 1;
}
function moreThanFiveDriver(driver){
return driver.numberOfLateDeliveries > 5;
}
// After
function getRating(driver){
return (driver.numberOfLateDeliveries > 5) ? 2 : 1;
}
3-2. 변수 인라인하기
변수명이 원래 표현식과 크게 다르지 않다면 변수를 인라인하여 사용하는 것을 고려한다.
예시
// Before
let basePrice = order.basePrice;
return (basePrice > 1000);
// After
return (order.basePrice > 1000);
4. 그룹화
4-1. 변수 캡슐화하기
불변 데이터가 아닌 변수의 유효범위가 넓어질수록 변수의 무결성이 점점 떨어져 문제가 발생할 가능성이 높아진다. 따라서 객체나 클래스를 활용하여 변수의 접근과 수정을 제한하는 캡슐화를 통해 이 문제를 해결할 수 있다. 변수를 캡슐화하게 되면 데이터를 변경하고 사용하는 코드를 감시할 수 있기 때문에 데이터 변경 전 검증이나 변경 후 로직을 손쉽게 추가할 수 있다.
예시
// Before
let defaultOwner = {firstName: "harry", lastName: "potter" }
// After
let defaultOwnerData = {firstName: "harry", lastName: "potter" };
export function getDefaultOwner() {
// return defaultOwnerData; → 이 방법은 원본 데이터에 접근이 가능하기 때문에 Getter의 본질을 훼손할 수 있다.
return Object.assign({}, defaultOwnerData); // 원본 데이터의 복사본을 반환한다.
}
export function setDefaultOwner(owner){
defaultOwnerData = owner;
}
4-2. 매개변수 객체 만들기
매개변수에 동일한 형태로 뭉쳐다니는 데이터 항목들을 묶어 하나의 데이터 구조로 변경한다. 이처럼 데이터 뭉치를 하나의 데이터 구조로 묶게 되면 데이터 간의 관계를 명확히 파악할 수 있다. 또한 매개변수의 개수가 줄어들기 때문에 함수를 파악하기 용이하고 모든 함수가 원소를 참조할 때 동일한 이름으로 참조하기 때문에 일관성이 높아진다.
예시
// Before
function amountInvoiced(startDate, endDate){ }
function amountReceived(startDate, endDate){ }
// After
function amountInvoiced(rangeDate){ }
function amountReceived(rangeDate){ }
4-3. 여러 함수를 클래스로 묶기
공통 데이터를 중심으로 긴밀하게 엮여 동작하는 함수 무리를 발견하면 하나의 클래스로 묶는다. 하나의 클래스로 묶게 되면, 이 함수들이 공유하는 공통 환경을 더 명확하게 표현할 수 있고 각 함수에 전달되는 인수를 줄여 객체 안에서 함수 호출을 간결하게 만들 수 있다.
예시
// Before
function base(aReading){ } // aReading 이라는 공통 데이터 사용
function taxCharge(aReading){ }
function calculateCharge(aReading){ }
// After
class Reading{
base(){ }
taxCharge(){ }
calculateCharge(){ }
}
4-4. 여러 함수를 변환 함수로 묶기
변환 함수 : 변환할 레코드를 입력받아 값을 변경한 후 변환된 레코드를 반환하는 함수
변환 함수 내에서 레코드 정보를 도출하는 로직을 모아 레코드를 변경한다. 만약 도출 과정을 살펴볼 일이 생기면 변환 함수만 살펴보면 된다. 위 과정은 변환 함수말고 클래스로 로직을 묶어도(4-3) 동일한 효과를 볼 수 있다.
✅ 원본 데이터가 코드 안에서 갱신될 때는 클래스로 묶는 방법이 더 좋다.
예시
// Before
function base(aReading){ }
function taxCharge(aReading){ }
// After
function enrichReading(argReading){
const aReading = _.cloneDeep(argReading); // 변환 함수 내에서는 원본 데이터를 복사하여 활용한다! (원본 훼손 방지)
aReading.baseCharge = base(aReading);
aReading.taxCharge = taxCharge(aReading);
return aReading
}
5. 네이밍
5-1. 함수 선언 바꾸기
함수 선언 바꾸기는 간단하게는 함수의 이름을 변경하여 해당 로직을 직관적으로 이해할 수 있도록하는 것부터 시작해, 매개변수 추가, 변경, 삭제까지 함수의 틀과 관련된 모든 사항을 포함한다.
함수 선언 바꾸기를 활용할때는 다음의 마이그레이션 절차를 활용한다. 먼저 변경할 새로운 함수를 만들고 기존 로직을 이전한다. 이후 기존 함수에 새로운 함수를 반환하는 형태로 구성하여 테스트를 진행하는 것으로 선언 변경간 발생할 수 있는 누락 및 오류에 대비하는 것을 추천한다.
예시
// Before
function order(productId, amount, orderDate){
// 함수명을 변경하고 매개 변수를 삭제할 예정
}
// After
function order(productId, amount, orderDate){
return orderProduct(productId, amount); // 마이그레이션 - 중간 매개로 테스트
}
// 새롭게 변경할 함수
function orderProduct(productId, amount){
}
5-2. 변수 이름 바꾸기
모호한 변수명을 수정해 변수의 역할을 명확히 알 수 있도록 한다.
6. 단계 쪼개기
단계 쪼개기 기법은 두 로직의 결합도를 낮추기 위해 사용한다. 주로 서로 다른 두 대상을 연속적으로 다루는 코드에 대해서 단계 쪼개기 기법을 활용한다. 단계 쪼개기를 잘 활용하기 위해서는 명령 분할과 중간 데이터 구조 설정을 명확히 파악해야한다.
예시
// Before
const orderData = orderString.split(/\s+/);
const productPrice = priceList[orderData[0].split("-")[1]];
// 가격을 계산하는 로직을 이해하려면 orderData에 대한 내용과 productPrice에 대한 내용을 이해해야한다.
const orderPrice = parseInt(orderData[1]) * productPrice;
// After
const orderRecord = parseOrder(order); // 중간 데이터 구조
// 가격 계산 로직은 orderRecord와 priceList를 사용한다는 사실을 알 수 있고,
// price 함수를 통해 orderRecord의 quantity와 priceList의 productID를 활용한다는 사실을 손쉽게 파악할 수 있다.
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];
}