일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- 운영체제
- 자바
- 자바스크립트
- react 기초
- useState
- codestates
- 알고리즘
- 파이썬
- Zerobase
- REACT
- Operating System
- 자료구조
- 비동기
- node.js
- algorithm
- 컴퓨터공학
- OS
- context switching
- Python
- 개발공부
- execution context
- Computer Science
- JavaScript
- typeScript
- java
- 파이썬 알고리즘 인터뷰
- 코드스테이츠
- 프로그래머스
- 글또
- python algorithm
- Today
- Total
Back to the Basics
[REFACTORING - 2] CH01 정리 및 리뷰 본문
리팩터링 2판이 나왔다고 해서 사놓고 책장에만 꽂아놓은지 1년이 지났다.
요즘 좋은 코드란 무엇인지, 무엇이 읽기 좋은 코드인지, 코드 정리를 하긴 하는데 제대로 하고 있는 것이 맞는지 등의 생각을 하게 되었고 책장에 장식용으로 사용되고 있던 이 책을 발견하게 되었다. 가볍게 누워서 읽어보다 보니 정리를 하면서 읽으면 더 좋을 것 같아 정리를 시작하게 되었다.
물론 항상 정답이 있는 것은 아니지만, 참고하기 좋은 책이라는 생각이 든다. 기회가 된다면 관련 스터디를 만들어서 추진해보아도 좋지 않을까 싶다.
이 책은 총 12장까지 있고 01~04 까지는 리팩터링 원칙의 전반적인 이야기, 리팩터링의 필요성 등을 이야기 하고 05장~12장 은 핵심이라고 할 수 있는 마틴파울러의 리팩터링 기법들이 소개된다.
이번 포스팅에서는 1장에 대한 늦은 리뷰(이미 6장을 읽고있다)이다. 복기를 위해 정리하는 의미로 정리를 해보았고 중간중간 개인적인 의견도 있는 리뷰이다.
1장. 리택터링 : 첫 번째 예시
이 책의 첫 장은 다른 책들과는 다르게 간단한 코드에 대한 리팩터링 예제를 소개한다. 예제를 소개하면서 리팩터링이 무엇인지 맛보기로 보여주는 챕터이다. 개인적으로 나쁘지 않은 방식이라고 생각한다. 초반엔 좀 지루할 수 있는 배경, 원칙을 먼저 설명하는 것보다는 간단한 예시를 들어서 전체적인 맥락을 겪어보고 배경, 원칙을 들어가는 것이 더 이해가 잘 될 때가 있었기 때문이다.
1-3 리팩터링의 첫 단계 - 자가진단하는 테스트코드
본격적으로 들어가기 전에, 리팩터링의 첫 단계로 테스트 코드를 강조한다. 사람은 언제나 실수를 할 수 있고 프로그램이 클 수록, 코드가 복잡할수록 그 확률은 커진다. 그리고 테스트는 반드시 성공/실패를 스스로 판단하는 자가진단 하도록 만들어야 한다고 강조한다. (테스트와 관련된 부분은 4장에서 조금 더 다룬다) 테스트 코드의 필요성은 매우 공감한다.
"리팩터링은 프로그램 수정을 작은 단계로 나눠서 진행한다. 이를 통해 중간에 실수를 하더라도 버그를 쉽게 찾을 수 있다"
작가는 리팩터링을 할 때 단계를 매우 작게 나누고, 나눈 단계별로 컴파일 하고 테스트를 하고 커밋을 한다. 변수 이름 하나 바꿀 때에도 진행을 한다.
나도 개발을 할 때 작업의 단위를 나누어서 단계별로 진행하곤 한다. 보통 커밋을 하는 범위는 기능별로 커밋을 하거나 히스토리를 파악하기 쉬운 정도로 하곤 했다. 이 책에서 말하는 단계는 이보다 더 작은 단위이다. 이 방법을 한번 따라 해봤는데 사실 쉽지는 않았다. 코드를 작성하다 보면 나눠놓은 단계의 범위를 넘어가는 경우가 많았던 기억이 있다. 그런데 이렇게 함으로써 코드를 수정하면서 발생할 수 있는 버그 유무를 미리 확인할 수 있어 마음은 좀 편했다
리팩터링 할 예제 코드는 아래와 같다.
function statement(invoice, plays) {
let totalAmount = 0;
let volumeCredits = 0;
let result = `청구 내역 (고객명: ${invoice.customer})\n`;
const format = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
minimumFractionDigits: 2,
}).format;
for (let perf of invoice.performances) {
const play = plays[perf.playID];
let thisAmount = 0;
switch (play.type) {
case "tragedy": // 비극
thisAmount = 4_000;
if (perf.audience > 30) {
thisAmount += 1000 * (perf.audience - 30);
}
break;
case "comedy": // 희극
thisAmount = 30000;
if (perf.audience > 20) {
thisAmount += 10000 + 500 * (perf.audience - 20);
}
thisAmount += 300 * perf.audience;
break;
default:
throw new Error(`알 수 없는 장르: ${play.type}`);
}
// 포인트를 적립한다
volumeCredits += Math.max(perf.audience - 30, 0);
// 희극 관객 5명마다 추가 포인트를 제공한다
if ("comedy" === play.type) volumeCredits += Math.floor(perf.audience / 5);
// 청구 내역을 출력한다
result += `${play.name}: ${format(thisAmount / 100)} (${perf.audience}석)\n`;
totalAmount += thisAmount;
}
result += `총액 ${format(totalAmount / 100)}\n`;
result += `적립 포인트 ${volumeCredits}점\n`;
return result;
}
위의 코드는 연극에 대한 비용계산을 하고, 청구내역을 출력(포멧팅)하는 기능을 한다. 중간에 적립포인트 계산 등의 기능도 포함되어 있다.
여기에 희극, 비극 외의 다른 type이 생긴다면? 출력하는 형식을 다르게 해 달라는 요구사항이 생긴다면? 조건문과 스위치문 추가로도 기능자체는 구현할 수 있지만 코드는 더욱 복잡해지는 것은 당연한 일이다. 위의 코드는 나름 짧다고 할 수 있겠지만 복잡한 비즈니스를 갖고 있다면 코드를 파악하는 것이 점점 어려워진다. 이런 이유로 리팩터링은 필요하다.
1장에서는 위의 코드를 리팩터링 하는 것을 단계별로 보여준다. 크게 "함수 쪼개기, 단계 분리하기, 다형성을 이용한 코드 재구성" 세 가지 단계를 거쳐 리팩터링 된다.
1-4. 코드 구조 개선 - 함수 쪼개기
리팩터링을 하려면 함수의 논리를 쉽게 파악할 수 있어야 한다. 당연한 이야기겠지만 정리된 코드가 수정도 쉽다. 그 첫 번째 단계로 코드의 구조를 보강하는 것에 집중한다.
statement라는 하나의 함수에서 분리할 수 있는 부분들을 먼저 중첩 함수로 추출 하고 크게 아래의 단계를 따른다
쪼갤 코드를 함수로 추출 -> 매개변수 정리 -> 함수를 코드에 적용 -> 변수 인라인을 통한 임시변수 제거
이런 단계를 거치면서 각 단계별로 컴파일 - 테스트 - 커밋 을 하면서 버그 발생 시 빠른 수정이 가능하도록 한다.
구조 정리 리팩터링의 결과는 아래와 같다. 조금 길기도 하고 분리된 포인트가 중요한 것 같아서 구현은 생략하였다.
전체 코드는 github에서 확인할 수 있다.
function statement(invoice, plays) {
let result = `청구 내역 (고객명: ${invoice.customer})\n`;
for (let perf of invoice.performances) {
result += `${playFor(perf).name}: ${usd(amountFor(perf))} (${perf.audience}석)\n`; // 추출된 함수로 변수 인라인하기
}
result += `총액 ${usd(totalAmount())}\n`; // 변수 인라인하기
result += `적립 포인트 ${totalVolumeCredits()}점\n`; // 변수 인라인하기
return result;
// ---- 여기부터 추출된 중첩함수 ------
function totalAmount() {
// totalAmount변수를 별도의 for문으로 추출하는 반복문 쪼개기, 함수로 추출하기를 통한 totalAmount 임시변수 제거
}
function totalVolumeCredits() {
// volumeCredits를 별도의 for문으로 추출하는 반복분 쪼개기, 함수로 추출하기를 통한 volumeCredits 임시변수 제거
}
function usd(aNumber) {
// format 임시변수를 함수로 추출하기를 통한 format 암시변수 제거
}
function volumeCreditsFor(perf) {
// 포인트 적립 계산 함수 추출
}
// amountFor의 play임시 변수룰 함수로 추출하기를 통한 play 암시변수 제거
function playFor(aPerformance) {
return plays[aPerformance.playID];
}
function amountFor(aPerformance) {
// 공연료 계산 switch문 함수로 추출
}
}
중첩함수로 구현되었지만, 메인 코드는 약 7줄 정도로 줄어들었다.
리팩터링을 하면서 성능상 비효율적인 코드가 될 수도 있다. 예를 들면 totalAmount나 totalVolumeCredits와 같은 함수처럼 기존에 한 번의 반복문에서 처리가 되었던 것을 별도의 반복문으로 추출하게 되는 경우도 있다. 하지만 이런 정도의 리팩터링은 성능에 크게 영향을 미치지 않으며 리팩터링에 의해 성능이 떨어졌다면 리팩터링을 마무리하고 나서 성능을 개선하는 것이 좋다고 책에서는 말한다. 잘 다듬어진 코드가 성능 개선도 효과적으로 수행될 수 있다.
"코드가 복잡할수록 단계를 나누자"
volumeCredits의 경우 아래와 같은 단계를 거쳤다
1. 반복문 쪼개기로 누적시키는 부분 정리 - 컴파일, 테스트, 커밋
// 반복문 별도 분리
for (const perf of invoice.performances) {
volumeCredits += volumeCreditsFor(perf);
}
2. 문장 슬라이드하기로 변수 초기화 문장을 변수 값 누적 코드 바로 앞으로 옮김 - 컴파일, 테스트, 커밋
let volumeCredits = 0; // 초기화 변수 위치 이동
for (const perf of invoice.performances) {
volumeCredits += volumeCreditsFor(perf);
}
3. 함수 추출하기로 적립 포인트 계산 부분을 별도 함수로 추출 - 컴파일, 테스트, 커밋
// 별도 함수로 추출 및 변수 이름 변경
function totalVolumeCredits() {
let result = 0;
for (const perf of invoice.performances) {
result += volumeCreditsFor(perf);
}
return result;
}
4. 변수 인라인하기로 volumeCredits 변수 제거 - 컴파일, 테스트, 커밋
let result = `청구 내역 (고객명: ${invoice.customer})\n`;
for (let perf of invoice.performances) { ... }
result += `총액 ${usd(totalAmount())}\n`;
result += `적립 포인트 ${totalVolumeCredits()}점\n`; // 변수 인라인하기 적용
위와 같이 작게 나누는 것이 조금 힘들 것 같긴 하지만, 리팩터링 중 버그의 원인을 찾기 수월하고 코드가 복잡할수록 단계를 작게 나누면 작업 속도가 빨라지기 때문에 단계를 나눠서 작업하는 것을 책에서는 추천한다
첫 번째 리팩터링을 통해 기능별로 함수로 추출하고 헷갈리기 쉬운 임시변수들을 제거하여 구조를 정리하였다. 다음 단계는 단계 분리하기이다.
1-6. 계산 단계와 포맷팅 단계 분리하기
다음 리팩터링으로 단계 쪼개기를 한다. 현재 statement()에는 크게 두 가지의 기능이 들어가 있다. 1. 데이터를 처리하는 기능 2. 텍스트로 그 결과를 표현하는 기능이 있다. 요구사항 중 하나인 HTML로 렌더링을 하는 것 또한 데이터를 표현하는 기능을 한다.
이번 단계에서는 1.statement() 안에 필요한 데이터를 처리하고 2. 처리 결과를 텍스트 또는 HTML 구조로 표현하는 기능으로 분리한다. 이 두 가지 기능을 서로 분리하여 기능별로 관리를 한다면 다른 랜더링 요구사항이 추가되어도 쉽게 기능추가를 할 수 있다.
리팩터링은 아래와 같은 순서로 진행된다
1. 1-4에서 만들었던 중첩된 함수들의 결과를 받아 객체 변수에 넣는다(중간데이터)
2. 1번의 코드를 별도 함수로 추출한다 (createStatementData)
3. 결과를 포맷팅 하는 코드를 별도의 함수로 추출하고 createStatementData에서 반환하는 중간데이터를 입력으로 받는다
4. 입력으로 중간데이터를 사용하여 포맷팅 하고 출력한다.
일부 생략된 결과 코드는 아래와 같다. 전체 코드는 github에서 볼 수 있다
1. createStatementData.js
module.exports = function createStatementData(invoice, plays) {
const result = {}; // 중간 데이터 구조
result.customer = invoice.customer;
result.performances = invoice.performances.map(enrichPerformance);
result.totalAmount = totalAmount(result);
result.totalVolumeCredits = totalVolumeCredits(result);
return result;
function enrichPerformance(aPerformance) {
const result = Object.assign({}, aPerformance); // 얕은복사
result.play = playFor(result); // performances 중간데이터에 연극 정보 저장
result.amount = amountFor(result); // performances 중간데이터에 공연료 정보 저장
result.volumeCredits = volumeCreditsFor(result);
return result;
}
function playFor(aPerformance) { ... }
function amountFor(aPerformance) { ... }
function volumeCreditsFor(aPerformance) { ... }
function totalAmount(data) { ... }
function totalVolumeCredits(data) { ... }
};
2. statement.js
statement는 기존과 같이 text로 보여주는 방식이고 htmlStatement는 html구조로 랜더링 해주는 방식이다.
각각 createStatementData를 인자로 받고 각 특성에 따라 데이터를 뿌려주는 기능을 한다
const createStatementData = require("./createStatementData");
function statement(invoice, plays) {
return renderPlainText(createStatementData(invoice, plays)); // 변수 인라인하기
}
function hemlStatement(invoice, plays) {
return renderHtml(createStatementData(invoice, plays));
}
function renderHtml(data) { ... }
function renderPlainText(data) { ... }
function usd(aNumber) { ... }
코드의 길이는 리팩토링 전보다 훨씬 많아졌지만, 각 역할별로 모듈화 함으로써 전체적인 과정을 이해하기 쉬워졌다.
이렇게 역할별로 모듈화를 함으로써 새로운 랜더링 방식의 요청이 있어도 createStatementData를 사용해서 계산코드를 중복하지 않아도 된다.
1-8. 다형성을 활용하여 계산 코드 재구성
두 번째 요구사항은 연극장르 추가이다. 맨 위의 코드에서 보았듯이 switch문에서 연극 장르마다 공연료가 적립 포인트 계산 방법이 다르다. 추가적인 장르가 생길 때마다 다른 정책을 갖는 공연료와 적립포인트 로직을 추가하는 것은 그다지 좋지 않아 보인다.
여기서는 이런 조건부 로직은 다형성으로 바꾸기 기법을 사용한다.
공연료 계산과 적립 포인트 계산은 연극 장르마다 다르기 때문에 하나의 하나로 묶는 것이 좋다. 객체지향의 다형성을 이요하여 영과 장르별 서브클래스로 분리하는 방식을 사용할 수 있다.
단계는, 공연료 관련 계산을 책임지는 1. 공연료 계산기 클래스를 생성하고 2. 각 계산 함수들 amountFor()와 volumeCreditsFor()를 계산기 클래스로 옮긴다. 3. 공연료 계산기를 다형성 버전으로 만든다(연극장르에 따른 서브 클래스 생성)
결론적으로 아래와 같이 만들 수 있다. 전체 코드는 github에 구현되어 있다.
- 타입코드를 서브클래스로 바꾸기, 생성자를 팩토리 함수로 바꾸기 등의 리팩터링 기법을 사용하여 다형성 버전으로 만든다
아래는 팩터리 함수를 만들어서 연극장르에 따른 서브클래스의 인스턴스를 반환한다
function createPerformanceCalculator(aPerformance, aPlay) {
switch (aPlay.type) {
case "tragedy":
return new TragdyCalculator(aPerformance, aPlay);
case "comedy":
return new ComedyCalculator(aPerformance, aPlay);
default:
throw new Error(`알 수 없는 장르: ${aPlay.type}`);
}
}
부모 클래스에서 amount, volumeCredit을 조건부로 처리하고 있는데, 이것을 각 서브 클래스에서 각 정책에 맞게 오버라이드한다.
class PerformanceCalculator { ... }
class TragdyCalculator extends PerformanceCalculator {
get amount() { ... }
}
class ComedyCalculator extends PerformanceCalculator {
get amount() { ... }
get volumeCredits() { ... }
}
이번 리팩터링을 통해 코드가 더 길어지만 장르가 또 추가된다면 서브클래스를 추가하고 팩토리 함수에 추가해 주면 된다. 처음 코드와 비교해 보았을 때 코드의 유지보수성, 확장성은 더욱 좋아졌다는 것을 알 수 있다.
마치며
리팩터링의 핵심 과정
이 장에서는 리팩터링의 전체적인 흐름을 보여주었다. 크게 세 단계로 진행되었는데, 먼저 전반적인 구조를 개선하고, 다음으로 기능별 분리를 위한 단계 쪼개기를 수행하며, 마지막으로 다형성을 활용한 개선이 이루어졌다. 이 과정에서 '함수 추출하기', '변수 인라인하기', '조건부 로직을 다형성으로 바꾸기' 등 다양한 리팩터링 기법들이 활용되었다. 이러한 기법들은 6장 이후에서 더 자세히 다루어진다.
좋은 코드의 기준
이 장의 핵심 메시지는 "좋은 코드를 가늠하는 확실한 방법은 '얼마나 수정하기 쉬운가'이다" 라는 점이다. 이는 실제 개발 현장에서 매우 중요한 의미를 갖는다. 운영 중인 서비스에서는 언제든 문제가 발생할 수 있으며, 이를 신속하게 파악하고 해결해야 한다. 코드가 복잡하고 이해하기 어렵다면 문제 해결에 더 많은 시간과 비용이 소요된다. 특히 담당자가 부재중이거나 다른 개발자들이 바쁜 상황에서도, 누구나 코드를 이해하고 수정할 수 있어야 한다. 따라서 읽기 쉽고 수정하기 쉬운 코드는 단순히 개발자의 편의를 넘어 생산성, 서비스 품질, 나아가 회사의 이익과 직결되는 중요한 요소라고 할 수 있다.
핵심 takeaways
이 장에서 특히 기억해야 할 두 가지 핵심 원칙은 다음과 같다:
1. 자가진단하는 테스트 코드 작성
- 리팩터링의 안전성을 보장하는 기본 토대
- 코드 변경 시 즉각적인 피드백 제공
2. 복잡한 코드는 단계적 접근
- 작은 단위로 나누어 진행
- 각 단계마다 컴파일-테스트-커밋 사이클 준수
- 안정적이고 추적 가능한 리팩터링 보장
적어도 위의 두 가지 원칙은 코드를 작성할 때 기억해두면 좋을 것 같다.
'Books & Reviews' 카테고리의 다른 글
데이터 중심 애플리케이션 설계 - 3장 저장소와 검색 (0) | 2023.05.02 |
---|