가장 기본적이고 많이 사용해서 제일 먼저 배워야 하는 리팩터링
1. 함수 추출하기
가장 빈번하게 사용되는 리팩터링 중 하나로, 코드조각을 찾아 무슨 일을 하는지 파악한 다음, 독립된 함수로 추출하고 목적에 맞는 이름을 붙인다.
배경
- 코드의 길이 혹은 재사용성, 코드의 시인성에 따라서 수행
- ‘목적과 구현을 분리’ ⇒ 코드를 보고 그 코드가 무슨일을 하는지 파악하는게 오래 걸린다면, 그 코드를 별도의 함수로 빼서 적절한 이름을 붙힘으로써 나중에 그 코드를 읽을때 함수의 이름으로 목적을 파악 하기 쉬움
- 함수를 짧게, 여러개 만들면 성능에 악영향을 끼치지 않을까?
- ⇒ 응 아니야
- 오히려 함수가 짧으면 캐싱하기가 더 쉽기 때문에 컴파일러가 최적화 하는데 유리할 때가 많다.
절차
- 함수를 새로 만들고 목적을 잘 드러내는 이름을 붙인다(’어떻게’가 아닌 ‘무엇을’ 하는지가 드러나는 이름)
- 추출할 코드를 원본함수에서 복사하여 새 함수에 붙여넣는다.
- 추출한 코드중 지역변수를 참조하거나 유효범위를 벗어나는 변수는 없는지 확인한다.
- 컴파일
- 원본코드를 새로만든 함수로 대치한다.
- 테스트한다.
- 추가한 함수와 동일하거나 비슷한 일을 하는 코드가 있는지 검토한다.
예시
function printOwing(invoice) {
let outstanding = 0;
console.log('**********');
console.log('**고객채무**');
console.log('**********');
...
}
function printOwing(invoice) {
let outstanding = 0;
printBanner(); => 배너 출력 로직을 함수로 추출
...
}
function printBanner() {
console.log('**********');
console.log('**고객채무**');
console.log('**********');
};
예시: 지역변수의 값을 변경할 때*
function printOwing(invoice) {
let outstanding = 0;
// 미해결 채무(outstanding)를 계산한다
for (const o of invoice.orders) {
outstanding += o.amount;
}
printBanner();
recordDueDate(invoice);
printDetails(invoice, outstanding);
}
function printOwing(invoice) {
printBanner();
const outstanding = calculateOutstanding(invoice); // 함수 추출 완료. 추출한 함수가 반환한 값을 원래 변수에 저장
recordDueDate(invoice);
printDetails(invoice, outstanding);
}
function calculateOutstanding(invoice) {
return invoice.orders.map((o) => {
result += o.amount;
});
}
2. 함수 인라인 하기
6.1의 ‘함수 추출하기’의 반대 리팩터링으로, 함수 본문을 코드로 인라인하여 불필요한 함수들을 줄이는 리팩터링이다.
배경
- 때로는 함수 본문이 이름만큼 명확한 경우도 있다.
- 간접 호출을 너무 과하게 사용해서 위임관계가 복잡하게 얽히면 코드가독성이 떨어진다.
- 리팩터링 중 잘못 추출된 함수들이 존재하는 경우, 함수 인라인을 수행한다.
절차
- 인라인 가능한 함수인지 검토한다.
- 서브클래스에서 오버라이드하는 다형 메서드는 인라인 하지 않는다.
- 인라인 할 함수를 호출하는 곳을 모두 찾아서 인라인이 적절한지 검토한다.
- 각 호출문을 함수 본문으로 교체한다.
- 교체 후 테스트 한다.
- 함수 정의(원래함수)를 삭제한다.
예시
function rating(aDriver) {
return moreThanFiveLateDeliveries(aDriver) ? 2 : 1;
}
function moreThanFiveLateDeliveries(aDriver) {
return aDriver.numberOfLateDeliveries > 5;
}
function rating(aDriver) {
return aDriver.numberOfLateDeliveries > 5;
}
아주아주 조금 더 복잡한 예시
function reportLines(aCustomer) {
const lines = [];
gatherCustomerData(lines, aCustomer);
return lines;
}
function gatherCustomerData(out, aCustomer) {
out.push(["name", aCustomer.name]);
out.push(["location", aCustomer.location]);
}
function reportLines(aCustomer) {
const lines = [];
lines.push(["name", aCustomer.name]);
lines.push(["location", aCustomer.location]);
return lines;
}
3. 변수 추출하기
지역변수를 활용하여 코드를 잘게 쪼개서 관리하면 복잡한 로직을 구성하는 단계마다 이름을 붙일 수 있어서 코드의 목적을 훨씬 명확하게 드러낼 수 있다.
배경
- 표현식이 너무 복잡해서 이해하기 어려운 경우가 있다.
- 변수를 추가하면 중단점으로 활용할 수 있기 때문에 디버깅하기도 쉬워진다.
- 변수를 추출하려면 문맥을 살펴서 추출방식을 선택한다.
- 한정된 영역 또는 문맥에서만 활용된다면 지역변수로 추출
- 넓은 문맥 혹은 동일한 코드가 존재한다면 함수로 추출
절차
- 추출하려는 표현식에 부작용은 없는지 확인한다.
- 불변 변수를 하나 선언하고 이름을 붙일 표현식의 복제본을 대입한다.
- 원본 표현식을 새로 만든 변수로 교체한다.
- 테스트
- 표현식을 여러 곳에서 사용한다면 각각을 새로 만든 변수로 교체한다.
예시*
function price(order) {
// 가격(price) = 기본 가격 - 수량 할인 + 배송비
return (
order.quantity * order.itemPrice -
Math.max(0, ordre.quantity - 500) * order.itemPrice * 0.05 +
Math.min(order.quantity * order.itemPrice * 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);
// 가격(price) = 기본 가격 - 수량 할인 + 배송비
return basePrice - quantityDiscount + shipping;
}
클래스 안에서의 예시
class Order {
constructor(aRecord) {
this._data = aRecord;
}
get itemPrice() {
return this._data.itemPrice;
}
get price() {
return this.basePrice - this.quantityDiscount + 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);
}
}
4. 변수 인라인하기
6.3의 ‘변수 추출하기’의 반대 리팩터링으로, 별도의 변수로 두지 않고 인라인 하는 리팩터링
배경
- 변수를 사용하는 것은 대체로 긍정적이지만, 문맥상 별도의 변수가 필요 없는 경우도 있다.
- 해당 변수가 주변 리팩터링을 진행하는 데에 방해가 될 수도 있다.
절차
- 대입문의 우변(표현식)에서 부작용이 생기지 않는지 검토한다.
- 변수가 불변으로 선언되지 않았다면 불변으로 만든 후 테스트한다.
- 변수에 값이 단 한번만 대입되었는지 확인
- 이 변수를 가장 처음 사용하는 코드를 찾아서 대입문 우변의 코드로 바꾼다.
- 테스트한다.
- 변수를 사용하는 모든부분을 교체하면서 테스트를 반복한다.
- 변수를 지운다.
예시
function getBasePrice(anOrder) {
let basePrice = anOrder.basePrice;
return basePrice > 1000;
}
function getBasePrice(anOrder) {
return anOrder.basePrice > 1000;
}
5. 함수 선언 바꾸기
함수 선언과 호출문들의 이름, 매개변수들을 추가/삭제 함으로써 코드를 개선하는 리팩터링
배경
- 함수 선언은 각 부분이 서로 맞물리는 방식을 표현하며, 건축과 마찬가지로 소프트웨어도 이러한 연결부에 상당히 의존한다.
- 이러한 연결부(함수)는 이름에 상당히 의존한다.
- 한번에 완벽한 이름을 지을 수는 없다. 코드를 보다가 함수의 이름이 와닿지 않는다면, 그때그때 더 나은 이름으로 바꿀 수 있는지 검토한다.
- Tip: 주석을 이용해 함수의 역할을 설명해보고 해당 주석을 통하여 함수 이름을 결정한다.
- 매개변수는 함수가 외부 세계와 어우러지는 방식을 정의하기 때문에, 매개변수의 선택에 따라 함수의 활용범위, 캡슐화 정도가 달라진다(ex. 전화번호를 포맷팅하는 함수의 매개변수로 사람을 받을 것인가? 번호만 받을것인가?)
간단한 절차
- 만약 매개변수를 제거하려거든 먼저 함수 본문에서 제거 대상 매개변수를 참조하는 곳은 없는지 확인한다.
- 메서드 선언을 원하는 형태로 바꾼다.
- 기존 메서드 선언을 참조하는 부분을 모두 찾아서 바뀐 형태로 수정한다.
- 테스트
마이그레이션 절차(이름 변경 & 매개변수 추가를 모두 할 경우)
- 먼저 함수의 본문을 적절히 리팩터링한다.
- 함수 본문을 새로운 함수로 추출한다.(임시이름으로)
- 추출한 함수에 매개변수를 추가해야 한다면 ‘간단한 절차’를 따라 추가한다.
- 테스트
- 기존 함수를 인라인한다.
- 임시 이름을 원래대로 돌린다.
- 이름부터 바꾸고 > 테스트하고 > 매개변수를 수정하고 > 테스트한다
예시: 함수이름 바꾸기
cuircum(radius);
function circum(radius) {
return 2 * Math.PI * redius;
}
circum(radius);
function circum(radius) {
return circumference(radius);
}
function circumference(radius) {
return 2 * Math.PI * redius;
}
circumference(radius);
function circumference(radius) {
return 2 * Math.PI * redius;
}
예시: 매개변수 추가하기(클래스에서)
...book 클래스
this.addReservation(customer);
addReservation(customer){
this._reservation.push(customer);
}
...book 클래스
this.addReservation(customer);
addReservation(customer){
this.zz_addReservation(customer, false);
}
zz_addReservation(customer, isPriority){
assert(isPriority === true || isPriority === false);
this._reservation.push(customer);
}
...book 클래스
this.zz_addReservation(customer, false);
zz_addReservation(customer, isPriority){
assert(isPriority === true || isPriority === false);
this._reservation.push(customer);
}
예시: 매개변수를 속성으로 바꾸기*
function inNewEngland(aCustomer) {
return ["MA", "CT", "ME", "VT", "NH", "RI"].includes(aCustomer.address.state);
}
const newEnglanders = someCustomers.filter((c) => inNewEngland(c));
function inNewEngland(stateCode) {
return ["MA", "CT", "ME", "VT", "NH", "RI"].includes(stateCode);
}
const newEnglanders = someCustomers.filter((c) =>
inNewEngland(c.address.stateCode)
);
6. 변수 캡슐화 하기
데이터 변수에 대한 접근 및 변경을 캡슐화하여 그 데이터로의 접근을 독점하고, 데이터를 변경하고 사용하는 코드를 감시할 수 있도록 하는 리팩터링
배경
- 데이터 관련 리팩터링은 함수와 달리 데이터를 참조하는 모든 부분을 한 번에 바꿔야 제대로 동작한다.
- 짧은 함수 안의 임시 변수처럼 유효범위가 좁으면 상관없지만, 데이터가 사용되는 유효범위가 넓어질 수록 데이터를 다루기가 어려워진다.
- 데이터의 범위가 넓을 수록 캡슐화하여 데이터에 대한 결합도를 낮추는 것이 좋다.
- 객체지향에서는 데이터를 private하게 유지하여 getter와 setter로 접근하게 하는것이 좋다.
- 불변데이터는 원본 데이터를 참조하는 코드를 변경할 필요가 없고, 데이터를 변형시키는 코드를 걱정할 일도 없기때문에 캡슐화할 이유가 적다.
절차
- 변수로의 접근과 갱신을 전담하는 캡슐화 함수들을 만든다.
- 변수를 직접 참조하던 부분을 모두 적절한 캡슐화 함수 호출로 바꾼다.
- 테스트
- 변수의 접근 범위를 제한하여 결합도를 낮춘다.
- 별도의 js파일로 관리, 이름 변경 등
예시*
let defaultOwner = {firstName: "마틴", lastName: "파울러"};
spaceship.owner = defaultOwner;
defaultOwner = {firstName: "레베카", lastName: "파울러"};
복제본을 반환하도록 하여 캡슐화
let _defaultOwnerData = {firstName: "마틴", lastName: "파울러"};
export function defaultOwner() {
return Object.assign({}, _defaultOwnerData);
}
export function setDefaultOwner(arg) {
_defaultOwnerData = arg;
}
레코드 캡슐화하기
let defaultOwnerData = {firstName: "마틴", lastName: "파울러"};
function defaultOwner() {
return new Person(defaultOwnerData);
}
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;
}
}
7. 변수 이름 바꾸기
코드의 목적과 역할, 흐름에 어울리는 더욱 적절한 변수의 이름을 바꾸는 리팩토링
배경
- 명확한 프로그래밍의 핵심은 이름짓기이며, 변수는 프로그래머가 하려는 일에 관해 많은 것을 설명해 준다.
- 문제에 대한 이해도가 높아져서, 혹은 사용자의 요구가 달라져서 목적이 달라진다면, 이에 따른 새로운 이름을 지어줄지에도 고민이 필요하다.
- 함수 호출 한 번으로 끝나지 않고 값이 영속되는 필드라면 이름에 더 신경써야 한다.
절차
- 폭넓게 쓰이는 변수라면 변수 캡슐화 하기를 고려한다.
- 이름을 바꿀 변수를 참조하는 곳을 모두 찾아서, 하나씩 변경한다.
- 다른 코드 베이스에서 참조하는 변수는 외부에 공개된 변수이므로 이 리팩터링을 적용할 수 없다.
- 변수 값이 변하지 않는다면 다른 이름으로 복제본을 만들어서 하나씩 점진적으로 변경하며 테스트 한다.
예시
let tpHd = ‘untitled’;
const result += '<h1>${tpHd}</h1>'; // tpHd 변수를 읽는데만 사용
tpHd = obj['articleTitle']; // tpHd값을 수정
변수 캡슐화 하기
const result += '<h1>${tpHd}</h1>';
setTitle(obj['articleTitle']);
function title() {return tpHd;} // tpHd 변수의 getter
function setTitle(arg) {tpHd = arg;} // tpHd 변수의 setter
8. 매개변수 객체 만들기
몰려다니는 여러개의 매개변수들을 하나의 데이터 구조로 변경하여 데이터 사이의 관계를 명확히 하고 코드를 근본적으로 관리하도록 하는 리팩터링
배경
- 매개변수의 수를 줄이면 코드가 명확해지고, 일관성이 높아진다.
- 새로 만든 데이터 구조가 문제 영역을 훨씬 간결하게 표현하는 새로운 추상 개념으로 격상된다.
절차
- 적당한 데이터 구조가 아직 마련되어 있지 않다면 새로 만든다.
- 객체 또는 클래스
- 함수 선언 바꾸기로 새 데이터 구조를 매개 변수로 추가한다.
- 함수 호출 시 새로운 데이터 구조 인스턴스를 넘기도록 수정한다. 하나씩 수정할 때마다 테스트 한다.
- 기존 매개 변수를 사용하던 코드를 새 데이터 구조의 원소를 사용하도록 바꾼다.
- 다 바꿨다면 기존 매개변수를 제거하고 테스트 한다.
예시*
const station = {
name: "ZB1",
readings: [
{temp: 47, time: "2016-11-10 09:10"},
{temp: 53, time: "2016-11-10 09:10"},
{temp: 58, time: "2016-11-10 09:10"},
{temp: 53, time: "2016-11-10 09:10"},
{temp: 51, time: "2016-11-10 09:10"},
],
};
function readingsOutsideRange(station, min, max) {
return station.readings.filter((r) => r.temp < min || r.temp > max);
}
const alerts = readingsOutsideRange(
staion,
operatingPlan.temperatureFloor,
operatingPlan.temperatureCeiling
);
const station = {
name: "ZB1",
readings: [
{temp: 47, time: "2016-11-10 09:10"},
{temp: 53, time: "2016-11-10 09:10"},
{temp: 58, time: "2016-11-10 09:10"},
{temp: 53, time: "2016-11-10 09:10"},
{temp: 51, time: "2016-11-10 09:10"},
],
};
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
);
const alerts = readingsOutsideRange(staion, range);
class NumberRange {
constructor(min, max) {
this._data = {min: min, max: max};
}
get min() {
return this._data.min;
}
get max() {
return this._data.max;
}
}