여기서는 언제 리팩터링을 해야하는지 알려준다. 리팩터링의 종류를 보면서 나중에 필요할 때 어떤 리팩터링을 해야할 지 확인하자.

3.1 기이한 이름

이름을 명확하게 지어야 한다. 이름만 잘 지어도 나중에 문맥을 파악하느라 헤매는 시간을 크게 절약할 수 있다.

리팩터링 종류

  • 함수 선언 바꾸기, 변수 이름 바꾸기, 필드 이름 바꾸기

3.2 중복 코드

똑같은 코드 구조가 여러 곳에서 반복된다면, 하나로 통합하여 더 나은 프로그램을 만들 수 있다.

리팩터링 종류

  • 두 메서드가 똑같은 표현식을 사용하는 경우
    • 함수 추출하기
  • 코드가 비슷하긴 한데 완전 똑같지는 않은 경우
    • 문장 슬라이드 하기
  • 같은 부모로부터 파생된 서브 클래스들에 중복된 코드가 있는 경우
    • 메서드 올리기.

3.3 긴 함수

함수가 길수록 이해하기 어렵다. 함수를 쪼개야 한다.

리팩터링 종류

  • 함수를 짧게 만드는 작업의 99%가 함수 추출하기이다.
    • 함수 추출하기
  • 함수가 매개변수와 임시변수를 너무 많이 사용하는 경우
    • 임시변수를 질의 함수로 만들기, 매개변수 객체 만들기, 객체 통째로 넘기기
  • 그래도 임시 변수와 매개 변수가 너무 많은 경우
    • 함수를 명령으로 바꾸기
  • 코드가 단 한줄이라도 따로 설명할 필요가 있다면 추출하자.
  • 조건문과 반복문도 추출 대상의 실마리를 제공한다.
    • 조건문 분해하기, 조건문을 다형성으로 바꾸기, 반복문 쪼개기

3.4 긴 매개변수 목록

전역 데이터가 늘어나는 것을 방지하기 위해서 매개변수를 사용하지만, 매개변수가 너무 많아지면 이해가 어려울 때가 많다.

리팩터링 종류

  • 다른 매개변수를 통해서 값을 얻어올 수 있는 경우
    • 매개변수를 질의 함수로 바꾸기
  • 사용중인 데이터 구조에서 값들을 뽑아 각각을 별개의 매개변수로 전달하는 코드인 경우
    • 객체 통째로 넘기기
  • 항상 함께 전달되는 매개변수인 경우
    • 매개변수 객체 만들기
  • 함수의 동작 방식을 정하는 플래그 역할의 매개변수인 경우
    • 플래그 인수 제거하기
  • 여러 개의 함수가 특정 매개변수들의 값을 공통으로 사용하는 경우
    • 여러 함수 클래스로 묶기

3.5 전역 데이터

전역 데이터는 코드 베이스 어디에서든 건드릴 수 있고, 값을 누가 바꿨는지 찾아낼 메커니즘이 없다는 게 문제다. 그래서 유령같은 원격작용처럼 버그를 끊임없이 발생 시키고 원인을 찾기 힘들게 한다.

클래스 변수와 싱글톤에서도 같은 문제가 발생한다.

리팩터링 종류

  • 변수 캡슐화하기를 통해서 누가 변경했는지 알기 쉽게 하고, 접근을 통제할 수 있다.

3.6 가변 데이터

데이터를 변경했더니 예상치 못한 결과나 골치 아픈 버그로 이어지는 경우가 종종 있다.

그래서 함수형 프로그래밍에서는 데이터는 절대 변하지 않고, 데이터를 변경하려면, 반드시 변경하려는 값에 해당하는 복사본을 만들어서 반환한다는 개념을 기본으로 삼고 있다.

데이터 수정에 따른 위험성을 줄이는 리팩터링 방법은 다음과 같다.

리팩터링 종류

  • 정해놓은 함수를 거쳐야만 값을 수정할 수 있도록 해서 감시나 개선을 쉽게하기
    • 변수 캡슐화하기
  • 하나의 변수에 용도가 다른 값들을 저장하는 경우
    • 변수 쪼개기
  • 갱신 로직을 떨어뜨려 놓기
    • 문장 슬라이드하기 + 함수 추출하기
  • 꼭 필요한 경우가 아니라면 부작용이 있는 코드를 호출할 수 없게 하기
    • 질의 함수와 변경 함수 분리하기, 세터 제거하기
  • 파생 변수를 질의 함수로 바꾸기
  • 유효 범위를 제한하기
    • 여러 함수를 클래스로 묶기, 여러 함수를 변환 함수로 묶기
  • 내부 필드에 데이터를 담고 있는 변수
    • 참조를 값으로 바꾸기

3.7 뒤엉킨 변경

코드를 수정할 때 시스템에서 고쳐야 할 딱 한군데를 수정하기 위해 하는 리팩터링이다.

하나의 모듈이 서로 다른 이유들로 인해 여러가지 방식으로 변경되는 일이 많을 때 뒤엉킨 변경이 발생한다.

리팩터링 종류

  • 단계가 순차적으로 실행되는 경우
    • 단계 쪼개기
  • 각기 다른 맥락의 함수를 호출하는 빈도가 높다면, 각 맥락에 해당하는 적당한 모듈들을 만들어서 관련 함수를 모은다.
    • 함수 옮기기
  • 여러 맥락에 관여하는 함수/클래스가 있다면
    • 함수/클래스 추출하기

3.8 산탄총 수술

뒤엉킨 변경과 비슷하지만, 반대의 경우이다. 코드를 변경할 때 마다 자잘하게 수정해야하는 클래스가 많을 때 해야하는 것이다.

변경할 부분이 코드 전반에 퍼져있다면 찾기도 어렵고 꼭 수정해야 할 곳을 지나치기 쉽기 때문에 리팩터링을 해야한다..

리팩터링 종류

  • 함께 변경되는 대상을 모으기
    • 함수 옮기기, 필드 옮기기
  • 비슷한 데이터를 다루는 함수가 많다면
    • 여러 함수를 클래스로 묶기
  • 데이터 구조를 변환하거나 보강
    • 여러 함수를 변환 함수로 묶기
  • 로직을 만들 수 있다면,
    • 단계 쪼개기
  • 잘 분리되지 못한 로직
    • 함수 인라인하기, 클래스 인라인하기

