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

1.6 계산 단계와 포맷팅 단계 분리하기

책에서는 statement() 함수 결과값의 포맷을 텍스트 버전과 HTML 버전 모두 지원하는 것으로 계획한다. 각 포맷을 위해 statement() 함수 내의 계산 로직이 중복 생성되는 것이 아닌, 동일한 함수를 사용할 수 있도록 개선하고자 한다.

함수 추출하기

  • 결과 데이터 포맷팅 하는 코드를 별도 함수로 추출한다.
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 {

return renderObject(type, data, userId, channelId, now); // 결과 Object 생성 전체를 별도 함수로 추출

function renderObject( // 결과 Object 생성 전체를 별도 함수로 추출
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),
};

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');
}
}

중간 데이터 역할 Object 생성

  • 두 단계 사이의 중간 데이터 구조 역할을 할 Object를 생성하여 추출한 함수의 인자로 전달한다.
  • 중간 데이터 구조로 옮기면, 계산 관련 코드는 전부 statement() 함수(나는 createMessage() 함수) 로 모으고 renderPlainText()(나는 renderObject())는 매개변수로 전달된 데이터만 처리하게 만들 수 있다.
import {IMessage, IMessageParam} 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 param: IMessageParam = { // 중간 데이터 생성
userId,
channelId,
type,
content: {...contentFor(type, data)},
displayDate: inKorean(now),
};

return renderObject(param); // 중간 데이터를 인수로 전달

function renderObject(param: IMessageParam): IMessage {
const message: IMessage = {
from: param.userId,
to: param.channelId,
contentType: param.type,
...param.content,
displayDate: param.displayDate
};

return message;
}

function contentFor(type: string, data: 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');
}
}

중간 데이터 처리에 해당하는 코드, 별도 함수로 분리

중간 데이터 Object를 처리하는 코드를 createMessageParam() 함수로 분리하고, createMessage() 함수에서는 createMessageParam() 함수를 사용한다.

// createMessage.ts 파일import {IMessage, IMessageParam} from '../data/types';
import createMessageParam from './03_separate_function_createMessageParam';
import 'moment/locale/ko';

export default function createMessage (
type: string,
data: string,
userId: number,
channelId: string,
now: Date,
): IMessage {

return renderObject(createMessageParam(type, data, userId, channelId, now)); // 중간 데이터 계산 및 생성 함수 사용

function renderObject(param: IMessageParam): IMessage {
const message: IMessage = {
from: param.userId,
to: param.channelId,
contentType: param.type,
...param.content,
displayDate: param.displayDate
};

return message;
}
}
// createMessageParam.ts 파일import {IMessageParam} from '../data/types';
import moment from 'moment-timezone';
import 'moment/locale/ko';

export default function createMessageParam ( // 중간 데이터 계산 및 생성 단계를 별도 함수로 추출
type: string,
data: string,
userId: number,
channelId: string,
now: Date,
): IMessageParam {
const param: IMessageParam = {
userId,
channelId,
type,
content: {...contentFor(type, data)},
displayDate: inKorean(now),
};

return param;

function contentFor(type: string, data: 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.7 중간 점검: 두 파일(과 두 단계)로 분리됨

이제 계산 단계(createMessageParam())와 포맷팅 단계(renderObject())로 명확히 분리됐다.

  • 나는 ‘항시 코드베이스를 작업하기 전보다 더 건강하게 고친다’라는 캠핑 규칙의 변형 버전을 적용한다. 완벽하지는 않더라도, 분명 더 나아지게 한다.

1.8 다형성을 활용해 계산 코드 재구성하기

계산 단계를 다시 살펴보면, type에 따라 로직이 달라진다는 것을 확인할 수 있다. 나의 예제 코드에서는 type이 text 혹은 qna인지에 따라 처리 로직이 달라진다. 그리고 type에는 다른 유형이 추가될 여지가 충분하다.

  • 조건부 로직을 명확한 구조로 보완하는 방법은 다양하지만, 여기서는 객체지향의 핵심 특성인 다형성(polymorphism)을 활용하는 것이 자연스럽다.

클래스 생성

우선 type에 따라 처리하는 로직을 별도 클래스로 분리한다.

class ContentGenerator { // content 생성 클래스
type: string;
data: string;

constructor(type: string, data: string) {
this.type = type;
this.data = data;
}

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

함수 인라인

생성한 클래스의 함수를 직접 호출하도록 수정한다.

export default function createMessageParam (
type: string,
data: string,
userId: number,
channelId: string,
now: Date,
): IMessageParam {
const generator = new ContentGenerator(type, data);
const param: IMessageParam = {
userId,
channelId,
type,
content: {...generator.content}, // content 생성 클래스의 함수 인라인
displayDate: inKorean(now),
};

return param;

function inKorean(now: Date) {
return moment(now).tz('Asia/Seoul').format('LT');
}
}

다형성 적용

  • 타입(type)에 따른 코드 대신 서브클래스를 사용하도록 변경하는 것
import {IMessageParam} from '../data/types';
import moment from 'moment-timezone';
import 'moment/locale/ko';

class ContentGenerator { // content 생성 클래스
type: string;
data: string;

constructor(type: string, data: string) {
this.type = type;
this.data = data;
}
}

class TextGenerator extends ContentGenerator { // text 유형을 위한 서브클래스
get content() {
return {
text: this.data
};
}
}

class QnaGenerator extends ContentGenerator { // qna 유형을 위한 서브클래스
get content() {
return {
qna: this.data
};
}
}

function createContentGenerator(type: string, data: string) { // 팩터리 함수
switch(type) {
case 'text':
return new TextGenerator(type, data);
case 'qna':
return new QnaGenerator(type, data);
default:
throw new Error(`지원하지 않는 유형: ${type}`);
}
}

export default function createMessageParam (
type: string,
data: string,
userId: number,
channelId: string,
now: Date,
): IMessageParam {
const generator = createContentGenerator(type, data); // 팩터리 함수를 통해 서브클래스 인스턴스 반환
const param: IMessageParam = {
userId,
channelId,
type,
content: {...generator.content},
displayDate: inKorean(now),
};

return param;

function inKorean(now: Date) {
return moment(now).tz('Asia/Seoul').format('LT');
}
}

1.9 상태 점검: 다형성을 활용하여 데이터 생성하기

  • 이번 수정으로 나아진 점은 타입별 계산 코드들을 서브클래스에 묶어뒀다는 것이다.
  • 같은 타입의 다형성을 기반으로 실행되는 함수가 많을수록 이렇게(a.k.a. 서브클래스) 구성하는 쪽이 유리하다.

1.10 마치며

  • 1장에서는 리팩터링을 크게 세 단계로 진행했다. 원본 함수를 중첩 함수 여러 개로 나누기, 계산 코드와 포맷팅 코드 분리하기, 계산 로직을 다형성으로 표현하기. 단계가 지날 때 마다 코드가 수행하는 일이 더욱 분명하게 드러났다.
  • 좋은 코드를 가늠하는 확실한 방법은 ‘얼마나 수정하기 쉬운가’다.
  • 리팩터링을 효과적으로 하는 핵심은, 단계를 잘게 나눠야 더 빠르게 처리할 수 있고, 코드는 절대 깨지지 않으며, 이러한 작은 단계들이 모여서 상당히 큰 변화를 이룰 수 있다는 사실을 깨닫는 것이다.

--

--

programming x writing

Love podcasts or audiobooks? Learn on the go with our new app.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store