Skip to main content

리팩터링 가이드 - 좋은 코딩 습관을 기르자

· 15 min read
한혜경

안녕하세요, 비브로스 웹 프론트엔드팀 한혜경입니다~! 😄

비브로스에서는 무제한으로 도서구입비를 지원해주시는데요,

이번에 개발 서적의 스테디셀러! “리팩터링”이라는 책을 신청해서 열심히 읽고 공유하고 싶은 내용들을 정리해보았습니다.


출처: yes24

출처: yes24

리팩터링 1판은 java로 되어있고, 제가 본 2판은 javascript로 되어 있어서 익숙한 언어로 예시를 확인할 수 있어 편하게 읽을 수 있었습니다.

(하지만 java, javascript만 적용할 수 있는 방법을 소개한 책이 아니에요~!!)

그럼 리팩터링을 왜? 언제? 어떻게? 해야 하는지 함께 알아보시죠~!

📚리팩터링이란?

소프트웨어의 겉보기 동작은 그대로 유지한 채, 코드를 이해하고 수정하기 쉽도록 내부 구조를 변경하는 기법

  • 출처: 리팩터링 2판

쉽게 말해 기능은 그대로~ 두고 코드를 가독성 있게, 유지 보수하기 쉽도록 수정하는 것을 말합니다.

여기서 중요한 건 기능을 그대로 둔다는 것!

리팩터링에서 금지하는 몇 가지가 있습니다.

  1. 기능 변경/추가 금지!
  2. 버그 수정 금지!
  3. 성능 개선 금지!
  4. 버전 업데이트 금지!

금지금지!! 허락 못 해!

금지금지!! 허락 못 해!

누군가가 리팩터링하다가 코드가 깨져서 며칠이나 고생했다 라고 한다면, 십중팔구 리팩터링한 것이 아니다.

  • 출처: 리팩터링 2판

리팩터링 시도하다가 버그 발견해서 같이 수정하려고 했던 과거의 나… 반성…

🧐 리팩터링은 왜 해야 하는 건가요?

냄새나면 당장 갈아라

  • 켄트 백 할머니의 육아 원칙


프로젝트 일정이 급해서 일단 기능 구현한 당신!

그 코드가 깨끗한 코드라고 확신할 수 있나요!?

저는 당당하게 말할 수 있습니다!


“아니오”


인라인 코드, 대충 지은 변수명, 일단 욱여넣은 데이터들….

찔리는 부분이 한두 가지가 아닙니다.


💩 코드에서 나는 악취

책에서는 “코드에서 나는 악취”라고 명명하는데요,

어떤 악취들이 있는지 알아봅시다.

(코드, 모듈 기준으로 너무 많은 악취들이 있어서 그 중 중요하다고 생각하는 악취만 뽑아봤어요!)

  • 기이한 이름 (Mysterious name)
    • 코드 이해력과 가독성 낮음.
  • 중복 코드 (Duplicated Code)
    • 실수와 에러 발생할 확률 높음.
  • 긴 함수, 긴 클래스, 긴 매개변수 (Long Function / Class / Parameter List)
    • 이해하기 어렵고, 재사용성이 낮음.
    • 잦은 실수 발생 확률 높음.
  • 전역데이터 (Global Data)
    • 어디서든 건드릴 수 있어 버그 발생 시 원인 파악 어려움.
  • 가변 데이터 (Mutable Data)
    • 예상하지 못한 곳에서 데이터가 변경되어 버그 발생.
  • 주석 (Comments)
    • 주석은 필요한 경우에는 코드에 향기를 입힐 수 있다.
    • 탈취제처럼 사용하는 데 문제가 있다.
    • 주석이 장황한 것은 코드를 잘못 작성했을 확률이 높음.

“위와 같은 악취 나는 코드들을 제거하기 위해 한다!”

프로그램은 꾸준히 기능 추가 또는 변경이 생기고,
일단 붙인 기능들은 더럽고, 복잡할 확률이 높아요.

그런 코드들이 뭉칠수록 예상하지 못한 에러가 발생하기 쉽고, 유지보수도 어렵죠.

출처: 리팩터링 2판

출처: 리팩터링 2판

리팩터링을 하면 처음엔 좋지 않은 설계였더라도 점점 좋은 설계가 될 수 있어요.

수정할수록 소프트웨어를 이해하기 쉬워지고,

버그를 쉽게 찾을 수 있게 되죠.

🥲 리팩터링은 언제 하는 거죠?

답은… 수.시.로…

시무룩…

시무룩…

프로그램은 모래성과 같아서 계속 보수해주지 않으면 무너진다고 하죠?

수시로, 시간 날 때, 심심할 때, 틈틈이!

