😀 책에서 기억하고 싶은 내용을 써보세요.
- 저자가 가장 많이 사용하는 리팩터링은 ‘**함수 추출하기’**와 ‘**변수 추출하기’**다. 이 두 리팩터링을 반대로 진행하는 ‘함수 인라인하기‘ 와 ‘변수 인라인하기' 도 자주 사용한다.
- 추출은 결국 이름 짓기이며, 코드 이해도가 높아지다 보면 이름을 바꿔야 할 때가 많다.
- 함수 선언 바꾸기는 함수의 이름을 변경할 때 많이 쓰인다.
- 함수의 인수를 추가하거나 제거할 때도 이 리팩터링을 적용한다.
- 바꿀 대상이 변수라면 ‘변수 이름 바꾸기’ 를 사용하는데, 이는 ‘변수 캡슐화하기’ 와 관련이 깊다.
- 자주 함께 뭉쳐 다니는 인수들은 ‘매개변수 객체 만들기’ 를 적용해 객체 하나로 묶으면 편리할 때가 많다.
6.1 함수 추출하기(Extract Function)
function printOwing(invoice) {
printBanner();
let outstanding = calculateOutstanding();
// 세부 사항 출력
console.log(`고객명: ${invoice.customer}`);
console.log(`채무액: ${outstanding}`);
}
function printOwing(invoice) {
printBanner();
let outstanding = calculateOutstanding();
printDetails();
function printDetails(outstanding) {
console.log(`고객명: ${invoice.customer}`);
console.log(`채무액: ${outstanding}`);
}
}
- 코드를 언제 독립된 함수로 묶어야 하는지?
- 길이를 기준으로 삼는 것
- 재사용성을 기준으로 삼는 것
- 저자는 ‘목적과 구현을 분리’ 하는 방식을 합리적이라고 생각하고 있다. ⇒ 이렇게 해두면 나중에 코드를 다시 맇을 때 함수의 목적이 눈에 확 들어온다. ⇒ 본문 코드(그 함수가 목적을 이루기 위해 구체적으로 수행하는 일)에 대해서는 더 이상 신경 쓸 일이 거의 없다.
- 저자의 경험상 함수 안에 들어갈 코드가 대여섯 줄을 넘어갈 때부터 슬슬 냄새를 풍기기 시작했고, 단 한 줄짜리 함수를 만드는 일도 적지 않았다.
- 함수를 짧게 만들면 함수 호출이 많아져서 성능이 느려질까 걱정하는 사람도 있다.
- 저자가 젊던 시절에는 간혹 문제가 되긴 했지만 요즘은 그런 일이 거의 없다.
- 함수가 짧으면 캐싱하기 더 쉽기 때문에 컴파일러가 최적화하는 데 유리할 때가 많다.
- 성능 최적화에 대해서는 항상 일반 지침을 따르도록 하자.
- 이러한 짧은 함수의 이점은 이름을 잘 지어야만 발휘되므로 이름 짓기에 특별히 신경 써야 한다.
- 이름을 잘 짓기까지는 어느 정도 훈련이 필요하다.
절차
- 함수를 새로 만들고 목적을 잘 드러내는 이름을 붙인다(’어떻게’가 아닌 ‘무엇을’ 하는지가 드러나야 한다).
- 추출할 코드를 원본 함수에서 복사하여 새 함수에 붙여넣는다.
- 추출한 코드 중 원본 함수의 지역 변수를 참조하거나 추출한 함수의 유효범위를 벗어나는 변수는 없는지 검사한다. 있다면 매개변수로 전달한다.
- 변수를 다 처리했다면 컴파일한다.
- 원본 함수에서 추출한 코드 부분을 새로 만든 함수를 호출하는 문장으로 바꾼다.(즉, 추출한 함수로 일을 위임한다.)
- 테스트한다.
- 다른 코드에 방금 추출한 것과 똑같거나 비슷한 코드가 없는지 살핀다. 있다면 방금 추출한 새 함수를 호출하도록 바꿀지 검토한다.(인라인 코드를 함수 호출로 바꾸기)
예시: 유효범위를 벗어나는 변수가 없을 때
리팩터링 전
function printOwing(invoice) {
let outstanding = 0;
console.log("*****************");
console.log("*** 고객 채무 ***");
console.log("*****************");
// 매해결 채무(outstanding)를 계산한다.
for (const o of invoice.orders) {
outstanding += o.amount;
}
// 마감일(dueDate)을 기록한다.
const today = Clock.today;
invoice.dueDate = new Date(today.getFullyear(), today.getMonth(),
today.getDate() + 30);
// 세부 사항을 출력한다.
console.log(`고객명: ${invoice.customer}`);
console.log(`채무액: ${outstanding}`);
console.log(`마감일: ${invoice.dueDate.toLocalDateString()}`);
}
리팩터링 후
function printOwing(invoice) {
let outstanding = 0;
printBanner(); // <-- 배너 출력 로직을 함수로 추출
// 미해결 채무(outstanding)를 계산한다.
for (const o of invoice.orders) {
outstanding += o.amount
}
...
// 세부 사항을 출력한다.
console.log(`고객명: ${invoice.customer}`);
console.log(`채무액: ${outstanding}`);
console.log(`마감일: ${invoice.dueDate.toLocalDateString()}`);
function printBanner() {
console.log("*****************");
console.log("*** 고객 채무 ***");
console.log("*****************");
}
}
리팩터링 후 2
function printOwing(invoice) {
let outstanding = 0;
printBanner();
....
printDetails(); // <-- 세부 사항 출력 로직을 함수로 추출
function printDetails() {
console.log("*****************");
console.log("*** 고객 채무 ***");
console.log("*****************");
}
}
예시: 지역 변수를 사용할 때
function printOwing(invoice) {
let outstanding = 0;
printBanner();
// 미해결 채무(outstanding)를 계산한다.
for (const o of invoice.orders) {
outstanding += o.amount;
}
// 마감일(dueDate)을 기록한다.
const today = Clock.today;
invoice.dueDate = new Date(today.getFullYear(), today.getMonth(),
today.getDate() + 30);
// 세부 사항을 출력한다.
console.log(`고객명: ${invoice.customer}`);
console.log(`채무액: ${outstanding}`);
console.log(`마감일: ${invoice.dueDate.toLocalDateString()}`);
}
function printOwing(invoice) {
let outstanding = 0;
printBanner();
// 미해결 채무(outstanding)를 계산한다.
for (const o of invoice.orders) {
outstanding += o.amount;
}
// 마감일(dueDate)을 기록한다.
const today = Clock.today;
invoice.dueDate = new Date(today.getFullYear(), today.getMonth(),
today.getDate() + 30);
printDetails(invoice, outstanding); // <--- 앞의 예와 달리 지역 변수를 매개변수로 전달
}
function printDetails(invoice, outstanding) {
console.log(`고객명: ${invoice.customer}`);
console.log(`채무액: ${outstanding}`);
console.log(`마감일: ${invoice.dueDate.toLocalDateString()}`);
}
- 지역 변수와 관련하여 가장 간단한 경우는 변수를 사용하지만 다른 값을 다시 대입하지는 않을 때다. 이 경우에는 지역 변수들을 그냥 매개변수로 넘기면 된다.
function printOwing(invoice) {
let outstanding = 0;
printBanner();
// 미해결 채무(outstanding)를 계산한다.
for (const o of invoice.orders) {
outstanding += o.amount;
}
recordDueDate(invoice); // <--- 마감일 설정 로직을 함수로 추출
printDetails(invoice, outstanding);
}
function recordDueDate(invoice) {
const today = Clock.today;
invoice.dueDate = new Date(today.getFullYear(), today.getMonth(),
today.getDate() + 30);
}
- 지역 변수가 (배열, 레코드, 객체와 같은) 데이터 구조라면 똑같이 매개변수로 넘긴 후 필드 값을 수정 할 수 있다.
예시: 지역 변수의 값을 변경할 때
지역 변수에 값을 대입하게 되면 문제가 복잡해진다.
⇒ 만약 매개변수에 값을 대입하는 코드를 발견하면 곧바로 그 변수를 쪼개서 임시 변수를 새로 하나 만들어 그 변수에 대입하게 한다.
만약 변수가 초기화 되는 지점과 실제로 사용되는 지점이 떨어져 있다면 문장 슬라이드하기를 활용하여 변수 조작을 모두 한곳에 처리하도록 모아두면 편하다.
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();
// 미해결 채무(outstanding)를 계산한다.
let outstanding = 0; // <--- 맨 위에 있던 선언문을 이 위치로 이동
for (const o of invoice.orders) {
outstanding += o.amount;
}
recordDueDate(invoice);
printDetails(invoice, outstanding);
}
- 그런 다음 추출할 부분을 새로운 함수로 복사한다.
function printOwing(invoice) {
printBanner();
// 미해결 채무(outstanding)를 계산한다.
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; // <--- 수정된 값 반환
}
- outstanding의 선언문을 추출할 코드 앞으로 옮겼기 때문에 매개변수로 전달하지 않아도 된다. 추출한 코드에서 값이 변경된 변수는 outstanding뿐이다.
- 내 자바스크립트 환경은 컴파일해도 아무런 값을 출력하지 않는다. 4 단계는 스킵
- 추출한 코드의 원래 자리를 새로 뽑아낸 함수를 호출하는 문장으로 교체한다. 추출한 함수에서 새 값을 반환하니, 이 값을 원래 변수에 저장한다.
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;
}
🤔 오늘 읽은 소감은? 떠오르는 생각을 가볍게 적어보세요.
- 저자처럼 상당한 실력의 개발자도 단계를 나눠서 리팩터링 한다는 것을 알게되었습니다.
- 리팩터링을 제가 예전에 작성했던 코드를 바탕으로 진행해야 겠다고 생각했습니다.
- 실제로도 이렇게 수정하면서 리팩터링을 실천하고 있습니다.
🔎 궁금한 내용이 있거나, 잘 이해되지 않는 내용이 있다면 적어보세요.
- 책에서는 최적화를 할 때 하지마라와 아직 하지 마라 고 했는데 이유가 궁금합니다.
- 최적화를 하면서 코드가 더러워지면 오히려 마이너스가 되서 그렇지 않을까 생각했습니다.
- 실제로 서비스중인 코드를 리팩터링 할려면?
- 이에대한 내용을 확인해보니 해당 회사의 담당 서비스에서 업무를 3년 이상해도 진행하기 힘들다고 한다.
- 섣불리 리팩터링을 하다가 잘 돌아가는 서비스가 망가질 수 있기 때문에 상당히 위험한 행동이라고 한다.
반응형