본문 바로가기
개발

[리팩터링 2판] JavaScript 리팩터링 도서학습 #1

by 개발과 운동, 그리고 책장 2025. 3. 23.

 

해당 내용은 위의 책의 내용을 제가 이해한대로 정리해둔 내용입니다.

이번 포스팅에선 Chapter01 ~ Chapter03 의 내용에 대해 다룹니다. 

 

  • Chapter01 리팩터링: 첫 번째 예시
  • Chapter02 리팩터링 원칙
  • Chapter03 코드에서 나는 악취 

 

Chapter01 리팩터링: 첫 번째 예시

리팩터링의 개념과 과정

 

참고) 아래의 단계는 예제 코드에서 단계별로 수정하는 과정을 보여주었음. 본인 상황에 맞추어서 단계는 바꿔가면서 진행해도 무방합니다. 

단계 리팩터링 기법 설명 효과
1 테스트 코드 작성 리팩터링 전 기존 코드의 동작을 검증하는 테스트 코드 작성 기능 손상 없이 안전하게 리팩터링
2 함수 추출하기 긴 함수를 목적별로 작은 함수들로 분리 코드 가독성 향상
3 변수 이름 개선 임시 변수와 매개변수의 이름을 명확하게 변경 코드의 의미 전달력 향상
4 임시 변수를 질의 함수로 바꾸기 반복 사용되는 계산식을 함수로 추출 중복 제거
5 반복문 쪼개기 하나의 반복문이 여러 일을 하는 경우 목적별로 분리 단일 책임 원칙 적용
6 단계 쪼개기 데이터 처리와 출력 형식 생성 로직 분리 관심사 분리
7 클래스 도입 관련 데이터와 함수를 클래스로 그룹화 데이터와 기능의 응집도 향상
8 조건부 로직을 다형성으로 바꾸기 조건문을 상속과 다형성으로 대체 새로운 조건 추가 시 기존 코드 수정 불필요

 

 

많은 예제가 존재하지만, 대표적으로 함수 추출하기 단계의 개선 전후의 코드만 비교하겠습니다.

// 개선 전
function statement(invoice, plays) {
  let totalAmount = 0;
  let volumeCredits = 0;
  let result = `청구 내역 (고객명: ${invoice.customer})\n`;
  
  for (let perf of invoice.performances) {
    const play = plays[perf.playID];
    let thisAmount = 0;
    
    switch (play.type) {
      case "tragedy":
        thisAmount = 40000;
        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);
    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;
}

 

//개선 후 
function statement(invoice, plays) {
  return renderPlainText(createStatementData(invoice, plays));
}

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);
    result.amount = amountFor(result);
    result.volumeCredits = volumeCreditsFor(result);
    return result;
  }
  
  function playFor(aPerformance) {
    return plays[aPerformance.playID];
  }
  
  function amountFor(aPerformance) {
    let result = 0;
    switch (aPerformance.play.type) {
      case "tragedy":
        result = 40000;
        if (aPerformance.audience > 30) {
          result += 1000 * (aPerformance.audience - 30);
        }
        break;
      case "comedy":
        result = 30000;
        if (aPerformance.audience > 20) {
          result += 10000 + 500 * (aPerformance.audience - 20);
        }
        result += 300 * aPerformance.audience;
        break;
      default:
        throw new Error(`알 수 없는 장르: ${aPerformance.play.type}`);
    }
    return result;
  }
  
  function volumeCreditsFor(aPerformance) {
    let result = Math.max(aPerformance.audience - 30, 0);
    if ("comedy" === aPerformance.play.type) 
      result += Math.floor(aPerformance.audience / 5);
    return result;
  }
  
  function totalAmount(data) {
    return data.performances.reduce((total, p) => total + p.amount, 0);
  }
  
  function totalVolumeCredits(data) {
    return data.performances.reduce((total, p) => total + p.volumeCredits, 0);
  }
}

function renderPlainText(data) {
  let result = `청구 내역 (고객명: ${data.customer})\n`;
  
  for (let perf of data.performances) {
    result += ` ${perf.play.name}: ${format(perf.amount/100)} (${perf.audience}석)\n`;
  }
  
  result += `총액: ${format(data.totalAmount/100)}\n`;
  result += `적립 포인트: ${data.totalVolumeCredits}점\n`;
  return result;
}

 

함수 추출로 인한 코드 라인 수 증가라는 단점도 있습니다. 단기적인 비용

