[패턴을 활용한 리팩토링] 4장. 코드 속에 냄새
가장 흔한 설계 문제는 다음과 같다.

물론 이런 목록은 코드에서 개선이 필요한 부분을 찾아내는데 도움이 될 것이다. 그러나 이것만으로는 너무 막연하다. 우리가 실제로 부딪히는 문제는 그리 간단하지 않기 떄문이다. 어떤 코드는 겉으로 동일하게 보이지는 않지만 사실은 중복된 것일 수도 있다. 그리고 코드의 의미가 명확한지 아닌지를 판단할 기준이 빠져있고, 복잡한 코드와 단순한 코드를 구별할 기준도 들어있지 않다.

코드의 냄새는 메서드, 클래스, 상속구조, 패키지(이름 공간, 모듈), 전체 시스템 등 모든 곳에서 발생하는 문제를 목표로 한다. 그리고 기능에 대한 욕심(Feature Envy), 기본 타입에 대한 강박 관념(Primitive Obsession), 추측성 일반화(Speculative Generality)와 같은 냄새 이름은 프로그래머들이 설계 문제를 논의할 때 사용할 수 있는 풍부하고 다채로운 어휘가 된다.

이 책에서 설졍하는 리팩토링이 Fowler와 Beck이 설명한 22개의 코드 냄새 중 어떤 것들을 처리하는지 살펴보자.

냄새 리팩터링
중복된 코드(Duplicated Code) Form Template Method

Introduce Polymorphic Creation with Factory Method

Chain Constructors

Replace One/Many Distinction with Composite

Extract Composite

Unify Interface with Adapter

Introduce Null Object
긴 메서드(Long Method) Compose Method

Move Accumulation to Collection Parameter

Replace Conditional Dispatcher with Command

Move Accumulation to Visitor

Replace Conditional Logic with Strategy
복잡한 조건문(Conditional Com-plexity) Replace Conditional Logic with Strategy

Move Embellishment to to Decorator

Replace State-Altering conditionals with State

Introduce Null Object
기본 타입에 대한 강박관념(Primitive Obses-sion) Replace Type Code with class

Replace State-Altering Conditionals with State

Replace Conditional Logic with Strategy

Replace Implicit Tree With Interpreter

Move Embellishment to Decorator

Encapsulate Composite with Builder
추잡한 호출(Indecent Exposure) Encapsulate Classes With Factory
문어발 솔루션(Solution Sprawl) Move Creation Knowledge to Factory
인터페이스가 서로 다른 대체 클래스(Alternative Classes with Different Interfaces) Unify Interfaces with Adapter
게으른 클래스(Lazy Class) Inline Singleton
거대한 클래스(Large Class) Replace Conditional Dispatcher with Command

Replace State-Altering Conditionals with State

Replace Implicit Language With Interpreter
Switch Replace COnditional Dispatcher with Command

Move Accumulation to Visitor
조합의 폭발적 증가(Combinatorial Explostion) Replace Implicit Language with Interpreter
괴짜 솔루션(Oddball Solution) Unify Interfaces With Adapter

중복된코드

중복된 코드는 소프트웨어에서 가장 흔한 악취다. 중복에는 명백한 중복과 잠재된 중복이 있는데, 명백한 중복이란 완전히 동일한 코드가 존재하는 것을 의미하고, 잠재된 중복이란 구조나 처리 단계에 있어 겉으로는 다르게 보이지만 본질적으로 같은 부분이 존재한다는 것을 뜻한다.

상속 구조에서 서브 클래스 사이의 명백한 또는 잠재적인 중복은 보통 Form Template Method 리팩토링을 통해 제거할 수 있다. 여러 서브 클래스에서 각각 구현한 어떤 메서드의 내부가 객체 생성 단계만 제외하고는 서로 거의 비슷한 경우에는, Introduce Polymorphic Creation with Factory Method 을 적용한후, Template Method 패턴을 도입하면 더 많은 중복 코드를 제거할 수 있다.

한 클래스의 여러 생성자 사이에 중복된 코드가 있다면 보통 Chain Constructors 리팩토링을 통해 중복을 제거할 수 있다.

