리팩토링 2판 요약 정리 Ch.3


리팩토링 2판을 번역/요약한 글입니다.


코드에서 나는 악취 (Bad Smells in Code)

  • 언제 리팩토링을 시작할 것인지(그리고 언제 멈출 것인지)를 아는 것은 리팩토링의 작동 원리를 아는 것 못지 않게 중요하다.
  • 인스턴스 변수를 제거하는 법이나 계층을 만드는 법을 설명하는 것은 쉽다. 하지만 이것들을 언제 해야 하는지를 설명하는 것은 쉽지 않다.
  • 우리의 경험에 따르면, 어떠한 (정량적) 지표도 숙련된 사람의 직관보다 못했다. 이제 우리는 리팩토링으로 개선할 수 있는 문제들에 대한 암시를 주려고 한다.
  • 이제부터 당신은 인스턴스 변수가 몇 개 정도 되면 많은 것인지, 혹은 메서드의 코드가 몇 줄 이상이면 많은 것인지 판단할 수 있는 감각을 기르려고 노력 해야 한다.

미스테리한 이름 (Mysterious Name)

컴퓨터 과학에서 어려운 점은 두 가지다: 캐시 무효화(invalidation)와 이름 짓기. - 필 칼튼

  • 코드는 추리 소설이 아니다. 국제적으로 미스테리한 인물이 되는 것에 대한 환상을 가질 수 있겠으나, 우리의 코드는 평범하고 깔끔해야 한다.
  • 깔끔한 코드의 가장 중요한 부분 중 하나는 좋은 이름이다. 따라서 함수, 모듈, 변수, 클래스 등이 무엇을 하는지, 그리고 어떻게 쓰는지를 잘 나타내는 이름을 (많은 생각을 통해) 지어야 한다.
  • 아마 우리가 하는 리팩토링의 거의 대부분은 이름을 바꾸기가 될 것이다.
  • 흔히 사람들은 이름을 바꾸는 리팩토링을 할 필요 없다고 생각하는데, 좋은 이름은 추후에 코드를 이해하는 데 소요되는 시간을 아껴줄 것이다.
  • 좋은 이름이 떠오르지 않는다면, 무언가 설계에 문제가 있지는 않은지 검토해 볼 필요가 있다. 좋은 이름을 골똘히 생각하다 보면 (설계에 대해 생각해 봄으로써) 코드가 상당히 간결해지는 경우도 있다.

중복된 코드 (Duplicated Code)

  • 코드 구조가 두 번 이상 반복된다면 하나로 합치는 것을 권장한다. 중복된 코드가 있다면 코드를 읽을 때도 무언가 차이가 있는지 면밀히 살펴봐야 하고, 중복된 코드를 수정하려고 하면 중복된 코드들 모두를 같이 바꿔주어야 한다.
  • 중복된 코드의 가장 단순한 예로는, 한 클래스에 존재하는 두 메소드가 같은 문장을 사용하는 경우이다. 이 경우 Extract Function를 써서 추출된 함수를 두 곳에서 호출하는 형태로 바꾸는 것이 좋다.
  • 만약 코드가 똑같진 않고 비슷하다면 Slide Statements를 이용하여 비슷한 부분을 하나로 묶을 수 있는지 살펴보는 것을 권장한다.
  • 공통 부모 클래스의 자식 클래스 내부에 중복되는 부분이 존재하는 경우엔 Pull Up Method를 적용할 수 있다.