3.9 기능 편애

프로그램을 모듈화 할 때는 코드를 여러 영역으로 나눈 뒤 영역 안에서 이뤄지는 상호작용은 최대한 늘리고, 영역 사이에서 이뤄지는 상호작용은 최소로 줄이는데 주력한다.

기능 편애는 흔히 어떤 함수가 자기가 속한 모듈의 함수나 데이터보다 다른 모듈의 함수나 데이터와 상호작용 할 일이 많을 때 나타난다.

리팩터링 종류

  • 데이터와 가까이 두기
    • 함수 옮기기.
  • 함수의 일부 기능만 편애할 경우
    • 함수 추출하기, 함수 옮기기
  • 함수를 여러 조각으로 나눈 후 적합한 모듈로 옮기기
    • 함수 추출하기

불가능한 복잡한 패턴

전략 패턴, 방문자 패턴, 자기 위임.

3.10 데이터 뭉치

여러 개의 데이터가 여러 곳에서 항상 함께 뭉쳐 다니는 경우.

리팩터링 종류

  • 데이터 뭉치 찾아서 클래스로 묶기
    • 클래스 추출하기
  • 메서드 시그니처에 있는 데이터 뭉치
    • 매개변수 객체 만들기. 객체 통째로 넘기기
  • 레코드가 아닌 클래스로 묶는 것을 추천한다고 한다.

3.11 기본형 집착

전화번호와 같은 것은 문자집합으로만 표현하기에는 아쉬움이 있다. 최소한 출력 기능은 갖춰야 한다.

이와 같이 기본형을 사용하기 보다는 객체로 바꾸는 것이 좋다.

리팩터링 종류

  • 기본형을 객체로 바꾸기
  • 기본형으로 표현된 코드가 타입코드로 작성된 경우
    • 타입 코드를 서브 클래스로 바꾸기, 조건부 로직을 다형성으로 바꾸기
    • enum 활용하는 방법도 있는 듯

3.12 반복되는 switch문

중복된 switch 문의 문제점은 조건절을 하나 추가할 때 마다 다른 switch문들도 모두 찾아서 수정해야한다는 문제점이 있다.

리팩터링 종류

  • 조건부 로직을 다형성으로 바꾸기

3.13 반복문

반복문이 별로라고 하는 것 같은데, 반복문을 사용하지 않는 방법으로 리팩터링한다.

리팩터링 종류

  • 반복문을 파이프라인으로 바꾸기

3.14 성의 없는 요소

코드의 구조를 잡을 때 프로그램 요소(함수, 클래스, 인터페이스 등)를 이용하는 것을 좋아한다.

하지만, 가끔은 그것이 필요 없을 때도 있다.(본문 코드를 그대로 쓰는 것과 다름 없는 코드 or 실질적인 메서드가 하나인 코드)

리팩터링 종류

  • 하는 일이 적은 클래스나 함수
    • 함수 인라인하기, 클래스 인라인하기
  • 상속을 사용한 경우
    • 계층 합치기

3.15 추측성 일반화

나중에 필요할 줄 알고 만들어둔 로직 때문에 관리하기 어려워진 코드들가 있을 수 있다. 이럴 때 당장 필요없는 코드는 지운다.

리팩터링 종류

  • 하는 일이 거의 없는 추상 클래스
    • 계층 합치기
  • 쓸데없이 위임하는 코드
    • 함수 인라인하기, 클래스 인라인하기
  • 사용하지 않는 매개변수
    • 함수 선언 바꾸기
  • 사용하는 곳이 없는 함수나 클래스
    • 죽은 코드 제거하기

3.16 임시 필드

사용하지 않는 필드가 존재하면 안 좋다고 한다.

리팩터링 종류

  • 클래스 추출하기, 함수 옮기기, 특이 케이스 추가하기

3.17 메시지 체인

클라이언트가 한 객체를 통해 다른 객체를 얻은 뒤 방금 얻은 객체에 또 다른 객체를 요청하는 식으로 다른 객체 요청하는 작업이 연쇄적으로 이어지는 코드를 말한다.

리팩터링 종류

  • 위임 숨기기
    • 예를 들면, managerName = aPerson.department.manager.name;과 같이 되어있으면, 그냥 aPerson.managerName()과 같은 함수를 만드는 것이다.

3.18 중개자

객체의 대표적인 기능 하나로, 외부로부터 세부사항을 숨겨주는 캡슐화가 있다.

캡슐화하는 과정에서 위임이 자주 활용되서, 이를 개선하기 위해 하는 리팩터링이다.

리팩터링 종류

  • 중개자 제거하기

3.19 내부자 거래

모듈사이에서 혹은 상속관계에서 많은 데이터를 주고 받으려고 할 때 결합(coupling)이 높아지는 문제가 생길 수 있다.

리팩터링 종류

  • 데이터를 주고 받는 함수가 있다면
    • 함수 옮기기, 필드 옮기기
  • 여러 모듈이 같은 관심사를 공유한다면, 이를 정식으로 처리하는 모듈을 새로 만들거나 위임 숨기기를 해야한다.
    • 위임 숨기기
  • 상속에서 자식 클래스가 너무 많은 것을 상속받아야 할 때
    • 서브클래스를 위임으로 바꾸기, 슈퍼클래스를 위임으로 바꾸기

3.20 거대한 클래스

클래스가 너무 많은 일을 하려다 보면 필드 수가 늘게 되고, 코드 중복도 늘게 된다.

리팩터링 종류

  • 같은 컴포넌트에 모아두는 것이 합당해 보이는 필드들을 묶기
    • 클래스 추출하기
  • 이렇게 분리한 컴포넌트를 원래 클래스와 상속 관계로 만들기
    • 슈퍼클래스 추출하기, 타입 코드를 서브클래스로 바꾸기

3.21 서로 다른 인터페이스의 대안 클래스들

클래스를 사용할 때 큰 장점은 언제든 다른 클래스로 교체할 수 있다는 것이다.

단, 교체하려면 인터페이스가 같아야 한다. 이를 위해서 하는 리팩터링 기법이다.