처리할 객체가 하나인지 여러 개인지를 구별하여 별도의 코드로 분기하고 있다면 Replace One/Many Distinctions with Composite 리팩토링을 통해 중복을 제거.

한 계층 구조의 서브 클래스들이 Composite 기능을 각자 구현하고 있고 그 구현이 동일하다면, Extract Composite 리팩토링을 사용할 수 있다.

비슷한 일을 하는 여러 객체를 다루는 데 있어 인터페이스가 다르다는 이유만으로 각각의 별도로 처리해야 한다면, Unify Interfaces with Adapter 리팩토링을 통해 중복된 처리 로직을 제거할 수 있다.

객체가 널인 경우에 처리하기 위한 조건 로직이 시스템 전체에 걸쳐 중복되어 있다면, Introduce Null Object 리팩토링을 통해 중복을 제거하고 시스템을 단순하게 할 수 있다.

긴 메소드

짫은 메소드가 긴 메서드보다 좋은 몃 가지 이유를 설명하는데,
가장 중요한 것은 로직의 공유와 관계가 있다. 두 개의 긴 메소드는 중복된 코드를 가질 가능성이 크다. 그러나 긴 메서드를 작은 조각으로 나누면 로직을 공유할 수 있는 방법이 보일 수도 있다.

또한 메서드 길이가 짧은 것이 코드를 이해하는 데 도움이 된다고 한다. 주어진 코드 덩어리가 무슨 일을 하는 것인지 이해하기 힘들 경우, 이 부분을 짧은 메서드로 뽑아내 이름을 잘 지어주면, 원래 코드를 이해하기가 더 쉬워질 것이다. 시스템이 짧은 메서드들로 구성되어 있다면 이해하기가 더 쉽고 중복도 적기 때문에, 확장과 유지보수가 쉬워진다.

짧은 메서드의 적정 길이는 어느 정도 일까? 나는 메서드의 길이를 10줄 이하로 하고, 대부분의 메서드는 1~5줄 정도가 되도록 하는 것이 좋다고 생각한다. 대부분의 메서드를 짧게 만든다면, 긴 메서드가 몇 개 정도 있어도 상관없다. 물론 그 긴 메서드도 이해가 쉽고 중복 코드를 포함하지 않아야 한다는 것은 두말할 필요 없다.

어떤 프로그래머들은 짧은 메서드를 많이 호출하는 연쇄 호출과 관련된 성능 문제를 걱정해 메서드를 길게 만들기도 한다. 그러나 이것은 다음과 같은 이유에서 불리한 선택이다. 첫째, 훌룡한 설계자는 조급하게 코드 최적화를 하지 않는다. 둘째, 짧은 메서드를 연쇄 호출하는 것은 대부분의 경우 성능에 아무런 영향을 미치지 않는다. 이것은 프로파일러를 통해 확인할 수 있는 사실이다. 셋째, 성능 문제가 발생하는 경우, 짧은 메서드를 포기하지 않으면서도 리팩터링을 통해 성능을 향상 시킬 수 있다.

나는 긴 메서드와 마주치면 Compose Method 리팩토링을 통해 짧은 메서드들로 나누고 싶은 충동을 느낀다. 이 작업에는 Extract Method 리팩토링을 적용하는 것도 포함된다. 그리고 그 과정에서, 어떤 변수가 쌓이고 있다는 것을 반견하면 Move Accmulation to Collection Parameter리펙토링의 적용을 고려한다.

여러 가지 요청을 처리하기 위한 거대한 switch 문 때문에 메서드가 길어졌다면, Replace Conditional Dispatcher with Command 리팩터링을 통해 메서드 길이를 줄일 수 있다.

서로 다른 인터페이스를 가진 다양한 클래스로부터 데이터를 수집하기 위한 switch 문을 사용한다면, Move Accumulation to Visitor 리팩터링을 통해 메서드의 길이를 줄일 수 있다.

여러 종류의 알고리즘과 조건 로직(런타임에 적절한 알고리즘으로 분기하기위한)을 포함하고 있어 메서드가 길어졌다면, Replace Conditional Logic with Strategy 리팩터링을 통해 메서드 길이를 줄일 수 있다.