긴 함수 (Long Function)

  • 우리의 경험상, 함수들이 작을 때 (함수 코드가 짧을 때) 프로그램이 오랫동안 잘 동작했었다. 이러한 프로그램을 처음 맞닥뜨리는 개발자는 (함수들이) 끊임없이 서로에게 위임을 하는 모습을 보고 실질적으로 어떠한 계산도 일어나지 않는 것처럼 보일 수도 있다.
  • 하지만 몇 년 동안 이러한 코드를 다뤄봤다면 함수들이 작은 것이 얼마나 중요한지 깨달았을 것이다.
  • 작은 함수를 사용해야 indirection의 이점을 누릴 수 있다.
  • 초창기 프로그래밍 언어에선 서브루틴을 호출할 때의 오버헤드로 인해 사람들이 짧은 함수를 사용하는 것을 꺼려 했다. 그러나 현대의 언어들은 이러한 오버헤드를 대부분 제거했다. 하지만 함수가 무슨 일을 하는지 살펴보려면 코드 이리저리 왔다 갔다 해야 하기 때문에 코드를 읽는 사람에겐 여전히 오버헤드가 존재하긴 한다.
  • 개발 환경을 통해 이러한 오버헤드를 줄일 수 있긴 하지만(vscode에서처럼 동시에 두 코드를 읽는다든지..), 작은 함수를 이해하는 핵심은 결국 좋은 이름이다. 함수의 이름이 잘 지어져있다면 함수의 body를 보지 않고서도 그 함수가 무엇을 하는지 알아낼 수 있다.
  • 좋은 이름의 이러한 이점을 최대한 활용해서, 함수를 공격적으로 쪼개야 한다. 우리는 주석을 달고 싶을 때 주석 대신 함수를 작성하는데, 이렇게 하면 우리가 주석으로 달려고 했던 내용을 이름과 함께 실제 코드로 작성함으로써 우리의 의도를 더욱 명확히 드러낼 수 있게 된다.
  • 함수 호출부가 원래의 코드보다 길어진다고 하더라도 함수로 분리한다. 단, 반드시 함수의 동작을 잘 설명하는 이름을 지어야 한다.
  • 여기서 핵심은 함수의 길이가 아니라 함수가 하는 동작과 그것을 구현한 코드 간의 괴리이다. 즉, 어떤 코드가 자신이 무엇을 하는지 잘 설명하지 못할 수록 함수로 분리하는 것이 좋다.
  • 큰 함수를 작은 함수로 분리할 땐 십중팔구 Extract Function 방법을 쓰면 된다. 함수 내에서 서로 연관된 부분들을 묶어 새로운 함수로 분리하라.
  • 매개 변수와 임시 변수가 많은 함수의 경우, 코드를 추출해서 새로운 함수로 만드는 것이 힘들 수 있다. Extract Function 방법을 쓰게 되면 분리된 함수에 너무 많은 인자를 넘겨야 해서 결국 분리한 효과를 누릴 수 없게 될 수 있다.
  • Replace Temp with Query를 사용해서 임시 변수를 줄일 수 있고, Introduce Parameter ObjectPreserve Whole Object기법을 사용해서 함수로 전달되는 매개 변수의 개수를 줄일 수 있다.
  • 만약 위 기법들을 사용했음에도 불구하고 여전히 매개 변수와 임시 변수가 많이 남아있는 경우, 최후의 수단으로 Replace Function with Command를 적용해볼 수 있다.
  • 추출할 코드를 판단하는 좋은 기준 중 하나는 바로 주석이다. 주석이 달려있는 코드를 함수로 분리한 후, 주석을 기반으로 이름을 지으면 된다. 코드가 더 명확해질 수만 있다면 비록 한 줄짜리 함수가 되더라도 분리하는 것을 추천한다.
  • 조건문과 반복문 또한 좋은 기준이 될 수 있는데, Decompose Conditional을 사용해서 조건문을 분리할 수 있다. 거대한 switch 구문도 Extract Function을 통해 한 줄짜리 함수 호출문으로 바꿀 수 있다. 만약 똑같은 조건에 의해 분기하는 switch 구문이 여러 개 있다면 Replace Conditional with Polymorphism을 적용할 수 있다.
  • 반복문의 경우 반복문 자체와 반복문 내부의 로직을 분리하여 함수로 만드는 것을 추천한다. 만약 이렇게 추출된 함수의 이름 짓는 것이 어렵다면 반복문이 너무 많은 일을 하고 있진 않은지 살펴보라. 만약 두 가지 이상의 일을 한다면 Split Loop을 적용해 볼 수 있다.

