2회차
6장 기본적인 리팩터링
함수 추출하기
function printOwing(invoice) {
printBanner();
let outstanding = calculateOutstanding();
console.log('...');
console.log('...');
}
function printOwing(invoice) {
printBanner();
let outstanding = calculateOutstanding();
printDetails(outstanding);
}
function printDetails(outstating) {
console.log(`채무액: ${outstanding}`);
}
- 목적과 구현을 분리한다.
- 하나의 일, 하나의 책임만 갖도록 만들자.
- console.log를 어떤 의도로 실행시키는지 알려면 넘겨주는 인자를 확인해야 하는데, printDetails 라는 함수로 추출해서 사용하면 의도(What)를 한번에 알 수 있다. 의도를 잘 드러낼 수 있는 이름이 매우 중요하다.
- 함수를 추출할 때는, 추상화를 할 때는 어떤 부분을 숨기고 어떤 부분을 외부로 노출시킬지에 대해 많은 고민이 필요한 것 같다.
함수 인라인하기
function getArrayLength(arr) {
return arr.length;
}
const users = [{...}, {...}, {...}];
const usersLength = getArrayLength(users);
const users = [{...}, {...}, {...}];
const usersLength = users.length;
- 처음에 코드를 작성할 때 무조건 함수로 추출하는 게 많이 했던 실수였다. 그 이유는 과도한 추상화가 되는 경우가 많았다.
- 예시의 경우에는 과도한 추상화가 되었다고 생각한다. 자바스크립트의 배열 프라퍼티인 Array: length로 배열의 길이를 가져오는게 훨씬 의도가 잘 드러난다고 생각하기 때문이다.
변수 추출하기
return order.quantity * order.itemPrice -
Math.max(0, order.quantity - 500) * order.itemPrice * 0.05 +
Math.min(order.quantity * order.itemPrice * 0.1, 100);
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
- 복잡한 표현식은 변수로 추출해서 사용하는 것이 매우 좋다고 생각한다. 특히 코드를 작성할 당시에는 맥락이 이해가 잘 되지만 나중에 코드를 읽는 경우에 복잡한 표현식은 이해하기 어려운 경우가 많다.
- 예전에 과제를 할 때 이런식으로 의도를 드러낸 적이 있었다.
const 대주주_최소_지분율 = 2;
const 대주주_최소_주식가치 = 금액['10억'];
/**
* '보유한 주식의 총 가치가 10억원 이상' 또는 '지분율이 2%' 이상인 주주는 대주주로 판단합니다.
*/
export function is대주주({
보유주식수,
주당단가,
주식총발행량,
}: {
보유주식수: number;
주당단가: number;
주식총발행량: number;
}) {
const 주주_주식가치 = 보유주식수 * 주당단가;
const 주주_지분율 = (보유주식수 / 주식총발행량) * 100;
if (주주_주식가치 > 대주주_최소_주식가치 || 주주_지분율 > 대주주_최소_지분율) {
return true;
}
return false;
}
변수 인라인하기
let basePrice = anOrder.basePrice;
return basePrice > 1000;
return anOrder.basePrice > 1000;
- 함수 인라인하기와 동일하게 변수로 추출해도 의도가 드러나는 정도에 큰 차이가 없다면 오히려 변수 인라인이 더 좋다고 생각한다.
함수 선언 바꾸기
function circum(radius) {...}
function circumference(radius) {...}
- 함수를 변경하는 것은 모두 함수 선언 바꾸기라고 얘기할 수 있는 것 같다
- 개발을 하다보면 인지 부하가 생겨서 좋은 변수명, 함수명을 생각하기 어려울때가 있는데 PR을 올리기 전에 스스로 셀프 코드 리뷰를 하면서 점검하기 좋은 부분인 것 같다.
- 함수의 매개변수가 너무 많은 정보를 담고 있거나 내부 구현이 잘못되어 있는 경우에도 수정한다.
변수 캡슐화하기
let defaultOwner = {firstName: '마틴', lastName:'파울러'};
let defaultOwnerData = {firstname: '마틴', lastName:'파울러'};
export function defaultOwner() {return defaultOwnerData;}
export function setDefaultOwner(arg) {defaultOwnerData = arg;}
- 프로그램에서 데이터를 변경하는 경우에는 해당 데이터를 사용하는 곳을 모두 변경해야 하는데, 데이터를 여러가지 방식으로 접근하고 있다고 가정하면 모든 방식에 대해 처리를 해줘야 한다. 이러한 방식은 유지보수가 매우 어렵다. 따라서 데이터의 접근을 한 곳에서만 가능하도록 한다.
- React에서는 화면에 보여지는 데이터를 useState훅으로 관리할 수 있다. 함수 추출하기를 통해 커스텀 훅으로 만들어주면 응집도가 더 높아진다.
- getter의 역할을 하는 코드에서 객체를 반환하는 경우에는 값을 바꿀 수 없도록 처리해줘야 한다.
변수 이름 바꾸기
let a = height * width;
let area = height * width;
- 변수 이름만 보고도 프로그램이 어떻게 동작할 수 있도록 파악할 수 있어야 한다. 신중하게 짓기. 변수 이름이 좋지 않으면 나중에 돌려받는다.
매개변수 객체 만들기
function amountInvoiced(startDate, endDate) {...}
function amountInvoiced(aDateRange) {...}
- 데이터 뭉치를 데이터 구조 하나로 묶어준다.
- 기존에는 단순히 매개변수가 많으면 객체로 다루는 걸로 생각했었는데 비슷한 데이터끼리 묶어준다는 개념은 좋은 것 같다.
- 값 객체로 만들어주는 것도 좋아보이는데 실제로 사용해보지 않아서 어색하게 느껴지는 것 같다. 기존에 코드를 작성할때는 파일명으로 값과 메서드를 구분지었던 것 같은데 클래스로 구분지어도 좋을 것 같다.
여러 함수를 클래스로 묶기
function base(aReading) {...}
function taxableCharge(aReading) {...}
function calculateBaseCharge(aReading) {...}
class Reading {
base() {...}
taxableCharge() {...}
calculateBaseCharge() {...}
}
- 비슷한 역할을 하는 함수를 클래스로 묶어준다.
- 기존에는 파일 단위(*.ts)로 묶어줬던 것 같다.
- 클래스를 활용하는 방식도 충분히 좋아보인다.
여러 함수를 변환 함수로 묶기
function base(aReading) {...}
function taxableCharge(aReading) {...}
function enrichReading(argReading){
const aReading = _.cloneDeep(argReading);
aReading.baseCharge = base(aReading);
aReading.taxableCharge = taxableCharge(aReading);
}
- 도출된 정보를 derived value라고 생각했다. 이러한 값들은 사용되는 곳마다 계속해서 계산해줘야하는데, 미리 값을 계산하는 코드를 한 곳에 모아두고 이 곳을 통해서만 접근하자.
- 유지보수를 좋은 방법이라고 생각한다.
단계 쪼개기
const orderData = orderString.split(/\\s+/);
const productPrice = priceList[orderData[0].split('-')[1]];
const orderPrice = parseInt(orderData[1]) * productPrice;
const orderRecord = parseOrder(order);
const orderPrice = price(orderRecord, priceList);
function parseOrder(aString) {
}
function price(order, priceList) {
}
- 동작을 연이은 두 단계로 쪼갠다. 순차적으로 진행할 수 있도록 만든다. 잘게 쪼갠다.
- 테스트 코드를 작성할 때의 Given-When-Then 처럼 느껴졌다.
7장 캡슐화
레코드 캡슐화하기
organization = {name: '구스베리', country: 'GB'};
class Organization {
constructor(data) {
this._name = data.name;
this._country = data.country;
}
get name() {return this._name;}
set name(arg) {this._name = arg;}
get country() {return this._country;};
set country(arg) {this._country = arg;}
}
- 데이터를 클래스로 캡슐화해서 관리한다.
- 추가적인 데이터를 만들어내기 위한 연산이 들어가는 경우에 좋다고 생각한다. (country가 한국인지 체크하는 경우 isKorean과 같은 메소드를 클래스안에 만들면 된다)
- 프론트엔드에서도 서버로부터 데이터를 받아왔을 때 해당 데이터로 추가적인 연산이 필요하다면 클래스로 관리하는것도 편리할 것 같다.
컬렉션 캡슐화하기
class Person {
get courses() {return this._courses;}
set courses(aList) {this._courses = aList;}
class Person {
get courses() {return this._courses.slice();}
addCourse(aCourse) {...}
removeCourse(aCourse) {...}
- getter의 경우에 복제본을 반환하고, setter의 사용을 지양하자. 의도를 더 드러내자.
기본형을 객체로 바꾸기
orders.filter(o => 'high' === o.priority || 'rush' === o.priority);
orders.filter(o => o.priority.higherThan(new Priority('normal')));
- 단순한 출력 이상의 기능이 필요해지는 순간 그 데이터를 표현하는 전용 클래스를 정의한다.
- 로직을 캡슐화하여 응집도를 높이고 의도를 더 잘 드러낼 수 있다.
- 개인적으로는 클래스로 데이터를 다룬 경험이 많이 없어서 어색하지만 좋다고 생각한다.
임시 변수를 질의 함수로 바꾸기
const basePrice = this._quantity * this._itemPrice;
if(basePrice > 1000){
return basePrice * 0.95;
} else {
return this.basePrice * 0.98;
}
get basePrice() {return this._quantity * this._itemPrice;}
...
if(basePrice > 1000){
return basePrice * 0.95;
} else {
return this.basePrice * 0.98;
}
- 코드의 결과값을 다른 곳에서도 사용하기 위해 임시 변수 대신에 함수로 만들어서 사용한다.
- 코드 중복을 줄이기 위해 사용하면 좋을 것 같다.
클래스 추출하기
class Person {
get officeAreaCode() {return this.officeAreaCode;}
get officeNumber() {return this.officeNumber;}
}
class Person {
get officeAreaCode() {return this._telephoneNumber.areaCode;}
get officeNumber() {return this._telephoneNumber.number;}
}
class TelephoneNumber {
get areaCode() {return this._areaCode;}
get number() {return this._number;}
}
- SRP, 클래스에게 하나의 책임만 갖도록 하자.
- 메서드와 데이터가 너무 많은 클래스는 클래스 추출의 신호로 볼 수 있다.
클래스 인라인하기
class Person {
get officeAreaCode() {return this._telephoneNumber.areaCode;}
get officeNumber() {return this._telephoneNumber.number;}
}
class TelephoneNumber {
get areaCode() {return this._areaCode;}
get number() {return this._number;}
}
class Person {
get officeAreaCode() {return this.officeAreaCode;}
get officeNumber() {return this.officeNumber;}
}
- 클래스를 추출했을 때 역할이 거의 없는 경우, 혹은 클래스를 다른 방식으로 추출하고 싶은 경우 우선 인라인을 한다.
위임 숨기기
manager = aPerson.department.manager;
manager = aPerson.manager;
class Person {
get manager() {return this.department.manager;}
- 모듈화 설계의 핵심은 캡슐화다. 외부에 어떤 정보를 노출할 것인지 범위를 정하는 것.
- 예제의 인터페이스는 매니저라는 정보를 알고 싶은데 department라는 객체까지 알아야 한다. 위임 메서드를 만들자.
중개자 제거하기
manager = aPerson.manager;
class Person {
get manager() {return this.department.manager;}
manager = aPerson.department.manager;
- 위임 객체의 다른 기능을 사용하고 싶을 떄 마다 계속해서 코드를 추가해야 하는 문제가 있다.
- 이 경우에 위임 객체를 직접 호출하는게 나을 수 있다.
- 정답은 없고 상황에 맞게 판단하고, 추후에 수정해도 된다.
알고리즘 교체하기
- 문제를 해결하는 방식(알고리즘)을 수정하는 것
- 교체했을 때 가독성이 좋아지거나, 성능이 좋아진다면 무조건 하면 좋을 것 같다. 다만 교체후에 겉보기 동작에 이상이 없는지 주의해서 확인하기
8장 기능 이동
함수 옮기기
class Account {
get overdraftCharge() {...}
class AccountType {
get overdraftCharge() {...}
- 모듈성을 높이려면 서로 연관된 요소들을 함께 묶고, 요소 사이의 연결 관계를 쉽게 찾고 이해할 수 있도록 해야 한다.
- 도메인에 대한 이해가 완벽하지 않다면 처음부터 적절한 위치에 함수를 위치시키는 것은 어려울 수 있다. 중간에 변경하면 된다.
필드 옮기기
class Customer {
get plan() {return this._plan;}
get discountRate() {return this._discountRate;}
class Customer {
get plan() {return this._plan;}
get discountRate() {return this.plan.discountRate;}
- 프로그램에서는 데이터 구조가 매우 중요하다. 적합한 데이터 구조를 활용하면 구현 코드는 자연스럽게 나온다.
- 함수를 옮기듯이 클래스의 필드를 옮기면 된다.
문장을 함수로 옮기기
result.push(person.photo.title);
result.concat(photoData(person.photo);
function photoData(aPhoto) {
return [
aPhoto.location,
aPhoto.date.toDateString(),
]
}
result.concat(photoData(person.photo);
function photoData(aPhoto) {
return [
aPhoto.title
aPhoto.location,
aPhoto.date.toDateString(),
]
}
- 중복을 제거하기 위해 문장을 함수에 통합시킨다.
- 중복되는 코드가 많으면 유지보수가 너무 힘들다. 중복을 제거하기 위함이다.
- 추후에 다시 뽑아낼 수도 있다.
문장을 호출한 곳으로 옮기기
emitPhotoData(outStream, person.photo);
function emitPhotoData(outStream, photo){
outStream.write(photo.title);
outStream.write(photo.location);
}
emitPhotoData(outStream, person.photo);
outStream.write(photo.location);
function emitPhotoData(outStream, photo){
outStream.write(photo.title);
}
- 함수의 기능이 다르게 동작하도록 변경되어야 할 때, 추상화의 경계가 달라져야할 때 달라진 동작을 함수에서 꺼낸다.
인라인 코드를 함수 호출로 바꾸기
let appliesToMass = false;
for (const s of states) {
if (s === 'MA') appliesToMass = true;
}
appliesToMass = states.includes('MA');
- 함수를 활용하면 의도가 잘 드러나고 중복을 줄일 수 있다.
- 자바스크립트 내장 메서드들을 많이 알고 있으면 더욱 좋다.
문장 슬라이드하기
const pricingPlan = retrievePricingPlan();
const order = retrieveOrder();
let charge;
const chargePerUnit = pricingPlan.unit;
const pricingPlan = retrievePricingPlan();
const order = retrieveOrder();
const chargePerUnit = pricingPlan.unit;
let charge;
- 관련된 코드들을 가까이 모아두자, 응집도. co-location
- 모든 변수 선언을 함수 첫머리에 모으기 vs 변수를 처음 사용할때 선언하기
- 문의 관점에서는 첫머리에 모으는게 응집도가 높다고 볼 수 있을 것 같다.
- 기능의 관점에서는 기능과 관련된 코드를 가까운데 모아두는 후자가 응집도가 높다고 볼 수 있을 것 같다.
반복문 쪼개기
let averageAge = 0;
let totalSalary = 0;
for (const p of people) {
averageAge += p.age;
totalSalary += p.salary;
}
averageAge = averageAge / people.length;
let averageAge = 0;
for (const p of people) {
averageAge += p.age;
}
let totalSalary = 0;
for (const p of people) {
averageAge += p.age;
}
averageAge = averageAge / people.length;
- 반복문을 수정하는 이유가 두 가지 이기 때문에 반복문도 한 가지 일만 하게 한다.
- 데이터가 같이 쓰이는 경우라면 꼭 쪼개지 않아도 괜찮을 것 같다.
반복문을 파이프라인으로 바꾸기
const names = [];
for (const i of input) {
if(i.job === 'programmer')
names.push(i.name);
}
const names = input
.filter(i => i.job === 'programmer')
.map(i -> i.name);
- 파이프라인을 이용하면 처리 과정을 선언적으로 작성할 수 있다.
- 훨씬 의도를 잘 드러낼 수 있다. 가독성이 좋다.
죽은 코드 제거하기
- 제일 좋은 리팩토링 기법이라고 생각한다.
- 필요없는 코드가 남아있으면 걸림돌만 된다.
데이터 조직화
변수 쪼개기
let temp = 2 * (height + width);
console.log(temp);
temp = height * width;
console.log(temp);
const perimeter = 2 * (height + width);
console.log(perimeter);
const area = height * width;
console.log(area);
- 변수에는 값을 단 한 번만 대입한다, 역할 하나당 변수 하나다.
- 자바스크립트에서는 변수를 선언할 때 const를 사용하자.
필드 이름 바꾸기
class Organization {
get name() {...}
class Organization {
get title() {...}
- 레코드 구조체의 필드 이름은 매우 중요하다.
- 데이터 구조는 무슨 일이 벌어지는지를 이해하는 열쇠다.
- 변수명은 정말 너무나도 중요하다!
파생 변수를 질의 함수로 바꾸기
get discountedTotal() {return this._discountedTotal;}
set discount(aNumber) {
const old = this._discount;
this._discount = aNumber;
this._discountedTotal += old - aNumber;
get discountedTotal() {return this.baseTotal - this._discount;}
set discount(aNumber) {this._discount = aNumber;}
- 가변 데이터의 유효범위를 줄이기 위해 함수로 만들어준다.
- 변수들을 제거하는게 핵심이다.
참조를 값으로 바꾸기
class Product {
applyDiscount(arg) {this._price.amount -= arg;}
class Product {
applyDiscount(arg) {
this._price = new Money(this._price.amount - arg, this._price.currency);
}
- 객체의 속성을 참조로 다루는 경우에는 객체의 속성만 갱신하며, 값으로 다루는 경우에는 새로운 속성을 담은 객체로 기존 내부 객체를 통째로 대체한다.
- 값으로 다루게 되면 불변성을 활용할 수 있다.
- 특정 객체를 여러 객체에서 공유해야하는 경우라면 공유 객체를 참조로 다뤄야 한다.
값을 참조로 바꾸기
let customer = new Customer(customerData);
let customer = customerRepository.get(customerData.id);
- 데이터를 값으로 다루는 경우에 데이터를 갱신해야 하면 모든 값을 갱신해줘야한다.
- 이러한 경우에는 참조로 바꿔주는게 좋다.
- 값이 변경될 때 변경된 값을 사용하고 있는 곳에 모두 적용시켜줘야 하는 문제를 참조로 변경해서 해결했다.
매직 리터럴 바꾸기
const 대주주_최소_지분율 = 2;
const 대주주_최소_주식가치 = 금액['10억'];
/**
* '보유한 주식의 총 가치가 10억원 이상' 또는 '지분율이 2%' 이상인 주주는 대주주로 판단합니다.
*/
export function is대주주({
보유주식수,
주당단가,
주식총발행량,
}: {
보유주식수: number;
주당단가: number;
주식총발행량: number;
}) {
const 주주_주식가치 = 보유주식수 * 주당단가;
const 주주_지분율 = (보유주식수 / 주식총발행량) * 100;
if (주주_주식가치 > 대주주_최소_주식가치 || 주주_지분율 > 대주주_최소_지분율) {
return true;
}
return false;
}
- 예전에 과제를 할 때 썼던 코드, 10억과 2%를 매직 리터럴로 변경해서 사용했다.
- 적용하기 쉬우면서 효과가 큰 리팩토링 기법이라고 생각한다.