3장 코드에서 나는 악취

냄새 나면 당장 갈아라.

About

리팩터링의 적용 방법(HOW)을 아는 것과 제때 적용(WHEN)할 줄 아는 것은 다르다.

3장에서는 리팩터링하면 해결할 수 있는 문제의 징후를 제시한다.

3.1 기이한 이름 (Mysterious Name)

"컴퓨터과학에서 어려운 건 딱 두 가지, 캐시 무효화(cache invalidation)와 이름 짓기다" - 필 칼튼

코드를 명료하게 표현하는 가장 중요한 요소 하나는 바로 '이름'이다.

이름만 잘 지어도 코드를 파악하느라 헤매는 시간을 크게 절약할 수 있다.

  • 함수 선언 바꾸기

  • 변수 이름 바꾸기

  • 필드 이름 바꾸기

3.2 중복 코드 (Duplicated Code)

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

  • 함수 추출하기

    • e.g., 한 클래스에 딸린 두 메서드가 똑같은 표현식을 사용할 경우,

    • 양쪽 모두 추출된 메서드를 호출하게 바꾼다.

  • 문장 슬라이드하기

    • e.g., 코드가 비슷하긴 한데 완전히 똑같지는 않다면,

    • 비슷한 부분을 한 곳에 모아 함수 추출하기를 더 쉽게 적용할 수 있는지 살펴본다.

  • 메서드 호출하기

    • e.g., 같은 부모로부터 파생된 서브클래스들에 코드가 중복되어 있다면,

    • 각자 따로 호출되지 않도록 메서드 올리기를 적용해 부모로 옮긴다.

3.3. 긴 함수 (Long Function)

함수가 길수록 이해하기 어렵다.

함수를 짧게 구성하면 간접 호출(indirection)의 효과를 누릴 수 있다. (코드를 이해하고, 공유하고, 선택하기 쉬워진다.)

  • 물론 코드를 이해하는 사람 입장에서 함수가 하는 일을 파악하기 위해 왔다갔다 해야 한다.

    • 함수 호출부와 선언부 사이를 빠르게 이동하거나 호출과 선언을 동시에 보여주는 개발환경을 활용하면 부담이 줄어든다.

    • 하지만 가장 확실한 방법은 좋은 이름이다. 함수 이름을 잘 지어두면 본문 코드를 볼 필요가 없다.

  • 주석을 달만한 부분은 무조건 함수로 만든다.

  • 함수가 매개변수와 임시 변수를 많이 사용하면 추출 작업에 방해가 된다.

    • 임시 변수를 질의 함수로 바꾸기 - 임시 변수의 수를 줄인다.

    • 매개변수 객체 만들기, 객체 통째로 넘기기 - 매개변수의 수를 줄인다.

    • 여전히 많다면 함수를 명령으로 바꾸기를 고려한다.

  • 추출할 코드 덩어리는 어떻게 찾아낼까?

    • 주석을 참고하자.

    • 조건문, 반복문도 추출 대상이 된다.

      • 조건문은 조건문 분해하기

      • switch는 case마다 함수 추출하기

      • 같은 switch가 여러 개면 조건부 로직 다형성으로 바꾸기

    • 반복문도 함께 추출해 독립된 함수로 만든다. - 반복문 쪼개기

3.4 긴 매개변수 목록 (Long Parameter List)

매개변수 목록이 길어지면 그 자체로 이해하기 어려울 때가 많다.

  • 다른 매개변수에서 값을 얻어올 수 있는 매개변수 - 매개변수를 질의 함수로 바꾸기

  • 사용 중인 데이터 구조에서 값을 뽑아 각각 별개의 매개변수로 전달하는 코드 - 객체 통째로 넘기기

  • 항상 함께 전달되는 매개변수들 - 매개변수 객체 만들기로 하나로 묶는다

  • 함수의 동작 방식을 정하는 플래그 역할의 매개변수 - 플래그 인수 제거하기

  • 클래스를 이용해도 매개 변수를 줄일 수 있다. 특히 여러 개의 함수가 특정 매개변수들의 값을 공통으로 사용한다면 - 여러 함수를 클래스로 묶기

    • 함수형 프로그래밍이었다면 일련의 부분 적용 함수(partially applied function)들을 생성한다고 했을 것이다

3.5 전역 데이터 (Global Data)