리팩터링 종류

  • 인터페이스를 같게 만들기
    • 함수 선언 바꾸기, 함수 옮기기
  • 위 리팩터링을 하다가 대안 클래스들 사이에 중복코드가 생기는 경우
    • 슈퍼 클래스 추출하기

3.22 데이터 클래스

데이터 클래스란 데이터 필드와 게터/세터 메서드로만 구성된 클래스를 말한다.

그저 데이터 저장용이다보니, 다른 클래스가 너무 함수로 다룰 때가 많다.

여기서 발생할 수 있는 문제들을 막고, 클래스를 효율적으로 만들기 위해서 하는 리팩터링이다.

리팩터링 종류

  • public 필드가 있다면 숨기기
    • 레코드 캡슐화 하기
  • 변경되면 안되는 필드의 경우
    • 세터 제거하기
  • 데이터 클래스의 게터나 세터를 사용하는 함수를 포함시킬 수 있는 경우
    • 함수 추출하기

3.23 상속 포기

부모 클래스의 유산이 필요 없는 경우에 하는 리팩터링이다.

리팩터링 종류

  • 물려받지 않을 부모코드를 서브클래스로 넘긴다. 이 과정을 거치면 부모 클래스는 공통된 부분만 남는다. 더 나아가서 부모 클래스는 모두 추상 클래스여야한다고 말하는 사람도 많다고 한다.
    • 메서드 내리기, 필드 내리기
  • 아니면 상속 메커니즘을 아에 벗어나는 것도 효과적이다.
    • 서브클래스 위임으로 바꾸기, 슈퍼클래스를 위임으로 바꾸기

3.24 주석

주석 자체는 좋은 것이지만, 잘 작성하지 못한 코드는 주석이 많다고 한다.

리팩터링 종류

  • 주석을 남겨야 겠다는 생각이 들면, 가장 먼저 주석이 필요 없는 코드로 리팩터링해본다.
    • 함수 추출하기, 함수 선언 바꾸기, 어서션 추가하기

2.1 리팩터링 정의

수많은 다른 소프트웨어 개발 용어와 마찬가지로, 리팩터링도 다소 두리뭉실한 의미로 통용된다. 이 책에서는 다음과 같이 정의한다.

  • 리팩터링: [명사] 소프트웨어의 겉보기 동작은 그대로 유지한 채, 코드를 이해하기쉽도록 내부 구조를 변경하는 방법
  • 리팩터링하다: [동사] 소프트웨어의 겉보기 동작은 그대로 유지한 채, 여러가지 리팩터링 기법을 적용해서 소프트웨어를 재구성한다.

코드를 정리하는 작업을 모두 리팩터링이라고 표현하고 있는데, 정의에 따르면 특정한 방식에 따라 코드를 정리하는 것만이 리팩터링이다.

코드베이스를 정리하거나 구조를 바꾸는 모든 작업을 재구성이라고 하고, 리팩터링은 재구성의 특수한 한 형태라고 보면 될 것 같다.

겉보기 동작이라고 한 것은 리팩터링 하기 전과 후에 똑같이 동작해야하는데, 내부에 함수 콜 스택이라든지 성능적인 부분들은 달라질 수 있다고 하는 것이다.

성능 최적화를 위해서는 코드가 더 복잡해 질 수도 있다.

2.2 두개의 모자

저자는 기능 추가와 리팩터링을 동시에 하지 않는다고 한다. 그리고 두 작업에서의 작업 방식의 차이가 분명하게 있다고 한다.

2.3 리팩터링하는 이유

리팩터링이 만병통치약은 아니지만, 코드를 건강한 상태로 유지하는데 도와주는 약임은 틀림 없다고 한다. 치약 같은 존재인가 보다.

다음과 같은 이유로 리팩터링을 한다고 한다.

  • 리팩터링하면 소프트웨어 설계가 좋아진다.
  • 리팩터링하면 소프트웨어를 이해하기 쉬워진다.
    • 다른 사람이 아닌 내가!
  • 리팩터링하면 버그를 쉽게 찾을 수 있다.
  • 리팩털이하면 프로그래밍 속도를 높일 수 있다.

2.4 언제 리팩터링해야할까?

저자는 한 시간 간격으로 한다고 한다. 돈 로버츠는 같은 일을 세번 반복하면 리팩터링을 하라고 한다. 저자는 다음과 같을 때 리팩터링 하는 것을 추천한다고 한다.

  • 준비를 위한 리팩터링: 기능을 쉽게 추가하게 만들기
    • 기능 추가하기 직전에 리팩터링을 한다고 한다.
  • 이해를 위한 리팩터링: 코드를 이해하기 쉽게 만들기
    • 리팩터링 하다보면 이해가 잘되더라.
  • 쓰레기 줍기 리팩터링
    • 쓸데없는 코드를 줄일 수 있다.
  • 계획된 리팩터링과 수시로 하는 리팩터링
    • 사실 리팩터링은 프로그래밍 과정에서 자연스럽게 해야하는 것이다.
    • 보기 싫은 코드를 발견하면 리팩터링하자. 그런데 잘 작성된 코드 역시 수많은 리팩터링을 거쳐야한다.
    • 무언가를 수정하려 할 때는 먼저 수정하기 쉽게 정돈하고, 그런 다음 쉽게 수정하자.
  • 오래걸리는 리팩터링
    • 리팩터링은 대부분 몇 분 안에 끝난다. 하지만 어떨 때는 대규모 리팩터링을 해야할 때가 있다.
      • 라이브러리 교체 작업, 컴포넌트를 빼내는 잡업 등이 있다.
  • 코드 리뷰에 리팩터링 활용하기
    • 여러 의견을 모을 수 있어서 좋다고 한다.
    • 옆에 앉혀놓고 같이 하는 방식이 좋다고 한다.
  • 관리자에게는 뭐라고 말해야 할까?
    • 리팩토링을 불신하는 관리자도 있다. 일을 진전시키지 않고 망치기만 한 경우가 있기 때문이다.
  • 리팩터링하지 말아야 할 때

