6. 기본적인 리팩터링
6.1 함수 추출하기
배경
- 코드를 독립된 함수로 묶을때의 기준은 코드의 목적과 구현을 분리하는 방식이 가장 합리적인 기준으로 보인다.
- 코드를 보고 무슨 일을 하는지 파악하는데 시간이 걸린다면 그 부분을 함수로 추출한 뒤 ‘무슨 일'에 걸맞는 이름을 짓는다.
- 이렇게 해두면 나중에 코드를 다시 읽을 때 함수의 목적이 눈에 확 들어오고, 본문 코드에 대해서는 신경 쓸 일이 현저히 줄어들기 때문이다.
- 짧은 함수의 이점은 이름을 잘 지어야만 발휘되므로 이름 짓기에 신경을 써야한다.
절차
- 함수를 새로 만들고 목적을 잘 드러내는 이름을 붙인다(어떻게 X, 무엇을 하는지 O)
- 함수로 뽑아서 목적이 더 잘 드러나는 이름을 붙일 수 있다면 추출한다.
- 추출할 코드를 원본 함수에서 복사하여 새 함수에 붙여넣는다
- 추출한 코드 중 원본 함수의 지역 변수를 참조하거나 추출한 함수의 유효범위를 벗어나는 변수는 없는지 검사한다. 있다면 매개변수로 전달한다.
- 변수를 다 처리했다면 컴파일한다.
- 원본 함수에서 추출한 코드 부분을 새로 만든 함수를 호출하는 문장으로 바꾼다.
- 테스트
- 다른 코드에 방금 추출한 것과 똑같거나 비슷한 코드가 없는지 살핀다. 있다면 방금 추출한 새 함수를 호출하도록 바꿀지 검토한다.
예시: 지역 변수의 값을 변경할 때
function printOwing(invoice) {
let outstanding = 0;
printBanner();
//미해결 채무(outstanding)를 계산한다.
for (const o of invoice.orders) {
outstanding += o.amount;
}
recordDueDate(invoice)
printDetails(invoice, outstanding)
}
- 선언문을 변수가 사용되는 코드 근처로 슬라이드한다.
- function printOwing(invoice) { printBanner(); let outstanding = 0; // 맨 위에 있던 선언문을 여기로 이동 for (const o of invoice.orders) { outstanding += o.amount; } recordDueDate(invoice) printDetails(invoice, outstanding) }
- 추출할 부분을 새로운 함수로 복사한다.
function printOwing(invoice) {
printBanner();
let outstanding = 0;
for (const o of invoice.orders) {
outstanding += o.amount;
}
recordDueDate(invoice)
printDetails(invoice, outstanding)
}
function calculateOutstanding(invoice) {
let outstanding = 0; // 추출할 코드 복사
for (const o of invoice.orders) {
outstanding += o.amount;
}
return outstanding // 수정된 값 반환
}
- 추출한 코드의 원래 자리를 새로 뽑아낸 함수를 호출하는 문장으로 교체한다.
function printOwing(invoice) {
printBanner();
let outstanding = calculateOutstanding(invoice) // 추출한 함수로 교체완료
recordDueDate(invoice)
printDetails(invoice, outstanding)
}
function calculateOutstanding(invoice) {
let outstanding = 0;
for (const o of invoice.orders) {
outstanding += o.amount;
}
return outstanding
}
- 반환 값의 이름을 내 코딩 스타일에 맞게 바꾼다.
function printOwing(invoice) {
printBanner();
let 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
}
값을 반환할 변수가 여러 개라면?
- 함수가 값 하나만 반환하는 방식을 선호하기 때문에 각각을 반환하는 함수 여러 개로 만든다.
6.2 함수 인라인하기
배경
- 때로는 함수 본문이 이름만큼 명확한 경우도 있다.
- 함수 본문 코드를 이름만큼 깔끔하게 리팩터링할 때도 있다.
- 이럴때는 그 함수를 제거한다. 쓸데없는 간접 호출은 거슬릴 뿐이다.
- 간접 호출을 과하게 쓰는 코드도 흔한 인라인 대상이다.
절차
- 다형 메서드인지 확인한다.
- 서브클래스에서 오버라이드하는 메서드는 인라인하면 안된다.
- 인라인할 함수를 호출하는 곳을 모두 찾는다.
- 각 호출문을 함수 본문으로 교체한다.
- 하나씩 교체할 때마다 테스트한다.
- 인라인하기가 까다로운 부분이 있다면 일단 남겨두고 여유가 생길 때마다 틈틈이 처리한다
- 원본 함수를 삭제한다.
예시
function rating(aDriver) {
return moreThanFiveLateDeliveries(aDriver) ? 2 : 1;
}
function moreThanFiveLateDeliveries(aDriver) {
return aDriver.numberOfLateDeliveries > 5;
}
- 호출되는 함수의 반환문을 그대로 복사해서 호출하는 함수의 호출문을 덮어쓰면 끝이다.
function rating(aDriver) {
return aDriver.numberOfLateDeliveries > 5 ? 2 : 1;
}
- 여기서 핵심은 항상 단계를 잘게 나눠서 처리하는 데 있다.
- 작업을 크게 묶어서 처리할때 테스트가 실패하면 가장 최근의 정상 코드로 돌아온 다음, 단계를 잘게 나눠서 다시 리팩터링한다.
6.3 변수 추출하기
배경
- 표현식이 너무 복잡해서 이해하기 어려울 때는, 지역 변수를 활용하면 표현식을 쪼개 관리하기 더 쉽게 만들 수 있다.
- 디버깅할때도 이점이 있는데, 디버깅할때 중단점을 지정하거나 상태를 출렭하는 문장을 추가할 수 있기 떄문이다.
절차
- 추출하려는 표현식에 부작용은 없는지 확인한다.
- 불변 변수를 하나 선언하고 이름을 붙일 표현식의 복제본을 대입한다.
- 원본 표현식을 새로 만든 변수로 교체한다.
- 테스트한다.
- 표현식을 여러 곳에서 사용한다면 각각을 새로 만든 변수로 교체한다. 하나 교체할 때마다 테스트 한다.
예시
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)
}
- 기본 가격을 담을 변수를 만들고 적절한 이름을 지어주고, 원래 표현식에서 새로 만든 변수에 해당하는 부분을 교체한다.
function price(order) {
// 가격 = 기본 가격 - 수량 할인 + 배송비
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) {
// 가격 = 기본 가격 - 수량 할인 + 배송비
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) {
// 가격 = 기본 가격 - 수량 할인 + 배송비
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);
}
}
- 클래스 전체에 영향을 줄 떄는 변수가 아닌 메서드로 추출한다.
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.quantity * this.itemPrice * 0.1, 100);
}
}
- 객체는 특정 로직과 데이터를 외부와 공유하려 할 때 공유할 정보를 설명해주는 적당한 크기의 문맥이 되어준다.
- 덩치가 큰 클래스에서 공통 동작을 별도 이름으로 뽑아내서 추상화해두면 그 객체를 다룰 때 쉽게 활용할 수 있어서 매우 유용하다.
6.4 변수 인라인 하기
배경
- 변수의 이름이 원래 표현식과 다를 바 없을때나 변수가 주변 코드를 리팩터링하는 데 방해가 되기도 한다. 이럴 때는 그 변수를 인라인한다.
절차
- 대입문의 우변(표현식)에서 부작용이 생기지는 않는지 확인한다.
- 변수가 불변으로 선언되지 않았다면 불변으로 만든 후 테스트한다.
- 이러헥 하면 변수에 값이 단 한 번만 대입되는지 확인할 수 있다.
- 이 변수를 가장 처음 사용하는 코드를 찾아서 대입문 우변의 코드로 바꾼다.
- 테스트한다.
- 변수를 사용하는 부분을 모두 교체할 때까지 이 과정을 반복한다.
- 변수 선언문과 대입문을 지운다.
- 테스트한다.
예시
let basePrice = anOrder.basePrice;
return (basePrice > 1000)
return anOrder.basePrice > 1000 // 변수 인라인하기
6.5 함수 선언 바꾸기
배경
- 이름이 좋으면 함수의 구현 코드를 살펴볼 필요 없이 호출문만 보고도 모슨 일을 하는지 파악할 수 있다.
- 이름이 잘못된 함수를 발견하면 더 나은 이름이 떠오르는 즉시 바꾸라는 명령으로 받아들인다. 그래야 나중에 그 코드를 다시 볼 때 무슨 일을 하는지 또 고민하지 않게 된다.
- 주석을 이용해 함수의 목적을 설명해보면, 주석이 멋진 이름으로 바뀌어 되돌아올 때가 있다.
- 동작에 필요한 모듈 수가 줄어들수록 무언가를 수정할 때 머리에 담아둬야 하는 내용도 적어진다.
- 어떻게 연결하는 것이 더 나은지 더 잘 이해하게 될 떄마다 그에 맞게 코드를 개선할 수 있도록 함수 선언 바꾸기 리팩터링과 친숙해져야한다.
절차
- 함수 선언 바꾸기는 간단한 절차만으로 충분할 때도 많지만, 더 세분화된 마이그레이션 절차가 훨씬 적합한 경우가 많다.
- 따라서 이 리팩터링을 할 떄는 먼저 변경 사항을 살펴보고 함수 선언과 호출문들을 단번에 고칠 수 있을지 가늠해본다.
- 가능하다면 간단한 절차를 따르고, 가능하지 않다면 마이그레이션 절차를 따른다. (마이그레이션 절차를 적용하면 호출문들을 점진적으로 수정할 수 있다.)
간단한 절차
- 매개변수를 제거하려거든 먼저 함수 본문에서 제거 대상 매개변수를 참조하는 곳은 없는지 확인한다.
- 메서드 선언을 원하는 형태로 바꾼다.
- 기존 메서드 선언을 참조하는 부분을 모두 찾아서 바뀐 형태로 수정한다.
- 테스트한다.
마이그레이션 절차
- 이어지는 추출 단계를 수월하게 만들어야 한다면 함수의 본문을 적절히 리팩터링한다.
- 함수 본문을 새로운 함수로 추출한다
- 새로 만들 함수 이름이 기존 함수와 같다면 일단 검색하기 쉬운 이름을 임시로 붙여둔다.
- 추출한 함수에 매개변수를 추가해야 한다면 “간단한 절차”를 따라 추가한다.
- 테스트한다.
- 기존 함수를 인라인한다
- 이름을 임시로 붙여뒀다면 함수 선언 바꾸기를 한 번 더 적용해서 원래 이름으로 되돌린다.
- 테스트한다.
예시: 함수 이름 바꾸기(간단한 절차)
function circum(radius) {
return 2 * Math.PI * radius;
}
- 함수 이름을 이해하기 쉽게 바꾼다.
function circumference(radius) { // 함수 이름 변경
return 2 * Math.PI * radius;
}
- circum()을 호출한 곳을 모두 찾아서 circumference()로 바꾼다.
- 단점은 호출문과 선언문을 한 번에 수정해야 한다는 것이다.
예시: 함수 이름 바꾸기(마이그레이션 절차)
function circum(radius) {
return 2 * Math.PI * radius;
}
- 함수 본문 전체를 새로운 함수로 추출한다.
function circum(radius) {
return circumference(radius);
}
function circumference(radius) { // 새로운 함수로 추출한다.
return 2 * Math.PI * radius;
}
- 수정한 코드를 테스트한다.
- 예전 함수를 인라인한다.
- 리팩터링 대상은 대부분 직접 수정할 수 있는 코드지만, 함수 선언 바꾸기만큼은 직접 고칠 수 없는 외부 코드가 사용하는 부분(공개된 api)을 리팩터링하기에 좋다.
6.6 변수 캡슐화하기
배경
- 데이터는 함수보다 다루기 까다롭다
- 데이터는 참조하는 모든 부분을 한 번에 바꿔야 코드가 제대로 작동하기 때문.
- 그래서 접근할 수 있는 범위가 넓은 데이터를 옮길 때는 먼저 그 데이터로의 접근을 독점하는 함수를 만드는 식으로 캡슐화하는 것이 좋은 방법일 때가 많다.
- 데이터 재구성이라는 어려운 작업을 함수 재구성이라는 더 단순한 작업으로 변환하는 것이다.
- 데이터의 유효범위가 넓을수록 캡슐화해야 한다.
- 그래야 자주 사용하는 데이터에 대한 결합도가 높아지는 일을 막을 수 있다.
- 불변성은 강력한 방부제이다.
절차
- 변수로의 접근과 갱신을 전담하는 캡슐화 함수들을 만든다.
- 정적 검사를 수행한다.
- 변수를 직접 참조하던 부분을 캡슐화 함수 호출로 바꾼다. 하나씩 바꿀 때마다 테스트한다.
- 변수의 접근 범위를 제한한다.
- 변수 이름을 바꿔서 테스트해보면 해당 변수를 참조하는 곳을 쉽게 찾아낼 수 있다.
- 테스트
- 변수 값이 레코드라면 레코드 캡슐화하기를 적용할지 고려한다.
예시
- 기본적인 캡슐화를 위해 가장 먼저 데이터를 읽고 쓰는 함수부터 정의한다.
function getDefaultOwner() { return defaultOwner; }
function setDefaultOwner(arg) {defaultOwner = arg; }
- defaultOwner를 참조하는 코드를 찾아서 방금 만든 getter 함수를 호출하도록 고친다. 대입문은 세터 함수로 바꾼다.
spaceship.owner = getDefaultOwner()
setDefaultOwner({firstName: "레베카", lastName: "파슨스"});
- 모든 참조를 수정했다면 이제 변수의 가시 범위를 제한한다.
let defaultOwner = {firstName: "마틴", lastName: "파울러"}
function getDefaultOwner() { return defaultOwner; }
function setDefaultOwner(arg) {defaultOwner = arg; }
- 복제본 만들기와 클래스로 감싸는 방식은 레코드 구조에서 깊이가 1인 속성들까지만 효과가 있다.
- 더 깊이 들어가면 복제본과 객체 래핑 단계가 더 늘어나게 된다.
- 데이터의 사용 범위가 넓을수록 적절히 캡슐화하는 게 좋다.
6.7 변수 이름 바꾸기
배경
- 명확한 프로그래밍의 핵심은 이름짓기다
- 값이 영속되는 필드라면 이름에 더 신경 써야 한다.
절차
- 폭넓게 쓰이는 변수라면 변수 캡슐화하기를 고려한다.
- 이름을 바꿀 변수를 참조하는 곳을 모두 찾아서, 하나씩 변경한다.
- 테스트한다.
예시
const cpyNm = "애크미 구스베리"
- 먼저 원본의 이름을 바꾼 후, 원본의 원래 이름과 같은 복제본을 만든다.
- 이제 기존 이름을 참조하는 코드들을 새 이름으로 점진적으로 바꿀 수 있다.
const companyName = "애크미 구스베리";
const cpyNm = companyName
6.8 매개변수 객체 만들기
배경
- 데이터 뭉치를 데이터 구조로 묶으면 데이터 사이의 관계가 명확해진다는 이점을 얻는다.
- 게다가 함수가 이 데이터 구조를 받게 하면 매개변수 수가 줄어든다.
- 같은 데이터 구조를 사용하는 모든 함수가 원소를 참조할 때 항상 똑같은 이름을 사용하기 때문에 일관성도 높여준다
절차
- 적당한 데이터 구조가 아직 마련되어 있지 않다면 새로 만든다.
- 테스트한다.
- 함수 선언 바꾸기로 새 데이터 구조를 매개변수로 추가한다.
- 테스트한다.
- 함수 호출 시 새로운 데이터 구조 인스턴스를 넘기도록 수정한다. 하나씩 수정할 때마다 테스트한다.
- 기존 매개변수를 사용하던 코드를 새 데이터 구조의 원소를 사용하도록 바꾼다.
예시
function amountInvoiced(startDate, endDate) {...}
function amountReceived(startDate, endDate) {...}
function amountOverdue(startDate, endDate) {...}
----------------------------------------
function amountInvoiced(aDateRange) {...}
function amountReceived(aDateRange) {...}
function amountOverdue(aDateRange) {...}
6.9 여러 함수를 클래스로 묶기
배경
- 클래스는 데이터와 함수를 하나의 공유 환경으로 묶은 후, 다른 프로그램 요소와 어우러질 수 있도록 그중 일부를 외부에 제공한다.
- 클래스로 묶으면 이 함수들이 공유하는 공통 환경을 더 명확하게 표현할 수 있고, 각 함수에 전달되는 인수를 줄여서 객체 안에서의 함수 호출을 간결하게 만들 수 있다.
절차
- 함수들이 공유하는 공통 데이터 레코드를 캡슐화한다.
- 공통 데이터가 레코드 구조로 묶여 있지 않다면 먼저 매개변수 객체 만들기로 데이터를 하나로 묶는 레코드를 만든다.
- 공통 레코드를 사용하는 함수 각각을 새 클래스로 옮긴다.
- 공통 레코드의 멤버는 함수 호출문의 인수 목록에서 제거한다.
- 데이터를 조작하는 로직들은 함수로 추출해서 새 클래스로 옮긴다.
예시
function base(aReading) {}
function taxableCharge(aReading) {}
function calculateBaseCharge(aReading) {}
----------------------------------------
class Reading {
base() {}
taxableCharge() {}
calculateBaseCharge() {}
}
6.10 여러 함수를 변환 함수로 묶기
배경
- 변환 함수는 원본 데이터를 입력받아서 필요한 정보를 모두 도출한 뒤, 각각을 출력 데이터의 필드에 넣어 반환한다.
- 이렇게 해두면 도출 과정을 검토할 일이 생겼을 때 변환 함수만 살펴보면 된다.
- 여러 함수를 한데 묵는 이유는 도출 로직이 중복되는 것을 피하기 위해서다.
절차
- 변환할 레코드를 입력받아서 값을 그대로 반환하는 변환 함수를 만든다.
- 묶을 함수 중 함수 하나를 골라서 본문 코드를 변환 함수로 옮기고, 처리 결과를 레코드에 새 필드로 기록한다. 그런 다음 클라이언트 코드가 이 필드를 사용하도록 수정한다.
- 테스트한다.
- 나머지 관련 함수도 위 과정에 따라 처리한다.
예시
function base(aReading) {}
function taxableCharge(aReading) {}
----------------------------------------
function enrichReading(argReading) {
const aReading =_.cloneDeep(argReading);
aReading.baseCharge = base(aReading);
aReading.taxableCharge = taxableCharge(aReading);
return aReading
}
6.11 단계 쪼개기
배경
- 모듈이 잘 분리되어 있다면 다른 모듈의 상세 내용은 전혀 기억하지 못해도 원하는 대로 수정을 끝마칠 수도 있다.
- 다른 단계로 볼 수 있는 코드 영역들이 마침 서로 다른 데이터와 함수를 사용한다면 단계 쪼개기에 적합하다는 뜻이다.
절차
- 두 번쟤 단계에 해당하는 코드를 독립 함수로 추출한다.
- 테스트한다.
- 중간 데이터 구조를 만들어서 앞에서 추출한 함수의 인수로 추가한다.
- 테스트한다.
- 추출한 두 번쨰 단계 함수의 매개변수를 하나씩 검토한다. 그중 첫 번째 단계에서 사용되는 것은 중간 데이터 구조로 옮긴다. 하나씩 옮길 때마다 테스트한다.
- 첫 번째 단계 코드를 함수로 추출하면서 중간 데이터 구조를 반환하도록 만든다.
예시