복잡한 조건문

이해하기 쉽고 길이도 몇 줄 되지 않는 초기의 조건 로직에는 아무런 문제가 없다. 그러나 불행히도 조건문이 이렇게 유지되는 경우는 거의 없다. 예를 들어 몇가지 새로운 기능을 추가하면서 조건 로직이 갑자기 복잡하게 팽창할 수 있다.

만약 조건 로직이 그 클래스의 핵심 동작 외의 특별한 경우에 대한 몇몇 동작을 제어하는 것이라면, Move Embellishment to Decorator 리펙터를 사용할 수 있다.

객체의 상태 전이를 제어하는 복잡한 조건식의 경우, Replace State-Altering Conditionals with State을 통해 로직을 단순화할 수 있다.

객체가 널인 경우에 처리하기 위한 조건 로직이 시스템 전체에 걸쳐 중복되어 있다면, Introduce Null Object 리팩토링을 통해 중복을 제거하고 시스템을 단순하게 할 수 있다.

기본 타입에 대한 강박관념

정수, 문자열, 부동소수점수, 배열 그리고 다른 저수준 언어 요소와 같은 기본 타입은 많은 사람들이 사용하므로 일반적이다. 반면에 클래스는 특정 목적을 위해 만드는 것이므로 필요한 만큼 특수해져야 한다. 많은 경우, 어떤 것을 모델화하려면 기본 타입보다는 클래스를 이용하는 것이 더 단순하고 자연스럽다.

또한, 한번 만들어 놓은 클래스는 시스템의 다른 부분에서도 재사용할 수 있다.

이것은 보통 높은 수준의 추상화가 어떻게 코드를 명확하고 단순하게 만드는지 보지 못했을 때 나타난다. 파울러가 설명한 여러 리팩터링에는 이 문제를 다루기 위한 가장 일반적인 해결책이 포함되어 있다. 이 책에서는 그에더해 몇 가지 해결책을 더 제시한다.

만약 클래스에서 로직의 흐름을 제어하는 변수가 기본 타입이고 그에 대한 타입 안전성이 보장되어 있지 않으면,(즉, 클라이언트에서 안전하지 않거나 적절하지 않은 값을 할당할 수 있으면), Replace Type Code with Class 리팩터링의 적용을 고려할 수 있다. 결과로 나온 코드는 타입 안전성이 보장되고, 새로운 동작을(기본 타입을 사용할 때에 비해) 쉽게 추가할 수 있게 된다.

만약 객체의 상태전이가 기본 타입의 값을 기준으로 한 복잡한 조건 로직을 통해 제어한다면, Replace State-Altering Conditionals with State 리팩터링을 사용할 수 있다. 그 결과로 각 상태를 나타내는 여러 가지 클래스와 단순화된 상태 전이 로직이 나올 것이다.

만약 복잡한 조건 로직이 기본 타입의 값에 따라 실행시킬 알고리즘을 선택한다면, Replace Conditional Logic With Strategy 리팩터링의 적용을 고려할 수 있다.

만약 문자열과 같은 기본 타입을 통해서 묵시적으로 트리 구조를 생성하고 있다면, 코드는 작업하기 어렵고 오류를 범하기 쉬우며 중복도 많아질 것이다. Replace Implicit Tree with Composite 리팩터링을 적용하면 이런 문제를 해결 할 수 있다.

만약 어떤 클래스에 입력되는 기본 타입 값의 수많은 조합을 지원하기 위한 많은 메서드가 있다면, 이것은 묵시적인 언어가 있음을 의미한다. 이런 경우에는 Replace Implicit Language With Interpreter 리팩터링의 적용을 고려해야 한다.

만약 클래스에 핵심이 아닌 추가 기능을 덧붙이기 위해 사용되는 기본 타입 값이 존재한다면, Move Embellishment to Decorator 리팩터링을 사용할 수 있다.

