[리팩터링 2판] CHAPTER 01. 리팩터링: 첫 번째 예시 (1)

Mijeong (Rachel)
10 min readDec 13, 2021

리팩터링 2판 책을 읽고 공부한 내용을 기록한다. 2판은 자바스크립트를 기반으로 예시를 제공하지만 저자 마틴 파울러는 리팩터링의 핵심 요소는 언어와 상관없이 동일하다고 이야기한다.

해당 기록에서는 책에서 다루는 예제 코드 대신 개인적으로 작성한 예제 코드를 사용한다. 즉, 책의 모든 내용과 순서를 그대로 담지 않을 수 있다.

각 장/절 별로 작성한 코드는 깃허브 개인 저장소에서 관리한다.

불릿 기호는 책의 내용 중 의미있다고 판단한 것을 옮겨 적은 것이다.

1장의 1.1절부터 1.5절까지 학습한 내용을 기록한다. 1장은 예제 코드와 함께 전반적인 리팩터링 진행 절차를 제시한다.

1.1 자, 시작해보자!

예제 코드를 제시한다. 책에서 제공하는 예제 코드 대신, 개인적으로 작성한 예제 코드를 사용한다. 책의 예제와 유사한 패턴이면서 최근에 개인적으로 경험한 코드를 기반으로 작성했다.

createMessage() 함수는 데이터의 유형(text, qna)에 따른 메시지를 반환하는 함수이다.

import {IMessage} from '../data/types';
import moment from 'moment-timezone';
import 'moment/locale/ko';

export default function createMessage (
type: string,
data: string,
userId: number,
channelId: string,
): IMessage {
let content = {};
const now = moment();
const displayDate = moment(now).tz('Asia/Seoul').format('LT');

switch(type) {
case 'text':
content = {
text: data
};
break;
case 'qna':
content = {
qna: data
};
break;
default:
throw new Error(`지원하지 않는 유형: ${type}`);
}

const message: IMessage = {
from: userId,
to: channelId,
contentType: type,
...content,
displayDate,
};

return message;
}

1.2 예시 프로그램을 본 소감

  • 프로그램이 새로운 기능을 추가하기에 편한 구조가 아니라면, 먼저 기능을 추가하기 쉬운 형태로 리팩터링하고 나서 원하는 기능을 추가한다.

1.3 리팩터링의 첫 단계

  • 리팩터링할 코드 영역을 꼼꼼하게 검사해줄 테스트 코드들부터 마련해야 한다.
  • 리팩터링하기 전에 제대로 된 테스트부터 마련한다. 테스트는 반드시 자가진단하도록 만든다.

createMessage() 함수 예제 코드를 위한 테스트 코드를 작성한다.

import createMessage from './createMessage';

import textData from '../data/test/text.data.json';
import qnaData from '../data/test/qna.data.json';
import unknownData from '../data/test/unknown.data.json';

describe('createMessage: 데이터 유형에 따른 메시지 생성', () => {
test('text 유형일 때, text 프로퍼티가 포함된 메시지 반환', () => {
const message = createMessage(
textData.type,
textData.data,
textData.userId,
textData.channelId,
new Date(textData.now),
);
const expectedMessage = {
from: textData.userId,
to: textData.channelId,
contentType: textData.type,
text: textData.data,
displayDate: '오후 4:24'
};
expect(message).toStrictEqual(expectedMessage);
});

test('qna 유형일 때, qna 프로퍼티가 포함된 메시지 반환', () => {
const message = createMessage(
qnaData.type,
qnaData.data,
qnaData.userId,
qnaData.channelId,
new Date(qnaData.now),
);
const expectedMessage = {
from: qnaData.userId,
to: qnaData.channelId,
contentType: qnaData.type,
qna: qnaData.data,
displayDate: '오후 4:24'
};
expect(message).toStrictEqual(expectedMessage);
});

test('지원하지 않는 유형일 때, 에러 반환', () => {
expect(() => createMessage(
unknownData.type,
unknownData.data,
unknownData.userId,
unknownData.channelId,
new Date(unknownData.now),
)).toThrow(`지원하지 않는 유형: ${unknownData.type}`);
});
});

1.4 statement() 함수 쪼개기

