본문 바로가기
독서/리팩터링

[리팩터링] 챕터06. 기본적인 리팩터링(6 - 1 함수 추출하기)

by 공부하는개미 2022. 5. 26.

 

😀 책에서 기억하고 싶은 내용을 써보세요.

  • 저자가 가장 많이 사용하는 리팩터링은 ‘**함수 추출하기’**와 ‘**변수 추출하기’**다. 이 두 리팩터링을 반대로 진행하는 ‘함수 인라인하기‘ 와 ‘변수 인라인하기' 도 자주 사용한다.
  • 추출은 결국 이름 짓기이며, 코드 이해도가 높아지다 보면 이름을 바꿔야 할 때가 많다.
  • 함수 선언 바꾸기는 함수의 이름을 변경할 때 많이 쓰인다.
    • 함수의 인수를 추가하거나 제거할 때도 이 리팩터링을 적용한다.
  • 바꿀 대상이 변수라면 ‘변수 이름 바꾸기’ 를 사용하는데, 이는 ‘변수 캡슐화하기’ 와 관련이 깊다.
  • 자주 함께 뭉쳐 다니는 인수들은 ‘매개변수 객체 만들기’ 를 적용해 객체 하나로 묶으면 편리할 때가 많다.

 

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}`);
	}
}
  • 코드를 언제 독립된 함수로 묶어야 하는지?
    • 길이를 기준으로 삼는 것
    • 재사용성을 기준으로 삼는 것
    • 저자는 ‘목적과 구현을 분리’ 하는 방식을 합리적이라고 생각하고 있다. ⇒ 이렇게 해두면 나중에 코드를 다시 맇을 때 함수의 목적이 눈에 확 들어온다. ⇒ 본문 코드(그 함수가 목적을 이루기 위해 구체적으로 수행하는 일)에 대해서는 더 이상 신경 쓸 일이 거의 없다.
  • 저자의 경험상 함수 안에 들어갈 코드가 대여섯 줄을 넘어갈 때부터 슬슬 냄새를 풍기기 시작했고, 단 한 줄짜리 함수를 만드는 일도 적지 않았다.
  • 함수를 짧게 만들면 함수 호출이 많아져서 성능이 느려질까 걱정하는 사람도 있다.
    • 저자가 젊던 시절에는 간혹 문제가 되긴 했지만 요즘은 그런 일이 거의 없다.
    • 함수가 짧으면 캐싱하기 더 쉽기 때문에 컴파일러가 최적화하는 데 유리할 때가 많다.
    • 성능 최적화에 대해서는 항상 일반 지침을 따르도록 하자.
  • 이러한 짧은 함수의 이점은 이름을 잘 지어야만 발휘되므로 이름 짓기에 특별히 신경 써야 한다.
    • 이름을 잘 짓기까지는 어느 정도 훈련이 필요하다.

 

절차

  1. 함수를 새로 만들고 목적을 잘 드러내는 이름을 붙인다(’어떻게’가 아닌 ‘무엇을’ 하는지가 드러나야 한다).
  2. 추출할 코드를 원본 함수에서 복사하여 새 함수에 붙여넣는다.
  3. 추출한 코드 중 원본 함수의 지역 변수를 참조하거나 추출한 함수의 유효범위를 벗어나는 변수는 없는지 검사한다. 있다면 매개변수로 전달한다.
  4. 변수를 다 처리했다면 컴파일한다.
  5. 원본 함수에서 추출한 코드 부분을 새로 만든 함수를 호출하는 문장으로 바꾼다.(즉, 추출한 함수로 일을 위임한다.)
  6. 테스트한다.
  7. 다른 코드에 방금 추출한 것과 똑같거나 비슷한 코드가 없는지 살핀다. 있다면 방금 추출한 새 함수를 호출하도록 바꿀지 검토한다.(인라인 코드를 함수 호출로 바꾸기)

 

 

예시: 유효범위를 벗어나는 변수가 없을 때

리팩터링 전

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);
}
  1. 그런 다음 추출할 부분을 새로운 함수로 복사한다.
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;  // <--- 수정된 값 반환
}
  1. outstanding의 선언문을 추출할 코드 앞으로 옮겼기 때문에 매개변수로 전달하지 않아도 된다. 추출한 코드에서 값이 변경된 변수는 outstanding뿐이다.
  2. 내 자바스크립트 환경은 컴파일해도 아무런 값을 출력하지 않는다. 4 단계는 스킵
  3. 추출한 코드의 원래 자리를 새로 뽑아낸 함수를 호출하는 문장으로 교체한다. 추출한 함수에서 새 값을 반환하니, 이 값을 원래 변수에 저장한다.
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년 이상해도 진행하기 힘들다고 한다.
    • 섣불리 리팩터링을 하다가 잘 돌아가는 서비스가 망가질 수 있기 때문에 상당히 위험한 행동이라고 한다.

 

 

반응형