프로젝트 시작 단계, 유지보수 단계, 오래된 프로젝트에서는 이렇게!


  • 프로젝트 시작 단계
    • 기능 구현을 위한 코드 작성 → 테스트 코드 작성
    • 3의 법칙: 비슷한 일을 세 번째 하게 되면 리팩터링
    • 코드리뷰 → 집단지성의 힘으로 코드를 이해하기 쉽게 만들기
  • 프로젝트 유지보수 단계
    • 버그 발견 → 버그를 검증할 수 있는 테스트 코드 작성
      → 코드를 이해하기 쉽게, 변경하기 쉽게 수정 (버그는 그대로 유지)
      → 버그 수정
    • 기능추가 및 디펜던시 마이그레이션
      → 기존 기능들에 대한 테스트 코드 작성
      → 코드를 이해하기 쉽게 수정
      → 기능 추가
  • 오래된 프로젝트
    • 버그 수정 및 기능 추가 시에만 한정적으로 테스트 추가
      → (새로운 코드를 작성하는 것이 빠를 때도 있음)
      → 코드 수정 또는 기능 추가

🤓 리팩터링은 어떻게 해야 해요??

많은 프로젝트들을 진행하고 있는 저희 팀은 리팩터링 진행할 프로젝트들이 산더미 같아요 🤷‍♀️

리팩터링을 시도할 때 좋은 가이드가 되길 바라며 정리해 보았어요~!

기본적인 리팩터링


1. 함수 추출하기

  • 코드를 보았을 때 “무엇”을 하는지 알 수 있도록 함수명을 짓자.
  • 하나의 함수는 하나의 목적을 가지고 한 가지 일만 해야 한다.

Before

interface Invoice {
orders: {
amount: number
}[];
customer: string;
}

function printOwing(invoice: Invoice){
console.log('***********************');
console.log('**** Customer Owes ****');
console.log('***********************');

const outstanding = invoice.orders.reduce((accu, curr) => accu + curr.amount, 0);

console.log(`고객명: ${invoice.customer}`);
console.log(`채무액: ${outstanding}`);
}

After

function printOwing(invoice: Invoice) {
printBanner();
const outstanding = calcOutstanding(invoice);
printDetails(invoice, outstanding);
}

function printBanner(){
console.log('***********************');
console.log('**** Customer Owes ****');
console.log('***********************');
}
function calcOutstanding(invoice: Invoice) {
return invoice.orders.reduce((accu, curr) => accu + curr.amount, 0);
}
function printDetails(invoice: Invoice, outstanding: number){
console.log(`고객명: ${invoice.customer}`);
console.log(`채무액: ${outstanding}`);
}

2. 변수 추출하기

  • 복잡한 표현식은 나누어서 표현한다.
  • 변수명은 문맥에 맞춰 잘 지어야 한다.
  • 단, 그 자체로 의미가 명확히 보인다면 추출하지 말자.

Before

interface Order {
quantity: number;
itemPrice: number;
}

function price(order: Order) {
return order.quantity * order.itemPrice - Math.max(0, order.quantity - 500) * order.itemPrice * 0.05 + Math.min(order.quantity * order.itemPrice * 0.1, 100)
}

After

function price(order: Order) {
const basePrice = order.quantity * order.itemPrice;
const discount = Math.max(0, order.quantity - 500) * order.itemPrice * 0.05;
const shipping = Math.min(order.quantity * order.itemPrice * 0.1, 100);

return basePrice - discount + shipping;
}

3. 매개변수 객체 만들기

  • 관련된 데이터는 데이터 구조 하나로 모아주자.

Before

function isValidTime(startTime, endTime) {
const startWorkTime = moment(startTime, 'HH:mm');
const endWorkTime = moment(endTime, 'HH:mm');

return endWorkTime.isAfter(startWorkTime);
}

After

interface TimeRange {
startTime: string;
endTime: string;
}

function isValidTime(timeRange: TimeRange) {
const { startTime, endTime } = timeRange;
const startWorkTime = moment(startTime, 'HH:mm');
const endWorkTime = moment(endTime, 'HH:mm');

return endWorkTime.isAfter(startWorkTime);
}

기능 이동


1. 문장 슬라이드 하기

  • 관련된 코드들이 가까이 모여있으면 이해하기 쉽다.

Before

function getInitResource() {
let result;
if (availableResources.length === 0) {
result = createResource();
allocatedResources.push(result);
} else {
result = availableResources.pop();
allocatedResources.push(result);
}
return result;
}

After

function getInitResource() {
const result =
availableResources.length === 0
? createResource()
: availableResources.pop();
allocatedResources.push(result);
return result;
}

2. 반복문 쪼개기

  • 하나의 반복문은 하나의 일만 해야 이해하기 쉽고 관리도 쉽다.
  • 성능 최적화는 당장 고려하지 않는다.
  • 성능적으로 문제가 있다면 그때 다시 합치면 된다.

Before

interface People {
age: number;
salary: number;
}

const peopleList = [
{age: 22, salary: 230},
{age: 31, salary: 180},
{age: 28, salary: 340}
]

let youngest = peopleList[0] ? peopleList[0].age : Infinity;

let totalSalary = 0;