장기적으로는 코드 품질, 이해도, 유지보수성 측면에서 훨씬 더 큰 가치를 제공할수있습니다. 

 

특히 많은 레거시들을 가지고있는 프로젝트나, 복잡한 프로젝트에서

코드 품질, 이해도, 유지보수성 이러한 장점들은 더욱 빛을 볼수있습니다.

 

 

 

Chapter01 리팩터링 정의

리팩터링은 소프트웨어 개발에서 중요한 개념이지만, 많은 엔지니어들 사이에서 의미가 모호하게 사용되기도 합니다.

해당 도서에서는 아래와 같이 명확하게 정의 합니다.

 

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

 

리팩터링[동사]
소프트웨어의 겉보기 동작은 그대로 유지한 채, 여러가지 리팩터링 기법을 적용해서 소프트웨어를 재구성하는 것

 

여기서 핵심은 겉보기 동작은 그대로 유지 한다는 점입니다.

리팩터링의 본질은 사용자 관점에서 기능의 변화 없이 코드 구조만 개선하는 것입니다.

 

 

리팩터링의 목적

리팩터링의 궁극적인 목적은 개발 속도를 높여서, 더 적은 노력으로 더 많은 가치를 창출하는 것

 

리팩터링의 주요 원칙

 

  • 점진적인 변경: 리팩터링은 한 번에 큰 변화를 주기보다 작은 단계로 나누어 진행합니다. 각 단계마다 테스트를 실행하여 기능이 정상적으로 동작하는지 확인합니다.
  • 테스트의 중요성: 안전한 리팩터링을 위해서는 견고한 테스트 코드가 필수적입니다. 테스트는 리팩터링 과정에서 기능이 손상되지 않았음을 보장하는 안전망 역할을 합니다.
  • 설계 개선: 리팩터링은 단순히 코드를 정리하는 것이 아니라, 소프트웨어 설계를 점진적으로 개선하는 과정입니다. 기술 부채를 갚는 방법이기도 합니다.
  • 가독성 향상: 코드는 컴퓨터가 실행하기 위한 것뿐만 아니라, 개발자가 읽고 이해하기 위한 것이기도 합니다. 리팩터링은 코드의 의도를 더 명확하게 표현하여 가독성을 높입니다.


리팩터링이 필요한 시점

 

  • 기능 추가 전: 새로운 기능을 추가하기 전에 코드를 리팩터링하면, 새 기능을 더 쉽게 구현할 수 있습니다.
  • 버그 수정 시: 버그를 수정할 때, 관련 코드를 리팩터링하면 근본 원인을 더 쉽게 파악하고 해결할 수 있습니다.
  • 코드 리뷰 중: 코드 리뷰는 리팩터링의 좋은 기회 다른 개발자의 관점에서 코드 구조를 개선할 방법을 발견할 수 있습니다.
  • 반복되는 패턴 발견 시: 코드에서 비슷한 패턴이 반복적으로 나타난다면, 이를 추상화하여 중복을 제거할 기회입니다.


리팩터링 실천 방법

  1. 자주, 작게 리팩터링하기: 대규모 리팩터링 프로젝트보다 일상적인 개발 과정에서 작은 리팩터링을 지속적으로 수행하는 것이 더 효과적입니다.
  2. 한 번에 한 가지만 변경하기: 여러 문제를 동시에 해결하려 하지 말고, 한 번에 한 가지 문제만 집중해서 해결합니다.
  3. 커밋 단위 구분하기: 리팩터링과 기능 추가/변경은 별도의 커밋으로 분리하는 것이 좋습니다. 이렇게 하면 변경 사항을 추적하고 필요시 롤백하기 쉬워집니다.
  4. IDE의 자동 리팩터링 도구 활용하기: 현대 IDE들은 안전한 리팩터링을 위한 다양한 도구를 제공합니다. 이러한 도구를 적극 활용하면 실수를 줄이고 생산성을 높일 수 있습니다.

 

Chapter03 코드에서 나는 악취 

코드에서 나는 악취(Code Smell) 더 깊은 문제를 암시하는 코드의 특징이나 패턴을 의미합니다.

여기에 주요 코드 악취를 정리했습니다. 다만, 외국서적이다 보니 번역상에 이해가 안되는 부분이 조금 있을수있고 예제 코드가 함께 제공되는 부분이 아니다보니 개념들만 정리해 두었고, 더 구체적인 코드 예제와 해결 방법은 이후 장에서 다루니 예제 코드는 이후의 포스팅에서 함께 다루도록하겠습니다. 

 