2.5 리팩터링 시 고려할 문제

  • 새 기능 개발 속도 저하

    • 리팩터링의 궁극적인 목적은 개발 속도를 높여서 더 적은 노력으로 더 많은 가치를 창출하는 것이다.
    • 보통 리팩터링 하면 더 빨리 기능 개발 할 수 있다. 리팩터링이 더 좋다. 리팩터링이 최고다. 리팩터링은 하면 개발이 빨라진다.
  • 코드 소유권

    • 남의 코드여서 소유권이 없는 경우는 리팩터링에 방해가 된다.
    • 코드 소유권을 느슨하게 하는 것이 좋다.
  • 브랜치

    • 일반적으로 브랜치를 하나씩 맡아서 작업하고 합친다.
    • 브랜치 통합 주기를 3일 단위로 짧게 관리해야 한다고 주장하는 사람이 많다. 이런 방식을 지속적 통합(CI), 트렁크 기반 개발(TBD)라고 한다.
      • 저자는 하루에 한번은 해야한다고 한다.
      • CI와 리팩터링을 합쳐서 익스트림 프로그래밍이라고 한다고 한다.
  • 테스팅

    • 리팩터링 과정에서 오류를 빨리 잡기 위해서 자가 테스트 코드를 만들어야한다.
  • 레거시 코드

    • 보통 테스트를 염두에 두고 설계한 시스템이 아니면, 테스트 코드를 사용하기가 까다롭다.
    • 리팩토링을 통해서 하나씩 나눠서 테스트를 하는 방식으로 테스트 코드를 작성한다.
  • 데이터베이스

    • 데이터 베이스 열 이름 바꿀 때 관련된 함수를 모두 바꿔야한다.
      • 이 때 과정을 잘게 나눠서 바꿔준다.

이후 내용에는 그냥 리팩터링과 관련된 여러 얘기들을 하는데... 그냥 긍정적인 얘기 나열이어서 목차만 적고 생략하도록 하겠다. 리팩터링은 확실히 필요한 것 같다.

1.1 자, 시작해보자!

우선, 연극을 외주로 받아서 공연하는 극단 예시가 처음 나온다.

극단에서 필요한 충성도 시스템을 다음과 같이 구현해 보았다. 원래는 자바스크립트인데, 파이썬으로 앞으로 하고 싶어서 파이썬으로 구현해보았다.

{
    "hamlet": {"name": "Hamlet", "type": "tragedy"},
    "as-like": {"name": "As You Like It", "type": "comedy"},
    "othello": {"name": "Othello", "type": "tragedy"}
}
[
    {
        "customer": "BigCo",
        "performances": [
            {
                "playID": "hamlet",
                "audience": 55
            },
            {
                "playID": "as-like",
                "audience": 35
            },
            {
                "playID": "othello",
                "audience": 40
            }
        ]
    }
]
import json


with open("plays.json", "r") as f:
    plays = json.load(f)
with open("invoices.json", "r") as f:
    invoices = json.load(f)


def statement(invoice, plays):
    total_amount = 0
    volume_credits = 0
    result = f"청구 내역 (고객명: {invoice['customer']})\n"
    cur_format = "${:,.2f}"

    for perf in invoice['performances']:
        play = plays[perf['playID']]
        this_amount = 0

        if play['type'] == "tragedy":
            this_amount = 40000
            if perf['audience'] > 30:
                this_amount += 1000 * (perf['audience'] - 30)
        elif play['type'] == "comedy":
            this_amount = 30000
            if perf['audience'] > 20:
                this_amount += 10000 + 500 * (perf['audience'] - 20)
            this_amount += 300 * perf['audience']
        else:
            raise Exception(f"알 수 없는 장르: ${play['type']}")

        volume_credits += max(perf['audience'] - 30, 0)  # 포인트를 적립한다
        if play['type'] == "comedy":
            volume_credits += perf['audience'] // 5  # 포인트를 적립한다

        result += f"  {play['name']}, {cur_format.format(this_amount/100)}, ({perf['audience']}석)\n"
        total_amount += this_amount

    result += f"총액: {cur_format.format(total_amount/100)}\n"
    result += f"적립 포인트: {volume_credits}점\n"
    return result


def main():
    result = statement(invoices[0], plays)
    print(result)


if __name__ == '__main__':
    main()

1.2 예시 프로그램을 본 소감

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

위 코드의 경우 다음 두 가지 변경사항이 있을 수 있다.

  1. 청구 내역을 HTML로 출력하는 기능이 필요하다.
  2. 새로운 요구 사항(다양한 장르...)을 추가하기 쉬워야 한다.

1.3 리팩터링의 첫 단계

리팩터링하기 전에 제대로 된 테스트부터 마련해야한다. 테스트는 반드시 자가진단하도록 만든다.

1.4 statement() 함수 쪼개기

  • 위 코드에서 switch문에 마음에 안드신다고 한다. 그래서 이를 별도의 함수로 추출하는 방식으로 바꿀 것이다. 이러한 과정은 이후에 함수 추출하기에서 더 절차적으로 할 수 있게 기록했다고 한다.
  • 함수를 별도로 빼내었을 때, 범위에 문제가 생기는 변수는 perf, play, this_amount가 있다.
    • 여기서 perf, play는 값을 변경하지 않기 때문에 매개변수로 이용할 수 있다.
    • this_amount의 경우에는 값이 변경되기 때문에 조심히 다뤄야하는데, 여기서는 return하도록 작성했다.
def amount_for(perf, play):
    this_amount = 0

    if play['type'] == "tragedy":
        this_amount = 40000
        if perf['audience'] > 30:
            this_amount += 1000 * (perf['audience'] - 30)
    elif play['type'] == "comedy":
        this_amount = 30000
        if perf['audience'] > 20:
            this_amount += 10000 + 500 * (perf['audience'] - 20)
        this_amount += 300 * perf['audience']
    else:
        raise Exception(f"알 수 없는 장르: ${play['type']}")

    return this_amount
def statement(invoice, plays):
...
    for perf in invoice['performances']:
        play = plays[perf['playID']]
        this_amount = amount_for(perf, play)
...
  • 이렇게 변경하고 실행을 반드시 해봐야 한다.

