리팩터링 2판의 Chatper 08를 보고 정리한 글입니다.
요소를 다른 컨텍스트로 옮기는 일 역시 리팩터링의 중요한 축이며 이에 대한 리팩터링 기법들을 알아보자.
좋은 소프트웨어 설계의 핵심은 모듈화가 얼마나 잘 되어 있느냐를 뜻하는 모듈성이다. 모듈성이란 프로그램의 어딘가를 수정하려 할 때 해당 기능과 깊이 관련된 작은 일부만 이해해도 가능하게 해주는 능력이다.
이러한 모듈성은 프로그램의 이해도에 따라 구체적인 방법이 달라진다.
ex) 함수를 어디(어떤 맥락에)에 둘지 결정하는 것은 사람마다 다르다. (이러한 것들은 많이 해보면서 적절한 위치를 찾게된다.)
책에서는 2개의 예시로 중첩 함수를 최상위로 옮기기, 다른 클래스로 옮기기가 나온다. 예제들이 간단하기 때문에 책 참고.
프로그램의 진짜 힘은 데이터 구조에서 나온다.
→ 주어진 문제에 적합한 데이터 구조를 활용하면 코드는 자연스럽게 단순하고 직관적으로 짜여지지만, 그렇지 않으면 데이터를 다루기 위한 코드로 범벅이 되기 때문
구조체 여러 개에 정의된 똑같은 필드들을 갱신해야 한다면 한 번만 갱신해도 되는 다른 위치로 옮기라는 신호다 → 필드 옮기기 리팩터링 적용 필요
리팩터링 전의 코드
class Customer {
constructor(name, discountRate) {
this._name = name;
this._discountRate = discountRate;
this._contract = new CustomerContract(dateToday());
}
get discountRate() { return this._discountRate;}
becomePreferred() {
this._discountRate += 0.3;
// other things...
}
applyDiscount(amount) {
return amount.subtract(amount.multiply(this._discountRate));
}
}
class CustomerContract {
constructor(startDate) {
this._startDate = startDate;
}
}
리팩터링 이후 코드
class Customer {
constructor(name, discountRate) {
this._name = name;
this._setDiscountRate(discountRate);
this._contract = new CustomerContract(dateToday());
}
get discountRate() { return this._discountRate;}
_setDiscountRate(aNumber) {this._discountRate = aNumber;}
becomePreferred() {
this._setDiscountRate(this.discountRate + 0.03);
// other things...
}
applyDiscount(amount) {
return amount.subtract(amount.multiply(this.discountRate));
}
}
class CustomerContract {
constructor(startDate, discountRate) {
this._startDate = startDate;
this._discountRate = discountRate;
}
get discountRate() { return this._discountRate; }
set discountRate(arg) { this._discountRate = arg; }
}
class Customer {
constructor(name, discountRate) {
this._name = name;
this._contract = new CustomerContract(dateToday());
this._setDiscountRate(discountRate);
}
get discountRate() { return this._contract.discountRate;}
_setDiscountRate(aNumber) {this._contract.discountRate = aNumber;}
}
두 번쨰 예제로 공유 객체로 이동하는 리팩터링이 등장하는데 이러한 리팩터링의 경우 겉보기 동작이 달라지는지 잘 확인하고 리팩터링을 해야함에 유의하자. 예시는 간단하여 따로 첨부 X
반대 리팩터링: 문장을 호출한 곳으로 옮기기
특정 함수를 호출하는 코드가 나올 때마다 그 앞이나 뒤에 똑같은 코드가 추가로 실행된다면, 반복되는 부분을 피호출 함수로 합쳐보자.
반복되는 부분에서 무언가 수정할 일이 생겼을 때 단 한곳만 수정하면 된다.
예시에서는 절차의 과정에 따라 변하는 과정을 보여주는데, 간단하여 리팩터링 전과 후의 코드를 보자.
리팩터링 전
function renderPerson(outStream, person) {
const result = [];
result.push(`<p>${person.name}</p>`);
result.push(renderPhoto(person.photo));
result.push(`<p>제목: ${person.photo.title}</p>`);
result.push(emitPhotoData(person.photo));
return result.join('\n');
}
function photoDiv(p) {
return ['<div>', `<p>제목: ${p.title}</p>`, emitPhotoData(p), '</div>'].join('\n');
}
function emitPhotoData(aPhoto) {
const result = [];
result.push(`<p>위치: ${aPhoto.location}</p>`);
result.push(`<p>날짜: ${aPhoto.date.toDateString()}</p>`);
return result.join('\n');
}
리팩터링 후
function renderPerson(outStream, person) {
const result = [];
result.push(`<p>${person.name}</p>`);
result.push(renderPhoto(person.photo));
result.push(emitPhotoData(person.photo));
return result.join('\n');
}
function photoDiv(p) {
return ['<div>', emitPhotoData(p), '</div>'].join('\n');
}
function emitPhotoData(aPhoto) {
return [
`<p>제목: ${person.photo.title}</p>`, // 안으로 가져옴
`<p>위치: ${aPhoto.location}</p>`,
`<p>날짜: ${aPhoto.date.toDateString()}</p>`,
].join('\n');
}
반대 리팩터링: 문장을 함수로 옮기기
코드베이스의 기능 범위가 달라지면 추상화의 경계도 움직이게 된다.
이러한 경우 호출자와 호출 대상의 경계를 완전히 다시 정하고, 새로운 추상화를 위해 문장을 호출한 곳으로 리팩터링을 진행해보자.
listRecentPhotos()가 위치 정보를 다르게 렌더링하도록 만들어야 한다고 가정해보자.
리팩터링 전
function renderPerson(outStream, person) {
outStream.write(`<p>${person.name}</p>\n`);
renderPhoto(outStream, person.photo);
emitPhotoData(outStream, person.photo);
}
function listRecentPhotos(outStream, photos) {
photos.filter(p => p.date > recentDateCutoff()).forEach(p => {
outStream.write("<div>\n");
emitPhotoData(outStream, p);
outStream.write("<div>\n");
})
}
function emitPhotoData(outStream, photo) {
outStream.write(`<p>제목: ${photo.title}</p>\n`);
outStream.write(`<p>날짜: ${photo.date.toDateString()}</p>\n`);
outStream.write(`<p>위치: ${photo.location}</p>\n`);
}
리팩터링 후
function renderPerson(outStream, person) {
outStream.write(`<p>${person.name}</p>\n`);
renderPhoto(outStream, person.photo);
emitPhotoData(outStream, person.photo);
outStream.write(`<p>위치: ${person.photo.location}</p>\n`); // 이동한 부분
}
function listRecentPhotos(outStream, photos) {
photos.filter(p => p.date > recentDateCutoff()).forEach(p => {
outStream.write("<div>\n");
emitPhotoData(outStream, p);
outStream.write(`<p>위치: ${p.location}</p>\n`); // 이동한 부분
outStream.write("<div>\n");
})
}
function emitPhotoData(outStream, photo) {
outStream.write(`<p>제목: ${photo.title}</p>\n`);
outStream.write(`<p>날짜: ${photo.date.toDateString()}</p>\n`);
}
함수의 이름이 코드의 동작 방식보다는 목적을 말해주기 때문에 함수를 활용하면 코드를 이해하기가 쉬워진다.
→ 인라인된 코드보다 제대로 작성된 함수이름을 호출하는 편이 가독성이 좋다는 의미
책에 따로 존재 X
관련된 코드들이 가까이 모여 있다면 이해하기가 더 쉽다.
코드를 관련된 것끼리 묶자.
리팩터링 전
const pricingPlan = retrievePricingPlan();
const order = retrieveOrder();
let charge;
const chargePerUnit = pricingPlan.unit();
리팩터링 후
const pricingPlan = retrievePricingPlan();
const chargePerUnit = pricingPlan.unit;
const order = retrieveOrder();
let charge;
하나의 반복문 안에서 두 가지 이상의 일을 수행하는 모습을 볼 수 있다. 그러나 반복문을 수정해야 할 때마다 두 가지 일 모두를 잘 이해하고 진행해야하기 때문에 반복문을 분리해보자.
반복문 쪼개기를 통해 최적화와 멀어지는 경우도 있지만, 다른 더 강력한 최적화를 적용할 수 있는 길을 열어주기도 한다.
리팩터링 전
let youngest = people[0] ? people[0].age : Infinity;
let totalSalary = 0;
for (const p of people) {
if (p.age < youngest) youngest = p.age;
totalSalary += p.salary;
}
return `최연소: ${youngest}, 총 급여: ${totalSalary}`;
리팩터링 후
let youngest = people[0] ? people[0].age : Infinity;
let totalSalary = 0;
for (const p of people) {
totalSalary += p.salary;
}
for (const p of people) {
if (p.age < youngest) youngest = p.age;
}
return `최연소: ${youngest}, 총 급여: ${totalSalary}`;
더 가다듬기 적용(함수 추출후 반복문을 파이프라인과 알고리즘 교체하기 적용)
function totalSalary() {
return people.reduce((total, p) => total + p.salary, 0);
}
function youngestAge() {
return Math.min(...people.map((p) => p.age));
}
return `최연소: ${youngestAge()}, 총 급여: ${totalSalary()}`;
파이프라인을 이용하면 처리 과정을 일련의 연산으로 표현할 수 있다.
→ 반복문 대신 map, filter 등을 사용하는 리팩터링
인도에 자리한 사무실을 찾아서 도시명과 전화번호를 반환하는 함수
리팩터링 전
function acquireDate(input) {
const lines = input.split('\n');
let firstLine = true;
const result = [];
for (const line of lines) {
if (firstLine) {
firstLine = false;
continue;
}
if (line.trim() === '') continue;
const record = line.split(',');
if (record[1].trim() === 'india') {
result.push({ city: record[0].trim(), phone: record[2].trim() });
}
}
return result;
}
리팩터링 후
function acquireDate(input) {
const lines = input.split('\n');
const result = lines
.slice(1)
.filter((line) => line.trim() !== '')
.map((line) => line.split(','))
.filter((record) => record[1].trim() === 'india')
.map((record) => ({ city: record[0].trim(), phone: record[2].trim() }));
return result;
}
사용되지 않는 코드가 있다면 다른 누군가가 그 코드를 보았을 때, 무슨 의도로 그 코드가 작성되었는지 헷갈리기 때문에 지워버리자.
따로 존재 X