주요 코드 악취

 

1. 기이한 이름 (Mysterious Name)
특징: 코드는 의도를 명확히 드러내야 합니다. 좋은 이름은 코드의 목적과 역할을 즉시 알려줍니다.
문제: 함수, 변수, 클래스 등의 이름이 목적이나 역할을 명확히 드러내지 못함
의도가 분명히 드러나는 이름으로 변경

2. 중복 코드 (Duplicated Code)
특징: 동일한 코드가 여러 곳에 있으면 한 곳 수정 시 다른 곳도 수정해야 합니다. 중복은 버그의 온상입니다.
문제: 동일하거나 유사한 코드가 여러 곳에 존재
함수 추출, 메서드 올리기 등을 통해 중복 제거

3. 긴 함수 (Long Function)
특징: 작은 함수는 이해하기 쉽고, 재사용성이 높으며, 테스트하기 쉽습니다.
문제: 너무 많은 일을 하는 긴 함수
  함수 추출, 조건문 분해 등을 통해 작고 명확한 함수로 분리

4. 긴 매개변수 목록 (Long Parameter List)
특징 : 매개변수가 많을수록 함수의 사용법을 이해하고 기억하기 어려워집니다.
문제: 함수의 매개변수가 너무 많음
   매개변수 객체 만들기, 객체 통째로 넘기기 등

5. 전역 데이터 (Global Data)
특징 : 전역 데이터는 코드 어디서나 변경될 수 있어 추적이 어렵고 예측할 수 없는 부작용을 일으킵니다.
문제: 전역 변수나 클래스 변수를 사용
   변수 캡슐화, 함수 옮기기 등

6. 가변 데이터 (Mutable Data)
특징 : 데이터가 예상치 못한 곳에서 변경되면 버그를 찾기 어렵습니다.
문제: 데이터가 여러 곳에서 변경됨
   변수 캡슐화, 불변 데이터 구조 사용

7. 뒤엉킨 변경 (Divergent Change)
특징 : 한 클래스는 한 가지 이유로만 변경되어야 합니다(단일 책임 원칙).
문제: 하나의 클래스가 서로 다른 이유로 자주 변경됨
   클래스 추출, 함수 옮기기 등을 통해 책임 분리

8. 산탄총 수술 (Shotgun Surgery)
특징 : 하나의 변경이 여러 클래스에 영향을 미치면 변경을 누락하기 쉽습니다.
문제: 한 가지 변경을 위해 여러 클래스를 수정해야 함
   관련된 기능을 한 곳으로 모아서, 변경이 필요할 때 한 곳만 수정하면 되도록 만드는 것

 

 

 

 

산탄총에 총상을 입으면 

산탄이 몸 여러 부위에 흩어져 들어가기 때문에 의사는 여러 곳을 동시에 수술해야 함

 

 


9. 기능 편애 (Feature Envy)
특징 : 함수는 자신이 조작하는 데이터와 같은 모듈에 있어야 응집도가 높아집니다.
문제: 한 함수가 자신이 속한 모듈보다 다른 모듈의 데이터와 더 많이 상호작용함
   함수 옮기기, 함수 추출 등

10. 데이터 뭉치 (Data Clumps)
특징 : 항상 함께 다니는 데이터는 하나의 의미 있는 객체로 표현하는 것이 좋습니다.
문제: 여러 곳에서 같은 데이터 항목들이 함께 나타남
   클래스 추출, 매개변수 객체 만들기

11. 기본형 집착 (Primitive Obsession)
특징 : 작은 객체로 개념을 표현하면 코드의 의미가 더 명확해집니다.
문제: 객체 대신 기본 타입을 과도하게 사용
   기본형을 객체로 바꾸기, 타입 코드를 클래스로 바꾸기

12. 반복되는 스위치문 (Repeated Switches)
특징 : 같은 조건으로 분기하는 코드가 여러 곳에 있으면 새 조건 추가 시 모두 수정해야 합니다.
문제: 같은 조건의 스위치문이 여러 곳에 반복됨
   다형성을 활용한 조건부 로직 대체

13. 반복문 (Loops)
특징 : 고전적인 반복문보다 더 읽기 쉽고 오류가 적은 고차 함수를 제공합니다.
문제: 복잡한 반복문 로직
   파이프라인으로 바꾸기(map, filter, reduce 등 활용)