리팩터링은 프로그램 수정을 작은 단계로 나눠서 진행한다. 그래서 중간에 실수하더라도 버그를 쉽게 찾을 수 있다.

  • 위 예시의 경우 중첩함수를 만들면 매개변수를 전달할 필요가 없어서 일반적으로 더 편하다고 한다.
  • amount_for()에서 변수명 this_amount를 result로 바꾸는 것도 좋다고 한다. 결과는 항상 result로 한다는 저자의 의견.
  • perf의 경우 aPerformance로 바꾸는 것이 더 좋다고 한다.
    • 동적 타입 언어를 사용할 때에는 변수명에 타입이 드러나게 작성하면 좋고, 역할이 뚜렷하지 않을 때에는 부정관사(a/an)을 붙이는 것도 좋다고 한다.

컴퓨터가 이해하는 코드는 바보도 작성할 수 있다. 사람이 이해하도록 작성하는 프로그래머가 진정한 실력자다.

play 변수 제거하기

  • perf(aPerformance)와 달리 play 변수는 매개변수로 전달할 필요가 없다. 그냥 함수 안에서 다시 계산해도 된다.
  • 이를 해결해주는 리팩터링으로는 '임시 변수를 질의 함수로 바꾸기'가 있다고 한다.
def play_for(aPerformance):
    return plays[aPerformance['playID']]
def statement(invoice, plays):
...
    for perf in invoice['performances']:
        play = play_for(perf)
        this_amount = amount_for(perf, play)

        volume_credits += max(perf['audience'] - 30, 0)  # 포인트를 적립한다
        if play['type'] == "comedy":
            volume_credits += perf['audience'] // 5  # 포인트를 적립한다

        result += f"  {play['name']}, {cur_format.format(this_amount/100)}, ({perf['audience']}석)\n"
        total_amount += this_amount
...

이제 play 변수를 다음과 같이 제거할 수 있다. (그런데 계산은 늘어난 거 아닌가..?)

def statement(invoice, plays):
...
    for perf in invoice['performances']:
        this_amount = amount_for(perf, play_for(perf))

        volume_credits += max(perf['audience'] - 30, 0)  # 포인트를 적립한다
        if play_for(perf)['type'] == "comedy":
            volume_credits += perf['audience'] // 5  # 포인트를 적립한다

        result += f"  {play_for(perf)['name']}, {cur_format.format(this_amount/100)}, ({perf['audience']}석)\n"
        total_amount += this_amount
...

그리고 amount_for함수도 변경이 가능하다. 변경하고 나서는 호출하는 곳에서도 매개변수를 없애줘야 한다.

def amount_for(aPerformance):
    result = 0

    if plays(aPerformance)['type'] == "tragedy":
        result = 40000
        if aPerformance['audience'] > 30:
            result += 1000 * (aPerformance['audience'] - 30)
    elif plays(aPerformance)['type'] == "comedy":
        result = 30000
        if aPerformance['audience'] > 20:
            result += 10000 + 500 * (aPerformance['audience'] - 20)
        result += 300 * aPerformance['audience']
    else:
        raise Exception(f"알 수 없는 장르: ${plays(aPerformance)['type']}")

    return result
  • 이렇게 해도 성능의 큰 차이는 없다고 한다. 그리고 이렇게 한 것이 나중에 개선하기가 더 쉽다고 한다.

  • 지역 변수를 제거해서 얻는 가장 큰 이점은 추출 작업이 훨씬 쉬워진다는 것이다.

  • 이제는 "변수 인라인하기"라는 작업을 할 것이다. this_amount라는 변수는 한번 설정되고 변경이 되지 않아서 하면 좋다고 한다.

    • this_amount를 전부 play_for(perf)로 바꾸었다.
def statement(invoice, plays):
    total_amount = 0
    volume_credits = 0
    result = f"청구 내역 (고객명: {invoice['customer']})\n"
    cur_format = "${:,.2f}"

    for perf in invoice['performances']:
        volume_credits += max(perf['audience'] - 30, 0)  # 포인트를 적립한다
        if play_for(perf)['type'] == "comedy":
            volume_credits += perf['audience'] // 5  # 포인트를 적립한다

        result += f"  {play_for(perf)['name']}, {cur_format.format(amount_for(perf)/100)}, ({perf['audience']}석)\n"
        total_amount += amount_for(perf)

    result += f"총액: {cur_format.format(total_amount/100)}\n"
    result += f"적립 포인트: {volume_credits}점\n"
    return result

적립 포인트 계산 코드 추출하기

  • perf와 volume_credits를 아직 처리해줘야하는데, volume_credits은 값이 계속 누적되어서 더 까다롭다고 한다.
  • 이 상황에서 최선의 방법은 volume_credits의 복제본을 초기화한 뒤 계산 결과를 반환하게 하는 것이다.
def volume_credits_for(aPerformance):
    volume_credits = 0
    volume_credits += max(aPerformance['audience'] - 30, 0)
    if play_for(aPerformance)['type'] == "comedy":
        volume_credits += aPerformance['audience'] // 5
    return volume_credits
def statement(invoice, plays):
...
    for perf in invoice['performances']:
        volume_credits += volume_credits_for(perf)  # 포인트를 적립한다

        result += f"  {play_for(perf)['name']}, {cur_format.format(amount_for(perf)/100)}, ({perf['audience']}석)\n"
...

format 변수 제거하기

  • 앞서 설명했듯이, 임시 변수는 나중에 문제를 일으킬 수 있다. 임시 변수는 자신이 속한 루틴에서만 의미가 있어서 루틴이 길고 복잡해지기 쉽다. format은 이중에서 가장 만만해 보인다.
  • 이처럼 함수 변수를 일반 함수로 변경하는 것도 리팩터링이다. 이런 것은 리팩토링 목록에 넣지 않았는데, 목록에 없는 리팩터링 기법도 많다.
def cur_format(aNumber):
    return "${:,.2f}".format(aNumber)
def statement(invoice, plays):
    total_amount = 0
    volume_credits = 0
    result = f"청구 내역 (고객명: {invoice['customer']})\n"

    for perf in invoice['performances']:
        volume_credits += volume_credits_for(perf)  # 포인트를 적립한다

        result += f"  {play_for(perf)['name']}, {cur_format(amount_for(perf)/100)}, ({perf['audience']}석)\n"
        total_amount += amount_for(perf)

    result += f"총액: {cur_format(total_amount/100)}\n"
    result += f"적립 포인트: {volume_credits}점\n"
    return result
  • 그런데, cur_format는 뭔가 함수의 역할을 정확히 말하지 않는다. 그렇다고 format_as_usd라기에는 너무 장황하므로 그냥 usd라고만 하자.
  • 단위 변환 로직도 안에 넣었다.