너무 많은 매개 변수 (Long Parameter List)

  • 프로그래밍을 처음에 배울 때, 우리는 함수가 필요로 하는 모든 것을 매개 변수로 넘기라고 배운다. 만약 이렇게 안 하면 글로벌 변수를 사용해야 하기 때문에 이는 꽤 합리적인 가르침이다. 하지만 매개 변수가 너무 많은 경우 그 자체로 혼란스러울 수 있다.
  • 만약 다른 매개 변수를 통해 값을 구할 수 있는 매개 변수가 있다면 Replace Parameter with Query를 사용하여 해당 매개 변수를 제거할 수 있다. 또한, Preserve Whole Object를 사용하여 기존에 존재하는 자료 구조에서 데이터를 추출하여 매개 변수로 넘기는 대신 원본 자료 구조 그 자체를 넘기도록 할 수 있다.
  • 여러 개의 매개 변수들이 항상 세트로 묶여 사용된다면 Introduce Parameter Object를 적용하여 하나의 객체로 묶을 수 있다.
  • 일종의 “flag”로 사용되는 매개 변수의 경우, Remove Flag Argument기법을 적용해 볼 수 있다.
  • 클래스를 사용하여 매개 변수의 개수를 줄일 수도 있다. 특히, 여러 함수들이 동일한 매개 변숫값을 공유하는 경우 더욱 도움이 된다. 이 경우 Combine Functions into Class를 사용하여 공유되어 사용되는 변수를 클래스 필드 변수로 바꿀 수 있다. 함수형 프로그래밍 관점에서 보자면 이는 부분 적용 함수들을 만드는 것과 흡사하다.

전역 데이터 (Global Data)

  • 프로그래밍을 처음 배울 때부터, 우리는 전역 변수(데이터)의 위험성에 대해 교육을 받는다. 전역 변수는 어디서나 접근이 가능하기 때문에 어디서든 그 값을 변경할 수 있다. 하지만 전역 변수를 수정하고 있는 부분을 찾는 것은 쉽지 않다. 이로 인해 전역 변수와 관련된 버그가 발생해도 어디서 잘못되었는지를 찾기 쉽지 않다.
  • 가장 흔한 예로 “전역 변수”를 들었지만, 이러한 문제는 클래스 변수와 싱글톤에서도 나타나는 문제이다.
  • 이러한 문제를 방지할 수 있는 핵심적인 방법은 바로 Encapsulate Variable이다. 일단 전역 변수를 함수로 감싸게 되면 적어도 어디서 해당 변수를 참조하고 있는지를 비교적 쉽게 파악할 수 있다. 그런 다음 변수의 스코프 범위를 최대한 축소하는 것이 바람직하다.
  • 전역 변수가 가변(mutable)인 경우 상황은 더욱 끔찍하다. 일단 프로그램이 시작되면 값이 바뀌지 않는다는 보장이 있는 경우엔 그나마 안전하다고 할 수 있다.
  • 파라켈수스가 “약과 독의 차이는 사용량이다”라고 한 것처럼, 전역 데이터가 적은 경우 어떻게든 잘 관리할 수 있지만, 전역 데이터가 많아지면 이를 관리하는 게 기하급수적으로 어려워진다.
  • 하지만 전역 데이터가 몇 개 없다고 해도, 추후 있을지도 모르는 변화에 대응하기 위해 이들을 캡슐화하는 것을 권장한다.