마지막으로, 클래스를 사용한다 해도 그 클래스가 너무 원시적이라 클라이언트에서 쉽게 사용할 수 없는 경우가 있다. 다루기 까다로운 컴포짓을 사용하는 경우가 이에 해당 될 것이다. 이럴 때는 Encapsulate Composite With Builder 리펙터링을 통해 클라이언트가 컴포짓을 쉽게 생성할 수 있도록 만들 수 있다.

추잡한 노출

이 냄새는 David Parnas가 명명한, 그 유명한 ‘정보 은폐(information Hiding)’가 부족함으로 나타낸다. 이 냄새는 클라이언트에게 보이지 말아야 할 메서드나 클래스가 공개적으로 보일 때 발생한다. 이것은 클라이언트가 중요하지 않거나 알필요가 없는 코드까지도 신경을 써야 함을 뜻하고, 설계를 복잡하게 만드는 데 한 몫 한다.

Encapsulate Classes with Factory 리펙터링을 이용하면 이 냄새를 제거할 수 있다. 클라이언트에 유용하다고 무조건 공개할 필요는 없다. 즉, 생성자를 public으로 선언하지 않아도 된다. 어떤 클래스는 공통 인터페이스를 통해서만 참조돼야 한다. 해당 클래스의 생성자를 public이 아니도록 하고, 그 객체를 생성할 때 팩터리를 통하도록 하면 된다.

문어발 솔루션

어떤 기능을 수행하는 데 사용되는 코드나 데이터가 여러 클래스에 걸쳐 있다면, 문어발 솔류션의 냄새가 풍기는 것이다. 특정 기능을 가장 잘 수용하도록 설계를 단순화하고 통합하는 데 충분한 시간을 보내지 않고 시스템에 해당 기능을 서둘러 추가할 때 이 냄새가 발생한다.

기능을 추가하거나 수정할 때 시스템의 다른 많은 부분도 같이 수정해야 하는 것을 산탄총 수술이라고 하는데, 문어발 솔루션의 폐해! 문어발 솔루션과 산탄총 수술이 나타내는 문제는 같지만, 감지되는 경로는 다르다. 문어발 솔루션은 관찰을 통해 감지되지만, 산탄총 수술은 작업을 통해 감지된다.

Move Creation Knowledge to Factory 은 객체 생성 기능이 여기저기에 분산되어 있는 문제를 해결하기 위한 리펙터링이다.

인터페이스가 서로 다른 대체 클래스

어떤 두 클래스의 인터페이스는 다르지만 클래스가 서로 상당히 비슷한 경우 발생한다. 만약 두 클래스에서 비슷한 점을 찾을 수 있다면, 리팩터링을 통해 공통 인터페이스를 공유하도록 만드는 것이 가능하다. 그러나 코드에 대한 제어권이 없어 클래스의 인터페이스를 직접 수정할 수 없을 때도 있다. 써드파트 라이브러리를 사용하는 경우가 대표적인 예다. 이런 경우, Unify Interfaces with Adapter 리팩터링을 통해 두 클래스를 위한 공통 인터페이스를 만들 수 있다.

게으른 클래스

이 냄새를 묘사할 때, ‘자신의 존재 비용을 감당할 만큼 충분한 일을 하지 않는 클래스는 삭제돼야 한다.’고 섰다. 자신의 존재 가치를 보이지 못하는 싱글턴을 접하는 것은 드문 일이 아니다. 사실 싱글턴을 사용하면 전역 데이터를 사용하는 것과 마찬가지로 설계를 지나치게 종속적으로 만드는 대가를 치러야 한다. Inline Singleton을 이용하면 존재가치가 없는 싱글턴을 깔끔하게 제거할 수 있다.

거대한 클래스

지나치게 많은 인스턴스 변수가 있는 것은 클래스 하나가 너무 많은 일을 하려 함을 나타내는 징조라고 언급했다. 보통 거대한 클래스는 지나치게 많은 책임을 떠맡고 있기 때문에 거대해진 경우가 많다. Extract Class와 Extract Subclass는 이 냄새를 다루는 데 사용되는 주요 리펙터링으로, 책임을 다른 클래스로 옮기는 것이다. 이 책에 담긴 패턴을 고려한 리펙터링에서는 클래스의 크기를 줄이는 데 이런 리팩터링을 활용한다.

