Chapter 05. 리팩터링 카탈로그 보는 법
❗️ 리팩터링 설명 형식
이름
리팩토링 용어 지칭
개요
리팩터링의 핵심 개념을 간략히 표현
개념도 + 코드 예시
배경
해당 기법이 왜 필요한지, 또는 적용하면 안되는 상황이 어떤건지에 대해 설명
절차
리팩터링 과정을 단계별로 제시
예시
리팩터링 실제 적용 예시 & 효과
Chapter 06. 기본적인 리팩터링
✏️ 함수 추출하기
배경
- 코드 조각을 찾아 무슨 일을 하는지 파악한 다음, 독립된 함수로 추출하고 목적에 맞는 이름을 붙인다.
- 이렇게 했을 때 함수 이름만으로 목적을 빠르게 파악할 수 있고, 본문 코드에 크게 신경쓰지 않아도 되기 때문이다.
- 이 방식으로 여러 짧은 함수가 만들어져 호출량의 증가로 성능 저하가 있다고 생각할 수 있지만, 함수가 짧으면 캐싱하기 더 쉽기 때문에 컴파일러가 최적화하는데 유리하다는 장점이 있다.
절차
- 함수의 목적을 드러내는 이름을 붙인다. ('어떻게' 가 아닌 ‘무엇을’ 하는지 드러나도록)
- 이름이 떠오르지 않는다면 함수로 추출하면 안 된다는 신호다.
- 추출할 코드를 새 함수에 복사한다.
- 참조하는 지역변수는 매개변수로 전달
- 새 함수에서만 사용되는 변수는 지역변수로
- 지역변수의 값을 변경할 경우 새 함수의 결과로 전달
- 새로 만든 함수를 호출하는 문으로 수정한다.
예시
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()}`);
}
✏️ 함수 인라인하기
배경
- 함수 본문이 이름만큼 명확한 경우, 또는 함수 본문 코드를 이름만큼 깔끔하게 리팩터링할 수 있는 경우
- 쓸데없는 간접 호출은 오히려 불필요함을 야기할 수 있다.
절차
- 다형 메서드인지 체크
- 서브클래스에서 오버라이딩된 메서드는 인라인 금지
- 모든 호출문을 (점진적으로) 인라인으로 교체
예시
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)으로 도움이 될 수 있다.
- 문맥을 고려해 현재 선언된 함수보다 더 넓은 문맥에서까지 쓰이는 역할과 이름을 갖게 된다면 함수로 추출하는 것을 권장
절차
- 추출하려는 표현식에 부작용(side effect)이 없는지 확인한다.
- 상수를 선언하고 표현식을 대입한다.
- 원본 표현식을 새로 만든 상수로 교체한다.
예시
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;
✏️ 변수 인라인하기
배경
- 변수명이 원래 표현식과 다를 바 없을 때
- 오히려 변수가 주변 코드를 리팩터링하는 데 방해가 되기도 한다.
절차
- 인라인할 표현식에 부작용(side effect)이 없는지 확인한다.
- 상수인지 확인하고, 아니라면 상수로 변경 후 테스트한다.
- 테스트 과정에서 변수에 값이 단 한번만 대입되는지 확인한다.
- 변수를 표현식으로 교체한다.
예시
Before
let basePrice = anOrder.basePrice;
return (basePrice > 1000);
After
return anOrder.basePrice > 1000;
✏️ 함수 선언 바꾸기
다른 이름
- 함수 이름 바꾸기
- 시그니처 바꾸기
배경
- 함수는 소프트웨어 시스템의 구성 요소를 조립하는 연결부 역할을 한다.
- 연결부를 잘 정의하면 시스템에 새로운 부분을 추가하기 쉬워지는 반면, 잘못 정의하면 지속적인 방해 요인으로 작용해 소프트웨어 동작을 파악하기 어려워지고 요구사항에 대한 유연성이 떨어지게 된다.
- 이러한 연결부에서 가장 중요한 요소는 함수의 이름이다.
함수의 이름이 떠오르지 않을 때 ‘주석을 이용해 함수의 목적을 설명’ 해보는 방식이 도움이 된다.
절차
마이그레이션 절차의 복잡도에 따라 간단한 절차와 마이그레이션 절차로 구분지어 따름
간단한 절차
- 매개변수 제거 시 참조하는 곳이 있는지 확인한다.
- 메서드 선언을 원하는 형태로 바꾼다.
마이그레이션 절차
- 함수 본문을 새로운 함수로 추출한다.
- 추출한 함수에 매개변수를 추가해야 한다면 간단한 절차를 따라 추가한다.
- 기존 함수를 인라인한다.
- 임시 이름을 붙인 새 함수를 원래 이름으로 수정한다.
예시 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)로 만들어 데이터를 외부로부터 보호하는 것.
배경
- 함수는 대체로 호출하는데 사용되고, 함수의 이름을 바꾸거나 다른 모듈로 옮기는 건 어렵지 않다.
- 반면 데이터는 유효범위가 넓을수록 참조하는 모든 부분을 바꿔야 코드가 제대로 동작하기 때문에 쉽지 않을 수 있다.
- 그래서 접근 범위가 넓은 데이터를 그 데이터로의 접근을 독점하는 함수를 만드는 식으로 캡슐화하는 것이 좋다.
- 데이터 캡슐화는 데이터를 변경하고 사용하는 코드를 감시할 수 있는 확실한 통로가 되어주기 때문에 데이터 변경 전 검증, 변경 후 추가 로직 추가가 용이하다.
- 불변 데이터는 변경될 일이 없기 때문에 캡슐화할 이유가 적다.
절차
- 변수로의 접근과 갱신을 전담하는 캡슐화 함수를 만든다.
- 변수를 직접 참조하던 부분을 모두 적절한 캡슐화 함수 호출로 수정한다.
- 변수의 접근 번위를 제한한다.
예시
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: "파슨스"});
✏️ 변수 이름 바꾸기
배경
- 변수는 프로그래머가 하려는 일에 관해 많은 것을 설명해준다.
- 특히 사용 범위가 넓은 변수라면 변수 캡슐화를 고려한다.
절차
- 이름을 바꿀 변수를 참조하는 곳을 찾아 하나씩 변경한다.
예시
Before
const a = width * height;
const cpyNm = 'Coloso.';
After
const area = width * height;
const companyName = 'Coloso.';
✏️ 매개변수 객체 만들기
배경
- 데이터 뭉치를 데이터 구조로 묶으면 데이터 사이의 관게가 명확해진다.
- 또한 함수가 이 데이터 구조를 받게 하면 매개변수 수가 줄어든다.
- 일관성 또한 높일 수 있다.
절차
- 데이터 구조를 생성한다.
- 함수 선언 바꾸기로 새 데이터 구조를 매개변수로 추가한다.
- 함수 호출 시 새로운 데이터 구조 인스턴스를 넘기도록 수정한다.
- 기존 매개변수를 제거하고 테스트한다.
예시
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);
✏️ 여러 함수를 클래스로 묶기
배경
- 클래스는 데이터와 함수를 하나의 공유 환경으로 묶은 후, 다른 프로그램 요소와 어우러질 수 있도록 그 중 일부를 외부에 제공한다.
- 클래스로 함수를 묶으면 함수들이 공유하는 공통 환경을 더 명확하게 표현할 수 있고, 각 함수에 전달되는 인수를 줄여 객체 안에서의 함수 호출을 간결하게 만들 수 있다.
절차
- 함수들이 공유하는 공통 데이터를 먼저 캡슐화한다.
- 공통 데이터를 사용하는 함수들을 클래스로 옮긴다.
예시
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;
✏️ 여러 함수를 변환 함수로 묶기
배경
- 여러 도출 로직을 일관된 장소(함수)에서 처리하면 로직 중복을 막고 검색과 갱신을 빠르게 처리할 수 있다.
- 이러한 역할을 하는 함수를 변환 함수라 하며, 원본 데이터를 입력받아 필요한 정보를 모두 도출한 뒤, 각각 출력 데이터의 필드에 넣어 반환한다.
- 이렇게 해두면 도출 과정을 검토할 일이 생겼을 때 변환 함수만 살펴보면 된다.
절차
- 데이터를 입력받아 값을 그대로 반환하는 변환 함수를 만든다.
- 묶을 함수 중 하나를 골라 본문 코드를 변환 함수로 옮기고 처리 결과를 레코드에 새 필드로 기록한다.
- 나머지 함수들도 위 과정을 따라 처리한다.
예시
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;
✏️ 단계 쪼개기
배경
- 서로 다른 두 대상을 한꺼번에 다루는 코드는 각각 별개 모듈로 나누어 하나에만 집중하도록 한다.
- 간편한 방법은 하나의 동작을 연이은 두 단계로 쪼개는 것이다.
절차
- 분리한 단계에 해당하는 코드를 독립 함수로 추출한다.
- 중간 데이터 구조를 만들어 추출한 함수의 매개변수로 추가한다.
- 이전 단계에서 사용하는 매개변수가 있다면 중간 데이터 구조로 옮기고 동일한 결과를 반환하도록 나머지 단계도 수정한다.
예시
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 |
댓글