6.1 함수 추출하기
- 코드 조각을 찾아 무슨 일을 하는지 파악한 다음, 독립된 함수로 추출하고 목적에 맞는 이름 붙임
- 코드를 독립된 함수로 묶는 기준은 ‘목적과 구현을 분리’하는 것
- 코드를 보고 무슨 일을 하는지 파악하는 데 한참이 걸린다면 그 부분을 함수로 추출한 뒤 ‘무슨 일’에 걸맞는 이름을 지음
- 함수는 짧게 작성(함수가 짧으면 캐싱하기도 쉽기 때문에 컴파일러가 최적화하는 데 유리할 때가 많음)
- 짧은 함수는 이름 짓기에 특별히 신경 써야 함(별도의 문서 없이 코드 자체만으로 내용을 충분히 설명되게 만들어야 함)
절차
- 함수를 새로 만들고 목적을 잘 드러내는 이름을 붙임 (’어떻게’가 아닌 ‘무엇을’ 하는지가 드러나야 함)
- 추출할 코드를 원본 함수에서 복사하여 새 함수에 붙여넣음
- 추출한 코드 중 원본 함수의 지역 변수를 참조하거나 추출한 함수의 유효범위를 벗어나는 변수는 없는지 검사(있다면 매개변수로 전달함)
- 변수를 다 처리했다면 컴파일
- 원본 함수에서 추출한 코드 부분을 새로 만든 함수를 호출하는 문장으로 바꿈(즉, 추출한 함수로 일을 위임함)
- 테스트
- 다른 코드에 방금 추출한 것과 똑같거나 비슷한 코드가 없는지 확인. 있다면 방금 추출한 새 함수를 호출하도록 바꿀지 검토(인라인 코드를 함수 호출로 변경)
함수를 중첩시키면, 추출한 함수에서 원본 함수에 정의된 모든 변수에 접근할 수 있지만, 중첩 함수를 지원하지 않는 언어에서는 불가능한 방법. 따라서 원본 함수에서만 접근할 수 있는 변수들에 특별히 신경 써야함
예시코드
// before
function printOwing(invoice) {
let outstanding = 0;
console.log('***************');
console.log('**** 고객 채무 ****');
console.log('***************');
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(`채무액: ${customer}`);
console.log(`마감일: ${invoice.dueDate.toLocaleDateString}`);
}
// after
function printOwing(invoice) {
printBanner();
const outstanding = calculateOutstanding(invoice);
recordDueDate(invoice);
printDetails(invoice, outstanding);
}
function calculateOutstanding(invoice) {
let result = 0;
for (const o of invoice.orders) {
result += o.amount;
}
return result;
}
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(`채무액: ${customer}`);
console.log(`마감일: ${invoice.dueDate.toLocaleDateString}`);
}
6.2 함수 인라인하기
- 함수 본문이 이름만큼 명확하거나, 리팩터링 과정에서 잘못 추출된 함수들은 인라인함.(간접 호출을 너무 과하게 쓰는 코드도 흔한 인라인 대상)
절차
- 다형 메서드인지 확인(서브클래스에서 오버라이드하는 메서드는 인라인하면 안 됨)
- 인라인할 함수를 호출하는 곳을 모두 찾음
- 각 호출문을 함수 본문으로 교체함
- 하나씩 교체할 때마다 테스트함
- 함수 정의(원래 함수)를 삭제함
// before
function reportLines(aCustomer) {
const lines = [];
gatherCustomerData(lines, aCustomer);
return lines;
}
function gatherCustomerData(out, aCustomer) {
out.push(['name', aCustomer.name]);
out.push(['location', aCustomer.location]);
}
// after
function reportLines(aCustomer) {
const lines = [];
lines.push(['name', aCustomer.name]);
lines.push(['location', aCustomer.location]);
return lines;
}
핵심은 항상 단계를 잘게 나눠서 처리하는 것
6.3 변수 추출하기
- 지역 변수를 활용하면 표현식을 쪼개 관리하기 더 쉽게 만들고, 복잡한 로직을 구성하는 단계마다 이름을 붙일 수 있어서 코드의 목적을 훨씬 명확하게 드러낼 수 있음 또 디버깅에도 도움이 됨
- 변수 추출, 즉 표현식에 이름을 붙이기로 했다면 그 이름이 들어갈 문맥도 살펴야 함
- 함수를 벗어난 넓은 문맥에서까지 의미가 된다면 변수가 아닌 함수로 추출해야 함
절차
- 추출하려는 표현식에 부작용은 없는지 확인
- 불변 변수를 하나 선언하고 이름을 붙인 표현식의 복제본을 대입 함
- 원본 표현식을 새로 만든 변수로 교체함
- 테스트 함
- 표현식을 여러 곳에서 사용한다면 각각을 새로 만든 변수로 교체, 하나 교체할 때마다 테스트 함
예시 코드
// 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, 100)
);
}
// 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(order.quantity * order.itemPrice * 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.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);
}
}
6.4 변수 인라인하기
절차
- 대입문의 우변(표현식)에서 부작용이 생기지는 않는지 확인함
- 변수가 불변으로 선언되지 않았다면 불변으로 만든 후 테스트함
- 이 변수를 가장 처음 사용하는 코드를 찾아서 대입문 우변의 코드로 바꿈
- 테스트
- 변수를 사용하는 부분을 모두 교체할 때까지 이 과정을 반복
- 변수 선언문과 대입문을 지움
- 테스트
예시 코드
// before
let basePrice = anOrder.basePrice;
return basePrice > 1000;
// after
return anOrder.basePrice > 1000;
6.5 함수 선언 바꾸기
- 함수는 프로그램을 작은 부분으로 나누는 주된 수단
- 함수 선언은 각 부분이 서로 맞물리는 방식을 표현하며, 실질적으로 소프트웨어 시스템의 구성 요소를 조립하는 연결부 역할
- 이러한 연결부에서 가장 중요한 것은 함수의 이름
- 함수 구현 코드를 살펴볼 필요 없이 호출문만 보고도 무슨 일을 하는지 파악할 수 있어야 함
- 함수의 매개변수 역시 중요
- 매개변수는 함수를 사용하는 문맥을 설정 함
- 매개변수를 적절히 사용하여 함수의 활용 범위를 넓힐 수 있으며, 다른 모듈과의 결합을 제거할 수도 있음
간단한 절차
- 매개변수를 제거하려거든 먼저 함수 본문에서 제거 대상 매개변수를 참조하는 곳은 없는지 확인 함
- 메서드 선언을 원하는 형태로 바꿈
- 기존 메서드 선언을 참조하는 부분을 모두 찾아서 바뀐 형태로 수정함
- 테스트한다.
마이그레이션 절차
- 이어지는 추출 단계를 수월하게 만들어야 한다면 함수의 본문을 적절히 리팩터링 함
- 함수 본문을 새로운 함수로 추출 함
- 추출한 함수에 매개변수를 추가해야 한다면 ‘간단한 절차’를 따라 추가 함
- 테스트한다.
- 기존 함수를 인라인
- 이름을 임시로 붙여뒀다면 함수 선언 바꾸기를 한 번 더 적용해서 원래 이름으로 되돌림
- 테스트
예시 코드
// before
function inNewEngland(aCustomer) {
return ['MA', 'CT', 'ME', 'VT', 'NH', 'RI'].includes(aCustomer.address.state);
}
const newEnglanders = someCustomers.filter((c) => inNewEngland(c));
// after
function inNewEngland(stateCode) {
return ['MA', 'CT', 'ME', 'VT', 'NH', 'RI'].includes(stateCode);
}
const newEnglanders = someCustomers.filter((c) => inNewEngland(c.address.satate));
6.6 변수 선언하기
- 데이터는 참조하는 모든 부분을 한 번에 바꿔야 코드가 제대로 작동하기 때문에 함수보다 다루기가 까다로움
- 접근할 수 있는 범위가 넓은 데이터를 옮길 때는 먼저 그 데이터로의 접근을 독점하려는 함수를 만드는 식으로 캡슐화하는 것이 좋음
- 데이터 캡슐화는 데이터 변경 전 검증이나 변경 후 추가 로직을 쉽게 끼워넣을 수 있다는 장점도 있음
- 데이터의 캡슐화를 위해 객체 지향에서 객체의 데이터는 항상 private으로 유지해야 함
절차
- 변수로의 접근과 갱신을 전담하는 캡슐화 함수들을 만듦
- 정적 검사를 수행
- 변수를 직접 참조하던 부분을 모두 적절한 캡슐화 함수 호출로 바꿈. 하나씩 바꿀 때마다 테스트 진행
- 변수의 접근 범위를 제한
- 테스트
- 변수 값이 레코드라면 레코드 캡슐화하기를 적용할지 고려해봄
예시 코드
// before
// 전역 변수에 중요한 데이터가 담겨 있는 경우
let defaultOwner = { firstName: '마틴', lastName: '파울러' };
// 데이터를 참조하는 코드
spaceship.owner = defaultOwner;
// 데이터를 갱신하는 코드
defaultOwner = { firstName: '레베카', lastName: '파슨스' };
// after
// defaultOwner.js
let defaultOwner = { firstName: '마틴', lastName: '파울러' };
export function getDefaultOwner() {
return defaultOwner;
}
export function setDefaultOwner(arg) {
defaultOwner = arg;
}
값 캡슐화하기
- 변수뿐 아니라 변수에 담긴 내용을 변경하는 행위까지 제어할 수 있게 캡슐화
- 게터가 데이터의 복제본을 반환하도록 수정
export function getDefaultOwner() {
return Object.assign({}, defaultOwner);
}
- 레코드 캡슐화를 통해 아예 변경할 수 없게 만드는 방법도 있음
6.7 변수 이름 바꾸기
- 명확한 프로그래밍의 핵심은 이름짓기
- 간단한 변수의 경우 대체로 파악이 쉽지만, 함수 호출 한 번으로 끝나지 않고 값이 영속되는 필드라면 신중하게 이름을 지어야 함
절차
- 폭넓게 쓰이는 변수라면 변수 캡슐화하기를 고려
- 이름을 바꿀 변수를 참조하는 곳을 모두 찾아서, 하나씩 변경
- 테스트
예시
// before
let tpHd = 'untitled';
// 변수를 읽기만 하는 경우
result += `<h1>${tpHd}</h1>`;
// 값을 수정하는 경우
tpHd = obj['articleTitle'];
// after: getter와 setter를 통해 변수 캡슐화하기
result += `<h1>${title()}</h1>`;
setTitle(obj['articleTitle']);
let _title = 'untitled';
function title() {
return _title;
}
function setTitle(arg) {
_title = arg;
}
6.8 매개변수 객체 만들기
- 데이터 뭉치를 데이터 구조로 묶으면 데이터 사이의 관계가 명확해짐, 나아가 함수가 이 데이터 구조를 받게 하면 매개변수 수가 줄어듬
절차
- 적당한 데이터 구조가 아직 마련되어 있지 않다면 새로 만듦
- 테스트
- 함수 선언 바꾸기로 새 데이터 구조를 매개변수로 추가
- 테스트
- 함수 호출 시 새로운 데이터 구조 인스턴스를 넘기도록 수정, 하나씩 수정할 때마다 테스트 진행
- 기존 매개변수를 사용하던 코드를 새 데이터 구조의 원소를 사용하도록 바꿈
- 다 바꿨다면 기존 매개변수를 제거하고 테스트 함
예시
// before
// 데이터
const station = {
name: 'ZB1',
readings: [
{ temp: 47, time: '2016-11-19 09:10' },
{ temp: 53, time: '2016-11-19 09:20' },
{ temp: 58, time: '2016-11-19 09:30' },
{ temp: 53, time: '2016-11-19 09:40' },
{ temp: 51, time: '2016-11-19 09:50' },
],
};
// 함수
function readingsOutsideRange(station, min, max) {
return station.readings.filter((r) => r.temp < min || r.temp > max);
}
// 호출문
alerts = readingsOutsideRange(
station,
operatingPlan.temperatureFloor,
operatingPlan.temperatureCeiling,
);
// after
class NumberRange {
constructor(min, max) {
this._data = { min: min, max: max };
}
get min() {
return this._data.min;
}
get max() {
return this._data.max;
}
}
function readingsOutsideRange(station, range) {
return station.readings.filter((r) => r.temp < range.min || r.temp > range.max);
}
const range = new NumberRange(operatingPlan.temperatureFloor, operatingPlan.temperatureCeiling);
alerts = readingsOutsideRange(station, range);
6.9 여러 함수를 클래스로 묶기
- 클래스는 데이터와 함수를 하나의 공유 환경으로 묶은 후, 다른 프로그램 요소와 어우러질 수 있도록 그중 일부를 외부에 제공
- 공통 데이터를 중심으로 긴밀하게 엮여 작동하는 함수 무리는 클래스 하나로 묶을 수 있음
- 여러 함수를 클래스로 묶으면 클라이언트가 객체의 핵심 데이터를 변경할 수 있고, 파생 객체들을 일관되게 관리할 수 있음
절차
- 함수들이 공유하는 공통 데이터 레코드를 캡슐화 함 2.공통 레코드를 사용하는 함수 각각을 새 클래스로 옮김
- 데이터를 조작하는 로직들은 함수로 추출해서 새 클래스로 옮김
예시
// before
// 클라이언트 1
const aReading = acquireReading();
const baseCharge = baseRate(aReading.month, aReading.year) * aReading.quantity;
// 클라이언트 2
const aReading = acquireReading();
const baseCharge = baseRate(aReading.month, aReading.year) * aReading.quantity;
const taxableCharge = Math.max(0, base - taxThreshold(aReading.year));
// 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(0, this.baseCharge - taxThreshold(this.year));
}
}
// 클라이언트 3
const rawReading = acquireReading();
const aReading = new Reading(rawReading);
const basicChargeAmount = aReading.baseCharge;
const taxableCharge = aReading.taxableCharge;
6.10 여러 함수를 변환 함수로 묶기
- 데이터를 입력받아서 여러 가지 정보를 도출하는 작업들을 한데로 모아두면 검색과 갱신을 일관된 장소에서 할 수 있고 로직 중복도 막을 수 있음
- 변환 함수를 사용하면 원본 데이터를 입력받아서 필요한 정보를 모두 도출한 뒤, 각각을 출력 데이터의 필드에 넣어 반환 함
💡 원본 데이터가 코드 안에서 갱신될 때는 클래스로 묶는 것이 좋음
절차
- 변환할 레코드를 입력받아서 값을 그대로 반환하는 변환 함수를 만듦
- 묶을 함수 중 함수 하나를 골라서 본문 코드를 변환 함수로 옮기고, 처리 결과를 레코드에 새 필드로 기록 그런 다음 클라이언트 코드가 이 필드를 사용하도록 수정 함
- 테스트
- 나머지 관련 함수도 위 과정에 따라 처리함
예시
// before
// 클라이언트 1
const aReading = acquireReading();
const baseCharge = baseRate(aReading.month, aReading.year) * aReading.quantity;
// 클라이언트 2
const aReading = acquireReading();
const base = baseRate(aReading.month, aReading.year) * aReading.quantity;
const taxableCharge = Math.max(0, base - taxThreshold(aReading.year));
// 클라이언트 3
const aReading = acquireReading();
const basicChargeAmount = calculateBaseCharge(aReading);
function calculateBaseCharge(aReading) {
return baseRate(aReading.month, aReading.year) * aReading.quantity;
}
// after
// 클라이언트 1, 3
const rawReading = acquireReading(); // 미가공 측정값
const aReading = enrichReading(rawReading);
const basicChargeAmount = aReading.baseCharge;
// 클라이언트 2
const rawReading = acquireReading(); // 미가공 측정값
const aReading = enrichReading(rawReading);
const taxableCharge = aReading.taxableCharge;
function enrichReading(original) {
const result = _.cloneDeep(original);
result.baseCharge = calculateBaseCharge(aReading);
result.taxableCharge = Math.max(0, result.baseCharge - taxThreshold(result.year));
return result;
}
enrichReading()
처럼 정보를 추가해 반환할 때 원본 측정값 레코드는 변경하지 않아야 함
6.11 단계 쪼개기
- 서로 다른 두 대상을 한꺼번에 다루는 코드를 발견하면 각각을 별개 모듈로 나눌 수 있음
- 이렇게 분리하는 가장 간편한 방법 하나는 동작을 연이은 두 단계로 쪼개는 것
- 가장 대표적인 예는 컴파일러
- 컴파일 작업은 여러 단계가 순차적으로 연결된 형태로 분리되어 있음
- 각 단계는 자신만의 문제에 집중하기 때문에 나머지 단계에 관해서는 자세히 몰라도 이해할 수 있음
절차
- 두 번째 단계에 해당하는 코드를 독립 함수로 추출함
- 테스트
- 중간 데이터 구조를 만들어서 앞에서 추출한 함수의 인수로 추가함
- 테스트
- 추출한 두 번째 단계 함수의 매개변수를 하나씩 검토. 그중 첫 번째 단계에서 사용되는 것은 중간 데이터 구조로 옮김 하나씩 옮길 때마다 테스트함
- 첫 번째 단계 코드를 함수로 추출하면서 중간 데이터 구조를 반환하도록 만듦
예시
// 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 priceOrder(product, quantity, shippingMethod) {
const priceData = calculatePricingData(product, quantity);
return applyShipping(priceData, shippingMethod);
}
function calculatePricingData(product, quantity) {
const basePrice = product.basePrice * quantity;
const discount =
Math.max(quantity - product.discountThreshold, 0) * product.basePrice * product.discountRate;
return { basePrice, quantity, 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;
}
- 험블 객체 패턴(Humble Object Pattern) : 명령줄 호출과 표준 출력에 쓰는 느리고 불편한 작업과 자주 테스트해야 할 복잡한 동작을 분리함으로써 테스트를 더 쉽게 수행하게 만듦