다양한 요청에 따라 서로 다른 동작을 수행하는 클래스에 Replace Conditional Dispatcher with Command 리팩터링을 적용해 각 동작을 별도의 커맨드 클래스로 뽑아내면, 그 크기를 상당히 줄일 수 있다.

상태 전이 코드로 가득 찬 거대한 클래스의 크기를 줄이려면, Replace State-Altering Conditionals with State 리팩터링을 통해 각 상태에 대한 처리를 별도의 State클래스로 분리하면 된다.

Replace Implicit Language with Interpreter 리펙토링은 다양한 입력 조건을 처리하느라 수많은 코드가 잠재적으로 중복되어 있는 거대한 클래스를 인터프리터로 대체해 간단하게 만드는 것이다.

Switch 문

원래부터 나쁜 것은 아니다. 설계를 복잡하게 만들거나 필요 이상으로 융통성 없게 만들 때만 나쁘다. 이런 경우에는 switch문을 좀더 객체지향적인, 다형성을 이용하도록 리펙터링하는 것이 좋다.

Replace Conditional Dispatcher with Command 리팩터링은 거대한 swich문을 좀더 객체지향적인, 다형성을 이용하도록 리팩터링하는 것이 좋다.

Replace Conditional Dispatcher with Command 은 거대한 switch문을 커맨드 객체(조건 로직에 의존하지 않고, 런타임에 검색되어 호출되는)의 집합으로 쪼개는 방법이다.

Move Accumulation to Visitor에서는 서로 다른 인터페이스를 갖는 클래스의 인스턴스들로부터 데이터를 얻기 위해 switch문을 사용하는 예제를 설명한다. 이 코드를 리팩터링해 Visitor 패턴을 도입하면, 조건 로직도 필요 없지고 설계도 더 융통서 있게 된다.

조합의 폭발적 증가

이 냄새는 중복의 잠재적 형태로, 다양한 종류의 데이터나 객체를 이용하지만 결국 하는 일은 동일할 때 발생한다.

예를 들어 어떤 클래스에 쿼리 수행을 위한 많은 메서드가 있다고 하자. 메서드는 각각 특정 조건과 데이터를 이용해 쿼리를 수행한다. 지원해야 하는 특수한 쿼리를 많을수록, 더 많은 쿼리 메서드를 만들어야 한다. 따라서 쿼리 수행을 위한 다양한 방법을 처리하기 위해 메서드의 수가 폭발적으로 증가한다. 즉, 묵시적으로 일종의 쿼리 언어가 있는 것과 마찬가지다. Replace Implicit Language With Interpreter 리팩터링을 이용하면 이러한 조합의 폭발적 증가 냄새를 제거 할 수 있다.

괴짜 솔루션

어떤 문제가 시스템 전체에서 한 가지 방법으로, 같은 문제가 특정 부분에서만 다른 방법으로 해결된다면, 이 다른 해결 방법은 괴짜 또는 비일관적인 솔루션이다. 이 냄새가 존재함은 잠재적 중복이 있음을 암시한다.

이런 중복을 제거하려면, 먼저 마음에 드는 솔루션을 결정한다. 때로는 가장 적게 사용되는 솔루션을 결정한 후, Substitue Algo 리팩터링을 통해 시스템 전체에서 사용되는 솔루션을 일관적으로 만든다. 그렇게 하고 나면 솔루션의 모든 인스턴스를 한곳에 모아 중복을 제거할 수 있을지도 모른다.

괴짜 솔루션 냄새는, 보통 비슷한 부류의 클래스를 사용하는 데 있어 선호하는 방법이 있지만 일부 클래스의 인터페이스가 나머지와 달라 일관적 방법으로 사용할 수 없는 경우에 발생한다. 이런 경우, 다른 모든 클래스와 동일하게 다룰 수 있도록 공통 인터페이스를 만들기 위해 Unify Interfaces with Adapter 리팩터링을 이용할 수 있는다, 그 후에 중복되 처리 로직이 발견하면 제거할 수 있다.