14. 성의 없는 요소 (Lazy Element)
특징 : 불필요한 추상화는 코드를 복잡하게 만듭니다.
문제: 실질적인 기능을 하지 않는 클래스나 함수
   함수 인라인, 클래스 인라인 등으로 제거

15. 추측성 일반화 (Speculative Generality)
특징 : 미래에 필요할 것이라는 예상으로 만든 코드는 대부분 불필요한 복잡성만 추가합니다.
문제: 미래에 필요할 것이라는 예상으로 만든 사용되지 않는 코드
   계층 합치기, 함수 인라인 등으로 불필요한 추상화 제거

16. 임시 필드 (Temporary Field)
특징 : 특정 상황에서만 사용되는 필드는 이해하기 어렵고 오류를 발생시킬 수 있습니다.
문제: 특정 상황에서만 값이 채워지는 필드
   클래스 추출, 특수 케이스 패턴 적용

17. 메시지 체인 (Message Chains)
특징 : 객체를 통해 다른 객체를 연쇄적으로 탐색하는 코드는 중간 객체의 변경에 취약합니다.
문제: 객체를 요청하고 그 객체에서 또 다른 객체를 요청하는 연쇄적 호출
   위임 숨기기, 함수 추출 등

18. 중개자 (Middle Man)
특징 : 단순히 다른 객체에 위임만 하는 클래스는 불필요한 복잡성을 추가합니다.
문제: 자신은 거의 일을 하지 않고 다른 객체로 메서드 호출을 위임하는 클래스
   중개자 제거, 함수 인라인 등

19. 내부자 거래 (Insider Trading)
특징 : 모듈 간의 숨겨진 의존성은 코드를 이해하고 변경하기 어렵게 만듭니다.
문제: 모듈 간에 숨겨진 지나친 의존성
   함수 옮기기, 필드 옮기기로 의존성 명확히 하기

20. 거대한 클래스 (Large Class)
특징 : 너무 많은 책임을 가진 클래스는 이해하기 어렵고 재사용하기 어렵습니다.
문제: 너무 많은 필드와 메서드를 가진 클래스
   클래스 추출, 슈퍼클래스 추출 등

21. 서로 다른 인터페이스의 대안 클래스들 (Alternative Classes with Different Interfaces)
특징 : 비슷한 기능을 하는 클래스는 동일한 인터페이스를 가져야 상호 교체가 가능합니다.
문제: 유사한 기능을 하지만 다른 시그니처를 가진 클래스들
   함수 선언 바꾸기, 함수 옮기기 등으로 인터페이스 통일

22. 불완전한 라이브러리 클래스 (Incomplete Library Class)
특징 : 외부 라이브러리를 확장하기 어려울 때 적용할 수 있는 패턴들이 있습니다.
문제: 필요한 기능이 없는 라이브러리 클래스
   외래 클래스에 함수 추가, 로컬 확장 등

23. 데이터 클래스 (Data Class)
특징 : 데이터만 있고 기능이 없는 클래스는 객체지향의 장점을 활용하지 못합니다.
데이터만 가지고 있고 동작은 없는 클래스
   레코드 캡슐화, 함수 옮기기 등으로 동작 추가

24. 상속 포기 (Refused Bequest)
특징 : 상속은 'is목적: a' 관계일 때만 사용해야 합니다. 그렇지 않으면 위임이 더 적합합니다.
서브클래스가 부모 클래스의 메서드나 데이터를 사용하지 않음
   위임으로 바꾸기, 계층 합치기 등

25. 주석 (Comments)
특징 : 주석은 코드로 표현하지 못한 의도를 설명하는 보조 수단이어야 합니다.
문제: 코드의 의도나 구조를 설명하는 과도한 주석
   함수 추출, 이름 바꾸기 등으로 코드 자체를 명확하게 만들기

 

 

정리

책을 1~3장까지 읽고 학습하며 리팩터링을 해야하는 이유와, 여러가지 기법들 해야하는 상황들에 대한 개념에 학습하고 그 내용을 정리해봤습니다. 가장 와닿았던 문제는 Chapter 02 리펙터링 원칙 - 리팩터링이 필요한 시점 이 부분이였는데, 이러한 문제들은 회사에서 업무를 진행하다보면 상당히 빈번하게 만나게 되는데 머리로는 알고있는데 실제로 못본척 내 몸이 편하자고 넘어갔던 문제들도 꾀 많았던거 같습니다. 뒤에 더 정리할 상세한 기법들을 하나씩 코드에 적용 할수있으면 좋겠습니다 :) 

 

 

 

댓글