가변 데이터 (Mutable Data)

  • 데이터를 변경하는 경우, 예기치 못한 버그가 발생할 수 있다. 어떤 데이터에 대해, 그 데이터를 사용하는 곳에서 예상하는 것과는 다르게 변경을 해버리면 버그가 발생할 수 있고, 특히 이러한 일이 특별한 조건에서만 일어나는 일이라면 디버깅하기 매우 어렵다.
  • 이러한 이유로 함수형 프로그래밍에선 아예 데이터를 수정할 땐 그 데이터를 직접 수정하는 것이 아니라, 그 데이터를 기반으로 한 새로운 버전(new copy)의 데이터를 새로 “생성” 하도록 하고 있다.
  • Encapsulate Variable 기법을 활용하여 데이터 변경이 특정 함수 내에서 일어나게 함으로써 데이터의 변화를 모니터링하기 쉽게 할 수 있다.
  • 만약 어떤 변수가 기존의 것과 다른 것을 저장하도록 변경된다면, Split Variable을 사용해서 두 값을 분리하여 관리하도록 하는 것을 권장한다.
  • Slide StatementsExtract Function을 활용하여 데이터를 업데이트 하는 부분을 최대한 따로 빼내어 함수를 순수 함수로 바꿀 수 있다.
  • API에선 Separate Query from Modifier를 사용하여, 정말 필요한 경우가 아니라면 side effect가 존재하는 코드를 호출자가 사용하지 못하도록 하는 것을 추천한다. 또한 최대한 빨리 Remove Setting Method를 적용하라.
  • 여러 군데에서 계산할 수 있는 (따라서 변경 가능한) mutable 데이터에선 상당히 불쾌한 냄새가 난다. 이와 같은 Mutable 데이터는 버그와 혼란의 근원일 뿐만 아니라, 딱히 필요가 없는 존재이다. 이러한 Mutable 데이터엔 Replace Derived Variable with Query를 사용하여 냄새를 제거할 수 있다.
  • 물론, mutable 데이터의 스코프 범위가 몇 줄 안되는 경우엔 딱히 문제 될 게 없을 수 있으나, 스코프가 커짐에 따라 리스크도 커지게 된다. Combine Functions into Class 혹은 Combine Functions into Transform과 같은 방법을 사용하여 변수를 업데이트하는데 필요한 코드의 수를 제한하라.
  • 만약 (mutable) 변수가 어떤 내부적인 구조를 가지고 있다면 일반적으로 그 자리에서 변경하기보단 구조 전체를 갈아치우는 게 더 좋을 수 있다. 이 경우 Change Reference to Value를 사용하라.

산발적인 수정 (Divergent Change)

  • 소프트웨어는 변경에 유연해야, 즉 “소프트”해야 소프트웨어답다.
  • 흔히 어떤 모듈이 서로 다른 이유로 인해 각기 다른 방식으로 변경이 일어날 때 산발적으로 수정이 일어난다고 한다.
  • 예를 들면 “새로운 DB가 추가되면 여기 3개의 함수를 바꿔야지”, “새로운 금융 상품이 추가되면 저기 4개의 함수를 바꿔야지”와 같은 것들이 (어떤 한 모듈 내에서) 산발적 수정이 존재함을 알리는 신호이다.
  • DB와 관련된 작업과 금융과 관련된 로직은 서로 다른 context 이므로 이들을 별개의 모듈로 분리하는 것이 정신건강에 좋다. 이렇게 하면, 특정 context과 관련된 변경 사항이 생겼을 때, 다른 context는 신경 쓸 필요 없이 해당 context만 이해하면 된다.
  • 물론 개발 초기에 이러한 context들의 경계가 불분명하고, 시스템이 커짐에 따라 변경되기도 한다.
  • DB로부터 데이터를 가져와서 금융과 관련된 로직을 수행하는 것이 자연스럽다면, Split Phase를 적용하여 이 둘을 분리한 다음 명확한 자료구조를 이용하여 둘 사이에 데이터를 전달하는 방법을 사용할 수 있다. 만약 앞뒤로 더 많은 함수 호출들이 존재한다면 Move Function을 이용하여 적절한 모듈로 분리할 수 있다. 만약 분리할 함수 내에 여러 종류의 로직이 섞여있다면 Extract Function을 활용하여 이들을 추출해낼 수 있다. 만약 모듈이 클래스라면 Extract Class가 도움이 될 것이다.