책의 예제 코드에서는 statement() 함수이지만, 나는 createMessage() 함수를 기준으로 책에서 다루는 내용을 적용한다.

함수 추출하기

  • 전체 동작을 각각의 부분으로 나눌 수 있는 지점을 찾는다.

책과 동일하게 switch 문을 선택한다.

import {IMessage} from '../data/types';
import moment from 'moment-timezone';
import 'moment/locale/ko';

export default function createMessage (
type: string,
data: string,
userId: number,
channelId: string,
now: Date,
): IMessage {
const content = contentFor(type); // 추출한 함수 사용
const displayDate = moment(now).tz('Asia/Seoul').format('LT');
const message: IMessage = {
from: userId,
to: channelId,
contentType: type,
...content,
displayDate,
};

return message;

function contentFor(type: string) { // 기존 switch문을 함수로 추출
let result = {};
switch(type) {
case 'text':
result = {
text: data
};
break;
case 'qna':
result = {
qna: data
};
break;
default:
throw new Error(`지원하지 않는 유형: ${type}`);
}
return result;
}
}
  • 이렇게 수정하고 나면 곧바로 테스트해서 실수한 게 없는지 확인한다.
  • 한 가지를 수정할 때마다 테스트하면, 오류가 생기더라도 변경 폭이 작기 때문에 살펴볼 범위도 좁아서 문제를 찾고 해결하기가 훨씬 쉽다. 이처럼 조금씩 변경하고 매번 테스트하는 것은 리팩터링 절차의 핵심이다.

변수 인라인하기

  • 지역 변수를 제거해서 얻는 가장 큰 장점을 추출 작업이 훨씬 쉬워진다는 것이다. 유효범위를 신경 써야 할 대상이 줄어들기 때문이다. 실제로 나는 추출 작업 전에는 거의 항상 지역 변수부터 제거한다.

content 지역 변수를 제거하고 인라인한다.

import {IMessage} from '../data/types';
import moment from 'moment-timezone';
import 'moment/locale/ko';

export default function createMessage (
type: string,
data: string,
userId: number,
channelId: string,
now: Date,
): IMessage {
const displayDate = moment(now).tz('Asia/Seoul').format('LT');
const message: IMessage = {
from: userId,
to: channelId,
contentType: type,
...contentFor(type), // content 변수 인라인
displayDate,
};

return message;

function contentFor(type: string) {
let result = {};
switch(type) {
case 'text':
result = {
text: data
};
break;
case 'qna':
result = {
qna: data
};
break;
default:
throw new Error(`지원하지 않는 유형: ${type}`);
}
return result;
}
}

함수 추출하기 및 변수 인라인하기

inKorean() 함수를 추출하고 변수 인라인하기를 한 번 더 수행한다.

import {IMessage} from '../data/types';
import moment from 'moment-timezone';
import 'moment/locale/ko';

export default function createMessage (
type: string,
data: string,
userId: number,
channelId: string,
now: Date,
): IMessage {
const message: IMessage = {
from: userId,
to: channelId,
contentType: type,
...contentFor(type),
displayDate: inKorean(now), // 추출한 함수 사용 및 displayDate 변수 인라인
};

return message;

function contentFor(type: string) {
let result = {};
switch(type) {
case 'text':
result = {
text: data
};
break;
case 'qna':
result = {
qna: data
};
break;
default:
throw new Error(`지원하지 않는 유형: ${type}`);
}
return result;
}

function inKorean(now: Date) { // 기존 날짜 포맷 변경을 함수로 추출
return moment(now).tz('Asia/Seoul').format('LT');
}
}

1.5 중간 점검: 난무하는 중첩 함수

createMessage() 함수는 메시지를 반환하는 코드만 남았으며, 그 외 로직은 보조 함수들로 분리됐다.

1장은 저자의 의도에 따라 리팩터링을 실제 수행해보는 예제를 먼저 다루고 있다. 리팩터링을 수행하는 전반적인 사고 과정을 먼저 체험해보는 것에 목적을 두고 있다. 핵심은, 리팩터링 해야하는 작업을 작게 나누고, 작은 변경이라도 항상 테스트를 수행할 수 있는 기반을 마련하는 것이다.

--

--