def usd(aNumber):
    return "${:,.2f}".format(aNumber / 100)

volume_credits 변수 제거하기

  • "반복문 쪼개기"로 빼낼 수 있다고 한다.
def statement(invoice, plays):
    total_amount = 0
    volume_credits = 0
    result = f"청구 내역 (고객명: {invoice['customer']})\n"

    for perf in invoice['performances']:
        result += f"  {play_for(perf)['name']}, {usd(amount_for(perf))}, ({perf['audience']}석)\n"
        total_amount += amount_for(perf)

    for perf in invoice['performances']:  # 값 누적 로직을 별도 for문으로 분리
        volume_credits += volume_credits_for(perf)
...
  • 이어서 "문장 슬라이드하기"를 적용해서 volume_credits 변수를 선언하는 문장 을 반복문 바로 앞으로 옮긴다.
    volume_credits = 0  # 변수 선언(초기화)을 반복문 앞으로 이동
    for perf in invoice['performances']:
        volume_credits += volume_credits_for(perf)
  • 이렇게 하고 나서는 앞에서 play 변수를 제거했던 것 처럼 "임시 변수를 질의 함수로 바꾸기"가 가능해진다. 이번에도 역시 "함수로 추출"해준다.
def total_volume_credits(invoice):
    result = 0  # 변수 선언(초기화)을 반복문 앞으로 이동
    for perf in invoice['performances']:
        result += volume_credits_for(perf)
    return result
def statement(invoice, plays):
    total_amount = 0
    result = f"청구 내역 (고객명: {invoice['customer']})\n"

    for perf in invoice['performances']:
        result += f"  {play_for(perf)['name']}, {usd(amount_for(perf))}, ({perf['audience']}석)\n"
        total_amount += amount_for(perf)

    volume_credits = total_volume_credits(invoice)

    result += f"총액: {usd(total_amount)}\n"
    result += f"적립 포인트: {volume_credits}점\n"
    return result
  • 바로 "변수를 인라인"해보자
def statement(invoice, plays):
    total_amount = 0
    result = f"청구 내역 (고객명: {invoice['customer']})\n"

    for perf in invoice['performances']:
        result += f"  {play_for(perf)['name']}, {usd(amount_for(perf))}, ({perf['audience']}석)\n"
        total_amount += amount_for(perf)

    result += f"총액: {usd(total_amount)}\n"
    result += f"적립 포인트: {total_volume_credits(invoice)}점\n"
    return result
  • 여기서, 반복문을 쪼갠 것이 성능의 저하를 일으키지 않을까 걱정할 수 있지만, 성능의 저하는 대체로 미미하다고 한다. 똑똑한 컴파일러들은 최신 캐싱 기법 등으로 무장하고 있어서 직관을 초월하는 결과를 내어 준다고 한다.
  • 그런데 대체로 미미한 것이지, 항상 그렇다고 생각하면 안된다. 어떨 때는 성능에 큰 영향을 준다고 한다. 하지만 저자는 그런 거에 신경 쓰지 않는다고 한다.

요약하자면, 다음과 같은 네 단계를 수행하면 voluime_credits를 제거할 수 있었다.

  1. "반복문 쪼개기"로 변수 값을 누적시키는 부분을 분리한다.
  2. "문장 슬라이드 하기" 로 변수 초기화 문장을 변수 값 누적 코드 바로 앞에 옮긴다.
  3. "함수 추출하기"로 적립 포인트 계산 부분을 별도 함수로 추출한다.
  4. "변수 인라인하기"로 voluime_credits변수를 제거한다.

이렇게 과정을 잘게 나누고, 테스트를 하고 커밋을 하면서, 테스트에 실패하면 가장 최근 커밋으로 돌아가는 방식으로 하면 된다고 한다.

total_amount도 이제 없어질 차례다.

  • 갑자기 무슨 apple_sauce라는 함수를 만드는데... 나는 그냥 total_amount로 하려고 한다.
def total_amount(invoice):
    result = 0
    for perf in invoice['performances']:
        result += amount_for(perf)
    return result
def statement(invoice, plays):
    result = f"청구 내역 (고객명: {invoice['customer']})\n"
    for perf in invoice['performances']:
        result += f"  {play_for(perf)['name']}, {usd(amount_for(perf))}, ({perf['audience']}석)\n"
    result += f"총액: {usd(total_amount(invoice))}\n"
    result += f"적립 포인트: {total_volume_credits(invoice)}점\n"
    return result

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

HTML 버전을 만들고 싶은데, 텍스트 버전과 동일한 방식으로 만들고 싶다고 한다.

  • 이럴 때 단계 쪼개기라는 방식으로 리팩터링을 한다.
  • 단계를 쪼개려면 먼저 두 번째 단계가 될 코드들을 함수 추출하기로 뽑아내야 한다. 텍스트 버전의 코드를 추출하고, statement에 중간 데이터 구조를 추가하면 다음과 같다.
def render_plane_text(data, invoice, plays):
    result = f"청구 내역 (고객명: {invoice['customer']})\n"
    for perf in invoice['performances']:
        result += f"  {play_for(perf)['name']}, {usd(amount_for(perf))}, ({perf['audience']}석)\n"
    result += f"총액: {usd(total_amount(invoice))}\n"
    result += f"적립 포인트: {total_volume_credits(invoice)}점\n"
    return result

def statement(invoice, plays):
    statement_data = {}
    return render_plane_text(statement_data, invoice, plays)

사실 render_plane_text에는 수많은 중첩함수가 있는 형태이다. 이것도 다 빼내야 한다. 이제, render_plane_text 함수에서 invoice를 없애보자.

def render_plane_text(data, plays):
...
    def total_volume_credits(data):
        result = 0  # 변수 선언(초기화)을 반복문 앞으로 이동
        for perf in data['performances']:
            result += volume_credits_for(perf)
        return result

    def total_amount(data):
        result = 0
        for perf in data['performances']:
            result += amount_for(perf)
        return result

    result = f"청구 내역 (고객명: {data['customer']})\n"
    for perf in data['performances']:
        result += f"  {play_for(perf)['name']}, {usd(amount_for(perf))}, ({perf['audience']}석)\n"
    result += f"총액: {usd(total_amount(data))}\n"
    result += f"적립 포인트: {total_volume_credits(data)}점\n"
    return result