for (const people of peopleList) {
if (people.age < youngest) youngest = people.age; // 가장 어린 사람을 찾는 코드
totalSalary += people.salary; // 총급여를 구하는 코드
}

return `최연소: ${youngest}, 총급여 : ${totalSalary}`;

After

function getYoungestAndTotalSalary(peopleList: People[]) {
function totalSalary() {
let totalSalary = 0;
for (const people of peopleList) {
totalSalary += people.salary;
}
return totalSalary;
}

function youngestAge() {
let youngest = peopleList[0] ? peopleList[0].age : Infinity;
for (const people of peopleList) {
if (people.age < youngest) youngest = people.age;
}

return youngest;
}

return `최연소: ${youngestAge()}, 총급여 : ${totalSalary()}`;
}

============= totalSalary, youngestAge 더 다듬기 ================
function totalSalary() {
return peopleList.reduce((total, people) => total + people.salary, 0);
}
function youngestAge() {
return Math.min(...peopleList.map((people) => people.age));
}

조건부 로직 간소화


1. 조건문 분해하기

  • 긴 조건문은 의도를 드러낼 수 있는 함수로 추출하여, 로직을 명확히 하자.

Before

let charge;
if (!date.isBefore(plan.summerStart) && !date.isAfter(plan.summerEnd)) {
charge = quantity * plan.summerRate
} else {
charge = quantity * plan.regularRate + plan.regularServiceCharge
}

After

function calcChargeByDate(date, plan, quantity){
const isSummer = !date.isBefore(plan.summerStart) && !date.isAfter(plan.summerEnd);
const summerCharge = quantity * plan.summerRate;
const regularCharge = quantity * plan.regularRate + plan.regularServiceCharge;

return isSummer ? summerCharge : regularCharge;
}

const charge = calcChargeByDate(date, plan, quantity);

2. 조건식 통합하기

  • 하나로 합칠 수 있는 조건식은 합친 뒤, 의도를 드러낼 수 있는 함수로 추출하자.

Before

if (_isEmpty(schedule)) return 0;
if (_isEmpty(schedule.startTime)) return 0;
if (_isEmpty(schedule.endTime)) return 0;

After

function isEmptySchedule(){
return _isEmpty(schedule) || _isEmpty(schedule.startTime) || _isEmpty(schedule.endTime);
}

if (isEmptySchedule()) {
return 0;
}

3. 특이 케이스 추가하기

  • 공통 동작을 요소 하나에 모아서 사용하면 관리하기 편하다.
  • null or undefined의 경우 default 값을 정의

Before

interface Schedule {
id: number;
startTime: string;
endTime: string;
}

class Hospital {
constructor(){
this.schedules = [];
}

addSchedule(schedule: Schedule){
this.schedules = [...this.schedules, new Schedule(schedule)];
}

emptySchedule(scheduleId){
this.schedules = this.schedules.map(schedule => schedule.id === scheduleId ? null : schedule);
}

resetSchedule(){
this.schedules.forEach(schedule => schedule.reset())
}
}

class Schedule {
constructor(schedule: Schedule){

}

reset(){
console.log('스케줄이 리셋되었습니다.');
}
}

const hospital = new Hospital();
hospital.addSchedule({id: 1, startTime: '09:00', endTime: '18:00'});
hospital.addSchedule({id: 2, startTime: '09:00', endTime: '18:00'});
hospital.emptySchedule(1);
hospital.resetSchedule(); // Cannot read properties of null Error!

After

// 특이 케이스 추가
class EmptySchedule {
constructor(){

}

reset(){
console.log('비어있는 스케줄입니다.');
}
}

class Hospital {
constructor(){
this.schedules = [];
}

addSchedule(schedule: Schedule){
this.schedules = [...this.schedules, new Schedule(schedule)];
}

emptySchedule(scheduleId){
this.schedules = this.schedules.map(schedule => schedule.id === scheduleId ? new EmptySchedule() : schedule);
}

resetSchedule(){
this.schedules.forEach(schedule => schedule.reset())
}
}

const hospital = new Hospital();
hospital.addSchedule({id: 1, startTime: '09:00', endTime: '18:00'});
hospital.addSchedule({id: 2, startTime: '09:00', endTime: '18:00'});
hospital.emptySchedule(1);
hospital.resetSchedule();
// console.log('비어있는 스케줄입니다.');
// console.log('스케줄이 리셋되었습니다.');

결론

책이 대학 전공 서적처럼 생겼는데 생각보다 문장이 잘 읽혀서 좋았어요~!

간단한 리팩터링 방법부터 깊이 있는 방법까지 500페이지 정도를 소개하는데

코드를 쭉 읽고 코드에서 나는 악취의 감을 익힐 수 있도록 도와주는 책이라고 생각해요!

관심 있으시다면 책을 직접 읽어보시길 추천드려요ㅎㅎ

여러 번 읽고 좀 더 익숙하게 가이드 할 수 있도록 노력해볼게요~!