샷건 수술 (Shotgun Surgery)

  • 샷건 수술은 산발적 수정과 흡사하지만 정반대의 경우이다. 코드를 변경할 때마다 여러 군데를 자잘하게 고쳐야 하는 경우 이러한 냄새를 맡을 수 있는데, 이렇게 되면 변경해야 할 부분들이 군데군데 흩어져있기 때문에 찾기도 어렵고 (변경하는 것을) 까먹기도 쉽다.
  • 이 경우 Move FunctionMove Field를 이용하여 변경해야 할 부분들을 하나의 모듈로 모을 수 있다.
  • 비슷한 데이터를 사용하는 함수들은 Combine Functions into Class를 사용하여 묶을 수 있고, 어떤 자료 구조를 변환하거나 보강하는 함수들은 Combine Functions into Transform을 사용할 수 있다. 이렇게 묶인 공통 함수들의 출력을 묶어서 다음 로직의 입력으로 전달할 수 있다면 Split Phase를 사용할 수 있다.
  • Inline Function 또는 Inline Class를 사용하여 어설프게 분리된 로직들을 리팩토링하는 것도 좋은 전략이 될 수 있다. 이렇게 하면 큰 함수(혹은 클래스)가 생길 수 있으나, 이후 적절한 단위로 분할하면 되므로 딱히 상관없다.
  • 비록 우리(저자)는 작은 함수(와 클래스)에 과도할 정도로 집착하긴 하지만, 코드의 구조를 잡아가는 과정에 있어 중간 단계에서 생기는 큰 함수들은 괜찮다고 생각한다.

기능에 대한 욕심 (Feature Envy)

  • 모듈화를 하는 경우, 우리는 모듈 내부의 코드끼리는 더욱 강하게 결합되도록, 그리고 모듈 간에는 약하게 결합되도록 하려고 한다.
  • “기능에 대한 욕심”의 가장 대표적인 케이스는 한 모듈 내의 어떤 함수가 같은 모듈에 있는 함수들(혹은 데이터) 보다 다른 모듈과 더욱 많이 소통하는 경우이다.
  • 이 경우, Move Function을 이용하여 해당 함수가 많이 사용(소통) 하는 데이터 근처로 함수를 옮기면 된다. 때로는 함수의 일부분만 욕심을 부리므는 경우가 있는데 이 땐 Extract Function을 사용하여 그 부분을 추출한 다음 Move Function을 이용하여 옮겨주면 된다.
  • 물론 어떤 함수가 여러 모듈의 기능을 사용하는 경우 좀 더 까다롭다. 이럴 때 우린 함수를 데이터가 가장 많이 있는 모듈로 옮겼다. 이때 Extract Function을 적용하면 각각의 함수 조각을 적절한 모듈들로 옮기는 게 더 수월할 수 있다.
  • 결국 핵심은 같이 변하는 것들을 한곳에 모으는 것이다.

데이터 뭉치 (Data Clumps)

  • 데이터는 마치 아이들처럼 서로 뭉쳐다니는 것을 좋아한다. 이렇게 같이 어울려다니는 데이터들은 하나로 묶는 것이 좋다.
  • Extract Class를 사용하여 클래스 필드 데이터 뭉치들을 하나의 객체로 묶을 수 있다. 또, Introduce Parameter ObjectPreserve Whole Object를 사용하여 매개 변수 뭉치들을 하나로 묶을 수 있다.
  • 이렇게 묶인 객체의 일부만을 사용하는 데이터 뭉치에 대해선 걱정할 필요가 없다. 두 개 이상의 데이터를 하나의 객체로 묶는 한, 결국에는 이득을 보게 될 것이다.

원시 타입에 대한 집착 (Primitive Obsession)

  • 많은 개발자들이 화폐, 좌표, 범위와 같이 자신들만의 데이터 타입을 만들기 꺼려 하는 것 같다. 이 때문에 종종 돈 계산을 (화폐 단위를 생략한 채) 평범한 숫자 타입으로 한다든지, (단위를 무시한 채) 물리적인 수량을 계산한다든지, 혹은 if (a < upper && a > lower) 와 같은 코드를 볼 수 있다.
  • Replace Primitive with Object를 이용하여 원시 타입을 의미있는 타입으로 변경할 수 있다. 만약 원시 타입이 분기문을 제어하는 역할을 한다면 Replace Type Code with SubclassesReplace Conditional with Polymorphism을 이용할 수 있다.
  • 같이 어울려 다니는 원시 타입들엔 Extract ClassIntroduce Parameter Object를 적용할 수 있다.