def statement(invoice, plays):
    statement_data = {}
    statement_data['customer'] = invoice['customer']
    statement_data['performances'] = invoice['performances']
    return render_plane_text(statement_data, plays)

이와 같이 변경해서 invoice 매개변수를 삭제할 수 있다.

그리고, 다음과 같이 해서 연극 정보를 data에 추가한다. 여기서, play_for 함수가 statement 함수로 옮겨졌다.

def statement(invoice, plays):
    def play_for(aPerformance):
        return plays[aPerformance['playID']]

    def enrich_performances(aPerformance):
        result = aPerformance
        result['play'] = play_for(result)
        return result

    statement_data = {}
    statement_data['customer'] = invoice['customer']
    statement_data['performances'] = [enrich_performances(perf) for perf in invoice['performances']]
    return render_plane_text(statement_data, plays)

그런 다음 render_plane_text의 모든 play_for 함수를 aPerformance['play']와 같이 변경한다.

amount_for, volume_credits_for 함수도 비슷한 방식으로 옮기고, total_amount와 total_volume_credits을 옮긴다.

중간 점검을 하면 다음과 같이 변하였다.

def render_plane_text(data, plays):
    def usd(aNumber):
        return "${:,.2f}".format(aNumber / 100)

    result = f"청구 내역 (고객명: {data['customer']})\n"
    for perf in data['performances']:
        result += f"  {perf['play']['name']}, {usd(perf['amount'])}, ({perf['audience']}석)\n"
    result += f"총액: {usd(data['total_amount'])}\n"
    result += f"적립 포인트: {data['total_volume_credits']}점\n"
    return result


def statement(invoice, plays):
    def play_for(aPerformance):
        return plays[aPerformance['playID']]

    def amount_for(aPerformance):
        result = 0

        if aPerformance['play']['type'] == "tragedy":
            result = 40000
            if aPerformance['audience'] > 30:
                result += 1000 * (aPerformance['audience'] - 30)
        elif aPerformance['play']['type'] == "comedy":
            result = 30000
            if aPerformance['audience'] > 20:
                result += 10000 + 500 * (aPerformance['audience'] - 20)
            result += 300 * aPerformance['audience']
        else:
            raise Exception(f"알 수 없는 장르: ${aPerformance['play']['type']}")

        return result

    def volume_credits_for(aPerformance):
        result = 0
        result += max(aPerformance['audience'] - 30, 0)
        if aPerformance['play']['type'] == "comedy":
            result += aPerformance['audience'] // 5
        return result

    def total_volume_credits(data):
        result = 0  # 변수 선언(초기화)을 반복문 앞으로 이동
        for perf in data['performances']:
            result += perf['volume_credits']
        return result

    def total_amount(data):
        result = 0
        for perf in data['performances']:
            result += perf['amount']
        return result

    def enrich_performances(aPerformance):
        result = aPerformance
        result['play'] = play_for(result)
        result['amount'] = amount_for(result)
        result['volume_credits'] = volume_credits_for(result)
        return result

    statement_data = {}
    statement_data['customer'] = invoice['customer']
    statement_data['performances'] = [enrich_performances(perf) for perf in invoice['performances']]
    statement_data['total_amount'] = total_amount(statement_data)
    statement_data['total_volume_credits'] = total_volume_credits(statement_data)
    return render_plane_text(statement_data, plays)

이렇게 하니 가볍게 반복문을 파이프라인으로 바꾸기가 하고싶어졌다고 한다. 일단 파이프라인이 뭔지 모르겠다. reduce를 쓰는 모습을 보인다.

    def total_volume_credits(data):
        return reduce(lambda acc, cur: acc + cur['volume_credits'], data['performances'], 0)

    def total_amount(data):
        return reduce(lambda acc, cur: acc + cur['amount'], data['performances'], 0)

대충 이런식으로 바꾸는 것 같은데... 파이썬의 경우 reduce를 쓰는 것 보다 sum이 가독성 및 성능의 측면에서 더 우수하다. 람다를 사용하는 것도 그냥 함수 객체를 만드는 것과 동일하다고 하니 다음과 같은 방법을 사용하자.(전문가를 위한 파이썬에서 봄)

    def total_volume_credits(data):
        return sum(perf['volume_credits'] for perf in data['performances'])

    def total_amount(data):
        return sum(perf['amount'] for perf in data['performances'])

여기서, 제너레이터를 쓴 것은 너무나도 당연한 이치.


def create_statement_data(invoice, plays):
    def play_for(aPerformance):
        return plays[aPerformance['playID']]

    def amount_for(aPerformance):
        result = 0

        if aPerformance['play']['type'] == "tragedy":
            result = 40000
            if aPerformance['audience'] > 30:
                result += 1000 * (aPerformance['audience'] - 30)
        elif aPerformance['play']['type'] == "comedy":
            result = 30000
            if aPerformance['audience'] > 20:
                result += 10000 + 500 * (aPerformance['audience'] - 20)
            result += 300 * aPerformance['audience']
        else:
            raise Exception(f"알 수 없는 장르: ${aPerformance['play']['type']}")

        return result

    def volume_credits_for(aPerformance):
        result = 0
        result += max(aPerformance['audience'] - 30, 0)
        if aPerformance['play']['type'] == "comedy":
            result += aPerformance['audience'] // 5
        return result

    def total_volume_credits(data):
        return sum(perf['volume_credits'] for perf in data['performances'])

    def total_amount(data):
        return sum(perf['amount'] for perf in data['performances'])

    def enrich_performances(aPerformance):
        result = aPerformance
        result['play'] = play_for(result)
        result['amount'] = amount_for(result)
        result['volume_credits'] = volume_credits_for(result)
        return result

    statement_data = {}
    statement_data['customer'] = invoice['customer']
    statement_data['performances'] = [enrich_performances(perf) for perf in invoice['performances']]
    statement_data['total_amount'] = total_amount(statement_data)
    statement_data['total_volume_credits'] = total_volume_credits(statement_data)

    return statement_data