전역 데이터는 코드베이스 어디에서든 건드릴 수 있고, 값을 누가 바꿨는지 찾아낼 메커니즘이 없다.

버그가 발생했을 때 원인을 찾기 어렵다.

  • 변수 캡슐화하기

전역 데이터가 아주 조금만 있더라도 변화에 대처하기 위해 캡슐화하는 것이 좋다.

3.6. 가변 데이터 (Mutable Data)

함수형 프로그래밍에서는 데이터가 불변이며, 복사본을 만들어 진행하는 것이 기본 개념이다. 하지만 함수형 프로그래밍이 차지하는 비중은 여전히 적고, 변수 값을 바꿀 수 있는 언어를 사용하는 프로그래머가 더 많다.

그렇다고 다른 방법이 없는 건 아니다.

  • 갱신 로직은 다른 코드와 떨어뜨려 놓는 것이 좋다.

  • 무언가를 갱신하는 코드로부터 부작용이 없는 코드를 분리하자.

  • 가능한 세터도 제거하자.

  • 여러 함수를 클래스로, 또는 변환 함수로 묶어서 유효범위를 제한하자.

  • 참조는 값으로 바꿔 내부 필드를 직접 수정하지 말고 구조체를 통째로 교체하자.

3.7 뒤엉킨 변경 (Divergent Change)

뒤엉킨 변경은 단일 책임 원칙(Single Responsibility Principle: SRP)이 제대로 지켜지지 않을 때 나타난다.

  • 순차적으로 실행되는 게 자연스러운 맥락이라면, 다음 맥락에 필요한 데이터를 특정 데이터 구조에 담아 전달하는 식으로 단계를 분리하자(단계 쪼개기).

  • 전체 처리 과정 곳곳에서 각기 다른 맥락의 함수를 호출하는 빈도가 잦다면, 각 맥락에 해당하는 적당한 모듈들을 만들어 관련 함수를 모은다(함수 옮기기). 그러면 처리 과정이 맥락별로 구분된다.

  • 여러 맥락의 일에 관여하는 함수가 있다면 옮기기 전에 함수 추출하기부터 수행한다.

  • 모듈이 클래스라면 클래스 추출하기로 맥락별로 분리하자.

3.8 산탄총 수술 (Shotgun Surgery)

산탄총 수술은 뒤엉킨 변경과 비슷하면서도 정반대다.

이 냄새는 자잘하게 수정해야 할 클래스가 많을 때 풍긴다. 변경할 부분이 코드 전반에 퍼져 있다면 찾기도 어렵고 수정해야 할 곳을 놓치기 쉽다.

  • 함수 옮기기, 필드 옮기기로 함께 변경되는 대상을 모듈에 모아두자.

  • 비슷한 데이터를 다루는 함수가 많으면 여러 함수를 클래스로 묶자.

  • 데이터 구조를 변환하거나 보강(enrich)하는 함수들은 변환 함수로 묶자.

  • 이렇게 묶은 함수들의 출력 결과를 다음 로직으로 전달할 수 있다면 단계로 쪼개자.

  • 어설프게 분리된 로직을 함수 인라인하기나 클래스 인라인하기와 같은 인라인 리팩터링으로 하나로 합친다.

3.9 기능 편애 (Feature Envy)

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

해결하기는 쉽다.

  • 소원대로 함수를 근처로 옮겨주면 된다.

  • 일부에서만 그 기능을 편애한다면 독립 함수로 추출해 원하는 모듈로 보내준다.

  • 어디로 보낼지 명확하지 않다면, 함수를 여러 조각으로 나눈 후 각각을 적합한 모듈로 보낸다.

  • 전략 패턴, 방문자 패턴, 켄트 벡의 자기 위임(self-delegation)등은 뒤엉킨 변경 냄새를 없앨 때 활용하는 패턴으로, 함께 변경할 대상을 한데 모으는 것이다.

3.10 데이터 뭉치 (Data Clumps)

몰려다니는 데이터 뭉치는 보금자리를 따로 두자.

  • 필드 형태의 데이터 뭉치를 클래스로 추출하고, 메서드 시그니처에 있는 데이터 뭉치는 매개변수를 객체로 만들거나 객체를 통째로 넘겨 매개변수를 줄인다. 그러면 호출부가 간결해진다.

  • 데이터 뭉치인지를 판별하려면 값 하나를 삭제해보자. 나머지 데이터만으로 의미가 없다면 객체로 환생하길 갈망하는 데이터 뭉치라는 뜻이다.