반복된 Switch 문 (Repeated Switches)

  • switch 문을 중복해서 사용하는 경우, 하나의 케이스가 추가될 때 (중복된) 모든 switch 문을 찾아 바꿔줘야 한다.
  • 이렇게 중복이 발생하고 있다면 Replace Conditional with Polymorphism을 이용하여 조건부 로직을 다형성 로직으로 바꿔보자.

루프문 (Loops)

  • Replace Loop with Pipeline 기법을 통해 루프문을 일급 함수로 변환하는 것을 추천한다. 우리의 경험으로 보자면 filtermap 같은 파이프라인 함수들을 통해 코드의 흐름을 더욱 쉽고 빠르게 파악할 수 있었다.

성의없는 요소 (Lazy Element)

  • 굳이 함수로 분리하지 않아도 이해할 수 있는 코드, 하나의 단순한 함수와 별 다를 것이 없는 클래스와 같이 굳이 별도의 구조를 형성하지 않아도 될만한 부분들이 존재할 수 있다.
  • 이 경우 Inline Function 혹은 Inline Class를 통해 이러한 구조를 제거하는 것을 권장한다. 상속을 사용하는 경우 Collapse Hierarchy를 사용할 수 있다.

추측성 일반화 (Speculative Generality)

  • 미래에 어떤 기능이 필요할 것이라는 이유로 존재하는, 현재로썬 쓸모없는 코드가 풍기는 악취이다. 보통 이러한 코드가 존재하면 전체적인 코드의 가독성이 떨어지고 유지 보수도 힘들어진다. 만약 정말로 미래에 이 기능이 사용된다면 그나마 낫지만, 그렇지 않은 경우 오히려 장애물이 되기 때문에 이것들을 제거하는 것이 낫다.
  • 그렇게 많은 일을 하지 않는 추상 클래스의 경우엔 Collapse Hierarchy를 사용하라. 불필요한 위임은 Inline FunctionInline Class로 제거할 수 있다.
  • 사용하지 않는 (불필요한) 매개 변수들은 Change Function Declaration을 적용하여 제거할 수 있다.
  • 어떤 함수, 혹은 클래스의 유일한 사용처가 테스트 케이스인 경우 코드에 추측성 일반화가 존재함을 알 수 있다. 이와 같은 상황에선 테스트 케이스를 제거하고 Remove Dead Code를 적용하라.

임시 필드 (Temporary Field)

  • 특정한 상황일때만 필드 변수에 값이 세팅되는 클래스의 경우, 언뜻 보기엔 사용되지 않는 필드인것 처럼 보여질 수 있다. 이 때문에 코드의 가독성이 저하될 수 있으므로 Extract Class를 이용해서 따로 분리하는 것을 권장한다.
  • 해당 필드를 참조하는 함수는 Move Function을 이용하여 분리된 클래스로 옮기는 것이 좋다.
  • 또, Introduce Special Case를 이용하여 해당 필드와 관련된 조건문 로직을 제거할 수 있다.

메시지 체인 (Message Chains)

  • 클라이언트가 어떤 객체를 얻기 위해 다른 객체에 물어보고, 그 다른 객체는 또 다른 객체에 물어보고, 또 다른 객체는 또 또 다른 객체에 물어보는것과 같이, 객체를 연속적으로 요청하여 발생하는 문제이다.
  • 이 경우 클라이언트가 객체의 navigation 구조에 종속됨으로 인해, 객체 사이의 관계가 변경되면 클라이언트 또한 변경되어야 하는 문제가 발생한다.
  • 이 경우 Hide Delegate를 사용할 수 있다. navigation 체인의 어느 지점에나 이를 적용할 수 있지만, 체인의 중간에 존재하는 객체가 “중개자(middle man)“이 되어버릴 가능성이 있다.
  • 따라서 이 보다 좀 더 나은 해결책으로는, 최종 객체가 어떤 용도로 사용되는지를 파악하는 것이다. Extract Function을 통해 최종 객체를 사용하는 코드를 분리할 수 있는지 살펴보고, Move Function을 통해 체인 안으로 밀어 넣을 수 있는지 살펴보라.