def statement(invoice, plays):
    return render_plane_text(create_statement_data(invoice, plays))

이제 이와 같이 별도로 다 빼버린다. 그리고 create_statement_data와 나머지 함수를 별도의 파일에 저장한다.

코드 길이가 늘어난 것은 너무 부정적으로 보지 않아도 괜찮다.

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

amount_for 함수에서 장르별로 다른 방식으로 계산하는데, 이와 같은 코드는 코드의 길이가 늘어나는 주범이다. 그래서 다형성을 이용해서 이를 해결하는데, 이러한 과정은 조건부 로직을 다형성으로 바꾸기 기법이다.

이를 리팩터링하려면 상속 계층부터 정의해야 한다.

volume_credits_for 함수와 amount_for에서 type에 따라서 다르게 계산하는 모습을 보이는데, 이렇게 두함수를 전용 클래스로 옮기는 작업을 해야한다.

class performance_calculator:
    def __init__(self, aPerformance):
        self.aPerformance = aPerformance
...
def enrich_performances(aPerformance):
        calculator = performance_calculator(aPerformance)
        result = aPerformance
...

우선 이렇게 정의를 한다. 아직은 할 수 있는 일이 없다. 이제 여기에 기존 코드의 함수들을 옮길 것이다.

대충 옮긴 모습은 다음과 같다.

class performance_calculator:
    def __init__(self, aPerformance, aPlay):
        self.performance = aPerformance
        self.play = aPlay

    @property
    def amount(self):
        result = 0
        if self.play['type'] == "tragedy":
            result = 40000
            if self.performance['audience'] > 30:
                result += 1000 * (self.performance['audience'] - 30)
        elif self.play['type'] == "comedy":
            result = 30000
            if self.performance['audience'] > 20:
                result += 10000 + 500 * (self.performance['audience'] - 20)
            result += 300 * self.performance['audience']
        else:
            raise Exception(f"알 수 없는 장르: ${self.play['type']}")

        return result

    @property
    def volume_credits(self):
        result = 0
        result += max(self.performance['audience'] - 30, 0)
        if self.performance['play']['type'] == "comedy":
            result += self.performance['audience'] // 5
        return result
def create_statement_data(invoice, plays):
...
    def enrich_performances(aPerformance):
        calculator = performance_calculator(aPerformance, play_for(aPerformance))
        result = aPerformance
        result['play'] = calculator.play
        result['amount'] = calculator.amount
        result['volume_credits'] = calculator.volume_credits
        return result

공연료 계산기를 다형성 버전으로 만들기

클래스에 로직을 담았으니 이제 다형성을 지원하게 만들면 된다. 가장 먼저 할 일은 타입코드를 서브클래스로 바꾸기이다.

이렇게 하기 위해서는 performance_calculator의 서브클래스들을 준비하고, create_statement_data에서 적절한 것을 사용하게 만들어야 한다.

그리고 왠지 모르겠는데 생성자를 팩터리 함수로 바꿔야한다고 한다.

def create_performance_calculator(aPerformance, aPlay):
    return performance_calculator(aPerformance, aPlay)

def create_statement_data(invoice, plays):
...
    def enrich_performances(aPerformance):
        calculator = create_performance_calculator(aPerformance, play_for(aPerformance))
        result = aPerformance
        result['play'] = calculator.play
        result['amount'] = calculator.amount
        result['volume_credits'] = calculator.volume_credits
        return result

이렇게 하고, create_performance_calculator를 다음과 같이 변경한다.

class tragedy_calculator(performance_calculator):

class comedy_calculator(performance_calculator):

def create_performance_calculator(aPerformance, aPlay):
    if aPlay['type'] == "tragedy":
        return tragedy_calculator(aPerformance, aPlay)
    elif aPlay['type'] == "comedy":
        return comedy_calculator(aPerformance, aPlay)
    else:
        raise Exception(f"알 수 없는 장르: ${aPlay['type']}")

클래스를 다음과 같이 수정한다.

class performance_calculator:
    def __init__(self, aPerformance, aPlay):
        self.performance = aPerformance
        self.play = aPlay

    @property
    def amount(self):
        raise Exception("서브클래스에서 하기로 했습니다.")

    @property
    def volume_credits(self):
        result = 0
        result += max(self.performance['audience'] - 30, 0)
        if self.performance['play']['type'] == "comedy":
            result += self.performance['audience'] // 5
        return result


class tragedy_calculator(performance_calculator):
    @property
    def amount(self):
        result = 40000
        if self.performance['audience'] > 30:
            result += 1000 * (self.performance['audience'] - 30)
        return result


class comedy_calculator(performance_calculator):
    @property
    def amount(self):
        result = 30000
        if self.performance['audience'] > 20:
            result += 10000 + 500 * (self.performance['audience'] - 20)
        result += 300 * self.performance['audience']
        return result

그리고, volume_credits의 경우에는 comedy에서만 변동이 생기므로, 다음과 같이 comedy_calculator 클래스를 변경한다.

class performance_calculator:
...
    @property
    def volume_credits(self):
        return max(self.performance['audience'] - 30, 0)


class comedy_calculator(performance_calculator):
...
    @property
    def volume_credits(self):
        return super().volume_credits + self.performance['audience'] // 5

super()에 주의하자! 괄호 있다.

앞으로 새로운 장르가 들어오면 create_performance_calculator에 장르를 추가하고, 서브클래스를 작성하면 된다.

요약

이번 장에서는 함수 추출하기, 변수 인라인하기, 함수 옮기기, 조건부 로직을 다형성으로 바꾸기를 비롯한 다양한 리팩터링 기법을 선보였다.

이번 장에서는 크게 다음의 세 단계로 진행하였다고 할 수 있다.

  1. 원본 함수를 중첩함수 여러 개로 나눴다.
  2. 단계 쪼개기를 적용해서 계산 코드와 출력 코드를 분리했다.
  3. 계산 로직을 다형성으로 표현했다.

중간 중간 테스트를 하는 것으로 결과가 잘 유지되고 있는지 확인하는 것도 중요했다.

좋은 코드를 가늠하는 확실한 방법은 '얼마나 수정하기 쉬운가'다.

Profile

한창헌

https://github.com/HanChangHun