3.11 기본형 집착 (Primitive Obsession)

기본형을 그대로 사용하지 말고 객체로 사용하면 의미있는 코드를 만들 수 있다.

  • 기본형으로 표현된 코드가 조건부 동작을 제어하는 타입 코드로 쓰였다면, 타입 코드를 서브클래스로 바꾸자. 이후 조건부 로직을 다형성으로 바꾸면 된다.

  • 자주 몰려다니는 기본형 그룹도 데이터 뭉치다.

3.12 반복되는 switch문 (Repeated Switches)

순수한 객체 지향을 신봉하는 사람들은 조건부 로직을 다형성으로 모조리 바꿔야 한다고도 한다.

하지만 요즘은 복잡한 타입을 지원하는 switch문을 제공하는 언어도 많다.

그러니 지향점을 바꿔서, 똑같은 조건부 로직이 여러 곳에서 반복해 등장하는 코드에 집중하자.

  • 중복된 switch문이 문제가 되는 이유는 조건절을 하나 추가할 때마다 다른 switch도 모두 바꿔야 하기 때문이다.

3.13 반복문 (Loops)

요즘은 일급 함수를 지원하는 언어가 많아졌다.

반복문을 파이프라인으로 바꿔보자.

  • filter

  • map

각 원소들이 어떻게 처리되는지 쉽게 파악할 수 있다.

3.14 성의 없는 요소 (Lazy Element)

우리는 프로그램 요소(함수, 클래스, 인터페이스 등 코드 구조를 잡는 데 활용되는 요소)를 이용하길 좋아한다.

때론 그 구조가 필요 없을 때도 있다.

  • 제거 작업에는 함수 인라인하기, 클래스 인라인하기

  • 상속을 사용했다면 계층 합치기

3.15 추측성 일반화 (Speculative Generality)

'나중에 필요할거야'라는 생각으로 당장은 필요 없는 모든 종류의 후킹(hooking) 포인트와 특이 케이스 처리 로직을 작성해둔 코드에서 풍긴다.

테스트 코드 말고는 사용하는 곳이 없는 함수나 클래스에서 흔히 발견할 수 있는데, 이런 코드를 발견하면 테스트 케이스부터 삭제한 뒤에 죽은 코드를 제거하자.

3.16 임시 필드 (Temporal Field)

특정 상황에서만 값이 설정되는 필드를 가진 클래스도 있다.

객체를 가져올 때는 당연히 모든 필드가 채워져 있으리라 기대하는 게 보통이라, 임시 필드는 갖도록 작성하면 코드를 이해하기 어렵다.

  • 이런 덩그러니 떨어진 필드들은 클래스 추출하기로 제 살 곳을 찾아준다.

  • 이후 함수 옮기기로 임시 필드 관련 코드를 모조리 새 코드로 몰아 넣는다.

  • 또한, 임시 필드들이 유효한지를 확인한 후 동작하는 조건부 로직이 있을 수 있는데, 특이 케이스 추가하기로 유효하지 않을 때를 위한 대안 클래스를 만들어 제거하자.

3.17 메시지 체인 (Message Chains)

메시지 체인은 클라이언트가 한 객체를 통해 다른 객체를 얻고, 그 객체에 또 다른 객체를 요청하는 식으로 연쇄적으로 다른 객체를 요청하는 작업이다.

이렇게 하면 클라이언트가 객체 내비게이션 구조에 종속된다.

  • 위임 숨기기

  • 함수 추출하기, 함수 옮기기로 체인 숨기기

3.18 중개자 (Middle Man)

외부로부터 세부사항을 숨겨주는 캡슐화(encapsulation) 과정에서 위임(delegation)이 자주 사용된다.

하지만 지나치면 문제가 된다. 중개자 제거하기(7.8)를 활용하여 실제로 일을 하는 객체와 직접 소통하게 하자.

3.19 내부자 거래 (Insider Trading)