중개자 (Middle Man)

  • 캡슐화는 객체의 주요 특징 중 하나로, 바깥세상으로 부터 세부적인 내용들을 감춘다. 캡슐화를 통해 어떤 작업을 다른 객체에 위임하고 그 세부적인 구현은 몰라도 된다.
  • 예를 들어 팀장에게 미팅을 요청하면 팀장은 자신의 일정을 확인한 뒤 답을 줄 것이다. 팀장이 일정을 확인할 때 종이로 된 다이어리를 쓰든, 스마트폰을 이용하든, 비서를 통해 확인하든 우리가 신경 쓸 일은 아니다.
  • 하지만 이것도 너무 지나치면 문제가 되는데, 클래스 내의 메서드 중 절반 이 다른 클래스에 위임을 하고 있는 경우 Remove Middle Man을 사용하여 중개자를 제거하고 실제로 동작을 수행하고 있는 객체와 직접 소통하는 것이 좋다.
  • 외부에 위임하는 메서드를 제거하고 남아있는 로직이 얼마 없다면 Inline Function을 적용하라.

내부자 거래 (Insider Trading)

  • 개발자들은 모듈 사이에 두꺼운 벽을 세우는 것을 좋아하며, 모듈끼리 데이터를 너무 많이 주고받으면 결합도가 증가한다고 투덜댄다.
  • 물론 모듈 간의 상호작용을 아예 제거해버리는 것은 불가능하겠지만, 그 양을 최소한으로 줄이고 투명하게 처리해야 한다.
  • 은밀하게 데이터를 주고받는 모듈이 있다면 Move FunctionMove Field를 통해 둘을 분리하는 것이 좋다. 만약 모듈들 간에 공통되는 부분이 있다면 그 부분을 따로 떼어내어 정식으로 처리하게 하거나, Hide Delegate를 이용하여 다른 모듈을 만들어 중개자 역할을 하도록 할 수 있다.
  • 상속 구조의 경우, 자식 클래스는 부모 클래스가 공개하는 범위를 넘어서 더 알려고 하는 경향이 있다. 부모의 품을 떠나 독립을 해야 할 때라면 Replace Subclass with Delegate 혹은 Replace Superclass with Delegate를 사용하자.

큰 클래스 (Large Class)

  • 한 클래스에서 너무 많은 일을 하려고 하면 그에 따라 필드도 너무 많이 생기게 되고, 그에 따라 중복된 코드가 생기기 쉽다.
  • 이럴 땐 Extract Class를 사용하여 같은 컴포넌트에 모아두면 좋을 것 같은 변수들을 묶을 수 있다. 일반적으로, 클래스 내에서 동일한 접두·접미사를 사용하는 변수들을 같이 묶기 좋다.
  • 이렇게 분리한 클래스를 원래의 클래스와 상속 관계로 만드는 것이 좋을 것 같은 경우, Extract Superclass 혹은 Replace Type Code with Subclasses를 사용해 보라.
  • 클래스의 코드가 너무 많으면 중복이 생길 가능성이 높아진다. 결국, 클래스 내에서 자체적으로 중복을 없애는 것이 핵심이다.

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

  • 클래스의 커다란 장점 중 하나는 필요에 따라 언제는 다른 클래스로 교체할 수 있다는 점이다. 하지만 이는 클래스들 간의 인터페이스가 동일한 경우에만 가능하다.
  • Change Function Declaration을 사용하여 함수의 모양을 일치시킬 수 있다.
  • 이것만으로 부족한 경우, 인터페이스가 같아질 때까지 Move Function을 사용하여 동작들을 클래스 내부로 집어넣어라.
  • 만약 이렇게 해서 중복이 생긴다면 Extract Superclass를 사용하라.

데이터 클래스 (Data Class)

  • 필드와, getter, setter만 있는 클래스를 데이터 클래스라고 한다. 이러한 클래스들은 단순히 데이터 주머니 용도에 지나지 않으며, 다른 클래스가 너무 깊게 파고드는 경우가 많다.
  • 또한, 어떤 경우엔 public 필드를 가지고 있는 경우가 있는데, 이 경우 즉시 Encapsulate Record를 통해 캡슐화를 해야 한다. 절대 변경되어선 안되는 필드의 경우 Remove Setting Method를 이용하자.
  • 어떤 클래스에서 이 클래스의 getter, setter를 사용하는지 살펴보고, Move Function을 이용하여 해당 메서드를 데이터 클래스 내부로 집어넣을 수 있는지 살펴보자. 메서드 전체를 옮길 수 없다면 Extract Function을 사용해 보자.
  • 데이터 클래스는 필요한 동작이 엉뚱한 곳에 위치해 있다는 신호일 수 있다. 이 경우 클라이언트 코드를 데이터 클래스로 옮기기만 해도 코드를 대폭 개서할 수 있다.
  • 물론 예외도 있는데, 다른 함수를 호출하여 얻은 결과를 저장하는 레코드가 대표적인 예시이다. Split Phase의 결과로 생긴 중간 데이터 구조가 이러한 레코드의 예시이다.
  • 이러한 레코드는 불변이므로 굳이 캡슐화할 필요가 없고, getter를 통하지 않고 필드 자체를 공개해도 딱히 상관없다.

상속 거부 (Refused Bequest)

  • 자식 클래스는 부모 클래스로부터 메서드와 데이터를 상속받는다. 하지만 자식 클래스가 이러한 상속을 원하지 않는 경우는 어떨까?
  • 일반적으로, 이러한 경우는 부모와 자식 클래스 간의 계층 구조가 잘못되었다는 신호이다. 이 땐 새로운 (같은 부모로부터 상속받는) 형제 클래스를 만들고, Push Down MethodPush Down Field를 사용하여 기존의 클래스에서 사용하지 않던 코드를 형제 클래스로 옮기는 것을 추천한다. 이렇게 하면 부모 클래스는 공통적인 것만 가지고 있게 된다.
  • 모든 부모 클래스는 추상 클래스이어야 한다는 조언을 들은 적이 있을 것이다. 하지만 항상 이 조언을 따르라고 권하고 싶지는 않다. 냄새가 나긴 나지만, 이 정도 냄새는 참을만하다.
  • 자식 클래스가 (부모로부터 받은) 동작을 재사용 하고 있지만 인터페이스는 따르고 싶어 하지 않는 경우 상속 거부의 악취가 훨씬 심해진다. 구현을 따르지 않는 것은 이해할 수 있지만, 인터페이스를 따르지 않는다는 것은 참을 수 없다.
  • 하지만 이 경우 클래스 간의 계층을 건드릴 필요는 없다. Replace Subclass with Delegate 또는 Replace Superclass with Delegate를 통해 이를 제거할 수 있다.

주석 (Comments)

  • 물론, 걱정 마시라. 주석을 달면 안 된다고 하는 것이 아니다. 사실 주석은 악취가 아니라 향긋한 냄새이다. 좋은 냄새임에도 불구하고 여기에서 언급하는 이유는, 주석이 데오도란트의 용도로 사용되는 경우가 많기 때문이다.
  • 주석을 달 필요가 없는 코드를 작성하는 것이 우선이다. 리팩토링을 통해 냄새를 제거하고 나면, 주석은 더 이상 필요 없는 경우가 많다.
  • 정 주석을 통해 코드의 역할을 설명해야겠다고 한다면 Extract Function을 시도해 보라. 추출했음에도 불구하고 주석이 필요하다면 Change Function Declaration을 통해 이름을 바꿔보라. 시스템이 필요로 하는 특정 상태를 적어놔야 하는 경우 Introduce Assertion을 사용해 보라.
  • 주석을 남겨야겠다는 생각이 들면 우선 주석이 필요 없는 코드로 리팩토링을 해보는 것을 추천한다.
  • 무엇을 해야 할지 모를 때 주석을 달면 좋다. 어떤 일이 일어나고 있는지를 설명하는 것과 더불어, 왜 그러한 코드를 짰는지 설명할 수 있다. 이러한 정보는 추후에 코드를 수정할 때 도움이 될 수 있다.