모듈 사이의 거래가 많으면 결합도(coupling)이 높아지며, 모두 투명하게 처리하는 것이 좋다.

  • 은밀히 데이터를 주고 받는 모듈들이 있다면 함수 옮기기, 필드 옮기기 기법으로 떼서 사적으로 처리한 부분을 줄이자.

  • 여러 모듈이 공통 관심사를 가지면 제 3의 모듈로 분리하거나 위임 숨기기로 다른 모듈이 중간자 역할을 하도록 하자.

  • 상속 구조에서는 부모 자식 사이에 결탁이 생기곤 하는데, 부모 품을 떠나야 할 때가 온다면 서브클래스를 위임으로, 또는 슈퍼클래스를 위임으로 바꾸자.

3.20 거대한 클래스 (Large Class)

한 클래스가 너무 많은 일을 하려다 보면 필드 수가 상당히 늘어나며, 필드가 너무 많으면 중복 코드가 생기기 쉽다.

  • 클래스 추출하기로 필드들 일부를 따로 묶자. (e.g., @Embeddable, @Embedded in Spring Data JPA)

  • 일반적으로 접두사, 접미어가 같은 필드들이 함께 추출할 후보들이다.

    • 클래스 추출이 아니라 원래 클래스와 상속 클래스로 만드는게 좋다면, 슈퍼클래스 추출하기, 타입 코드를 서브클래스로 바꾸기를 적용하는 게 더 쉬울 수 있다.

코드량이 너무 많은 클래스도 중복 코드를 만들고 혼동을 일으킬 수 있다.

  • 간단한 해법은 클래스 안에서 자체적으로 중복을 제거하는 것이다.

  • 개별 클래스로 분리해 단서를 얻을 수 있다.

3.21 서로 다른 인터페이스의 대안 클래스들 (Alternative Classes with Different Interfaces)

클래스의 장점은 필요에 따라 다른 클래스로 교체할 수 있는 것인데, 이러려면 인터페이스가 같아야 한다.

  • 함수 선언 바꾸기로 메서드 시그니처를 일치시킨다.

  • 부족하면 함수 옮기기로 인터페이스가 같아질 때까지 필요한 동작을 클래스 안으로 밀어 넣자.

3.22 데이터 클래스 (Data Class)

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

데이터 저장 용도로만 쓰다 보니 다른 클래스가 너무 깊이까지 함부로 다룰 때가 있다.

  • public 필드는 레코드 캡슐화하기로 숨기자.

  • 변경하면 안되는 필드는 세터를 제거하자.

데이터 클래스는 필요한 동작이 엉뚱한 곳에 정의돼 있다는 신호일 수 있다.

  • 이런 경우 클라이언트 코드를 데이터 클래스로 옮기기만 해도 대폭 개선된다.

  • 예외가 있는데, 특히 다른 함수를 호출해 얻은 결과 레코드(데이터 객체)로는 동작 코드를 넣을 이유가 없다.

    • e.g., 단계 쪼개기의 결과로 나온 중간 데이터 구조 (불변이므로, 캡슐화할 필요가 없다. 게터도 없이 공개해도 된다.)

3.23 상속 포기 (Refused Bequest)

서브클래스는 부모로부터 메서드와 데이터를 물려받지만, 관심이 있는 걸 제외하고 제거하려면 어떻게 할까?

  • 같은 계층에 서브클래스를 하나 만들고 메서드 내리기, 필드 내리기로 물려받지 않을 부모 코드를 새로 만든 서브클래스로 넘기면 부모 클래스에는 공통된 부분만 남는다.

  • 상속 포기 냄새는 특히 서브클래스에서 부모의 동작은 필요하지만 인터페이스를 따르고 싶지 않을 때 난다.

    • 이럴 땐 서브클래스를 위임으로 바꾸거나 슈퍼클래스를 위임으로 바꿔 상속 메커니즘에서 벗어나자.

3.24 주석 (Comments)

주석은 악취가 아니라 향기다.

문제는 주석이 장황하게 달린 원인부터가 코드를 잘못 작성했기 때문인 경우가 의외로 많다.

주석을 남겨야겠다는 생각이 들면, 가장 먼저 주석이 필요 없는 코드로 리팩터링해본다.

일단 주석이 필요하다면 함수로 추출해보자.

뭘 할지 모르겠다면 주석을 달아두면 좋다.

  • 진행상황, 확실하지 않은 부분 등

  • 코드를 작성한 이유

이렇게 주석을 달아두면 나중에 코드를 수정할 프로그래머, 특히 건망증이 심한 프로그래머에게 도움될 것이다.

Last updated