본문 바로가기

리뷰/책 리뷰

[IT/리뷰] 테스트 주도 개발

728x90

테스트 주도 개발 : 네이버 도서 (naver.com)

 

테스트 주도 개발 : 네이버 도서

네이버 도서 상세정보를 제공합니다.

search.shopping.naver.com

회사에서 스터디로 TDD를 해보기로 했다. 그때 정한 책이 이 책이다. 

 

사실 TDD를 처음 할 때 스탭바이스탭으로 시작하기에는 이전에 읽었던 <임베디드 C를 위한 TDD>가 더 유용했다. <임베디드 C를 위한 TDD>를 읽고 당시 관심이 있던 TDD에 호기심이 정립됐다. 정석대로 하는 방법에 대해 배울 수 있었다.

 

<테스트 주도 개발>은 이전에 알던 개념에서 이어서 읽어서 더 좋았던 것 같다. XP의 창시자이자 애자일의 선구자인 켄트 벡의 TDD 이야기를 들으니 확실히 알게 되었던 점은 내가 이 책을 읽기 전까지 TDD를 어렵게 생각하고 있었다는 것이다.

물론, 실제로 TDD를 적용해 그것이 설계와 상호 도움을 줄 정도가 되려면 어려운 점은 맞는 것 같다. 설계를 미리 생각하고 테스트를 작성하기도, 테스트를 작성하다 보니 설계가 바뀌기도 하는데, 이 부분은 경험한 게 많아야 한다.

하지만, TDD를 꼭 정석대로 할 필요가 없다. TDD에서 중요한 원칙은 있지만, 그 원칙만 지킨다면 개발자가 자신에게 맞는 속도, 규칙을 스스로 정해 나아가면 된다는 것을 알았다.

 

아직 회사에서 실제로 적용해보지는 못했다. TDD를 적용하기 위해 기존의 코드의 설계를 바꾸기에는 당장 시간이 없기 때문이다. 다만, 새로 만드는 경우에는 TDD를 바로 적용할 수 있도록 해봐야겠다. 회사의 개발 프로세스와는 맞지 않는 부분이 있어 어려움이 있을 것으로 예상이 된다. 이건 어느 정도 협의를 통해 양해를 구하고 진행을 한 번이라도 해봐야 하는 게 맞을 것 같다.

 

아래는 정리 내용이다. 직전에 작성한 <클린 아키텍처> 정리 내용이 너무 많았다. 다 중요하다 생각돼서 그랬는데, 이번에도 정리내용이 조금 길다. 최대한 줄인다고 줄여봐야겠다.

 

책은 3부로 이루어진다. 1부는 TDD로 일상적인 모델 코드를 만드는 예제다. 2부는 자동화 테스트 프레임워크(xUnit)을 만드는 것을 통해 보다 TDD에 대한 이해도를 높인다. 이때도 TDD를 적용해 만든다. 마지막 3부는 TDD를 위한 패턴들로, 여러 팁들이 있다고 보면 된다.


TDD 두 가지 규칙

  1. 오직 자동화된 테스트가 실패할 경우에만 새로운 코드를 작성한다.
  2. 중복을 제거한다.

TDD 두 가지 규칙에 의한 프로그래밍 순서

  1. 빨강 막대 : 실패하는 작은 테스트를 작성한다. 처음에는 컴파일조차 되지 않을 수 있다.
  2. 초록 막대 : 빨리 테스트가 통과하게끔 만든다. 어떤 죄악(ex. 복사 붙여넣기, 테스트 통과할 수 있는 상수 반환 등)을 저질러도 좋다.
  3. 리팩토링 : 테스트를 통과하게만 하는 와중에 생긴 중복을 제거한다.

 

다음의 기술적 함의로 인해 개인이나 집단 차원의 복잡한 행동 패턴이 만들어진다.

  • 매 결정사항에 대해 피드백을 제공하는 실행 가능한 코드를 기반으로 하는 유기적 설계를 해야 한다.
  • 자동화된 테스트를 다른 사람이 만들어주길 기다릴 수 없으므로, 직접 테스트를 작성해야 한다.
  • 개발 환경은 작은 변화에도 빠르게 반할 수 있어야 한다.
  • 테스트를 쉽게 만들려면 반드시 응집도는 높고 결합도는 낮은 컴포넌트들로 구성되게끔 설계해야 한다.

 

TDD는 XP와 논조의 차이가 있다. TDD는 XP처럼 절대적이지 않다. XP는 다음과 같다.

"앞으로 더 진화할 수 있도록 준비하려면 이런 것들을 할 수 있어야만 한다."

TDD는 보다 애매하다. 프로그래밍 도중 내린 결정과 그 결정에 대한 피드백 사이 간격을 인지하고, 간격을 통제할 수 있게 해주는 기술이다.

 

"작동하는 깔끔한 코드"를 얻어야 한다는 문제 중에 '작동하는'에 해당하는 부분을 먼저 해결하라. 그러고 나서 '깔끔한' 부분을 해결하는 것이다. TDD 프로그래밍 순서를 보면 알 수 있다. '깔끔한' 부분을 먼저 해결하는 아키텍처 주도 개발과는 정반대다.

 

느낌(부작용에 대한 혐오감)을 테스트로 변환하는 것은 TDD의 일반적인 주제다. 이 작업을 오래 할수록 미적 판단을 테스트로 담아내는 것에 점점 익숙해진다. 이걸 할 수 있을 때, 설계 논의는 훨씬 흥미로워진다.

즉, TDD가 설계에 영향을 주는 것이다.

 

1부

1부는 Money 예제로 TDD를 처음 접하는 사람이라면 TDD가 어떤 식으로 진행되는지 알 수 있게끔 예제를 통해 소개한다. 개인적으로는 정석적인 예제는 <임베디드 C를 위한 TDD>가 더 맞는 것 같다.

하지만, 켄트 벡의 <테스트 주도 개발>은 나처럼 TDD에 대해 겁먹은 사람이 읽기 좋은 책인 것 같다. 이 책은 정석을 설명하긴 하지만, 중간 과정들이 많이 생략되어 있기도 하고 어느 정도 건너뛰는 작업도 용인한다고 직접 말하기 때문이다.

좀 더 자유도를 주고 빡빡하게 진행하지는 않지만, 처음 접하는 사람이 봤을 때 중요한 점을 놓치고 지나치는 경우가 있을 것 같다. 스터디를 진행하는 중간에 나와 팀원들도 이해가 안되는 부분이 있었다. 얘기를 통해 저자가 말하는 점이 무엇인지, 우리가 무엇을 잘못 이해하고 있었는지 등에 대해 확인하면서 진행해 다행이었다. 

1부는 예제라 정리하기에는 맞지 않은 것 같고, 1부 중에 나온 얘기들에 대해 정리해봤다. 또, 중간중간에 의아한 부분들이 있었는데 그런 부분들에 얘기했던 점을 적었다.

 

저자가 아래의 말을 했었는데 이걸로 내가 TDD에 대해서 너무 어렵고 힘들다고 생각했던 것임을 알았다.

"TDD 하는 동안 종종걸음으로 진행하는 것이 답답하다면, 보폭을 넓혀라. 성큼성큼 걷는 것이 불안하면, 보폭을 줄여라. 정해진 올바른 보폭이라는 것은 존재하지 않는다."

 

책을 읽으면 TDD를 할 때 설계를 고려했기 때문에 가능한 부분들이 보인다. 팩토리 패턴을 사용했기 때문에 중복 제거가 가능했을 것이고, 어떤 개념을 추가했기 때문에 좀 더 명확한 클래스 구분이 되기도 했다. 이 부분들은 여러 경험이 쌓여 가능한 것이고, TDD 자체적으로 도움을 줄 수 없는 부분이라 생각했다. 팀원들과 나는 '그냥 개발 잘하는 분이 이미 머리로 다 개발해 놓고 테스트와 코드만 순서 바꿔서 진행하는 것 아닌가'하는 생각을 했다.

생각해 보면 TDD 반복의 마지막 단계는 항상 '리팩토링'이다. 리팩토링 과정에서 팩토리를 사용하게끔 변경하고, 명확한 클래스를 만들 수 있는 개념을 추가하게 되는 것이 아닌가 생각했다.

예제가 간단하기 때문에 이미 머리로 생각을 다 한 것은 아닐까라는 생각이 들었다고 생각한다. 실제로는 보다 복잡한 것을 만들 텐데, 이때 한 스탭씩 필요한 것의 테스트와 기능 코드를 순서대로 만들고 리팩토링을 하게 되면 더 좋은 설계를 유도할 것이라 생각된다.

 

책을 보다가 내가 TDD에 관심을 갖게된 것에 대해 말해주는 부분이 있었다.

TDD는 적절한 때에 번뜩이는 통찰을 보장하지 못한다. 그렇지만 확신을 주는 테스트와 조심스럽게 정리된 코드를 통해 통찰에 대한 준비와 함께 통찰이 번뜩일 때 그것을 준비할 수 있다.

오래된 코드에 기능을 추가하거나 리팩토링할 때 안전장치가 없이 불안해하며 임의로 몇 가지 기능 테스트하는 것은 너무 불안하다.

 

1부에서 TDD로 얻을 수 있는 효과를 살펴봤는데, 중요한 개념은 메타포(기교)인 것 같다. 중복을 제거하고 기능을 구현할 때 메타포를 사용했고, 메타포에 의해 설계가 엄청나게 영향을 받았기 때문이다.

이 작은 순간들이 모두 안전하게 리팩토링 될 수 있었던건 TDD로 쌓아놓은 테스트 덕분이다. 기능 코드가 수정되면 테스트 코드도 먼저 수정되어야 하지만 서로 영향을 주며 발전해 가는 게 느껴졌다.

 

할 일 목록 작성, 테스트 작성, 코드 작성, 리팩토링이라는 실질적 4단계를 빠르게 반복할 수 있도록 하자.

 

2부

2부는 xUnit 예시를 보여주기 위해 파이썬을 사용한다. 1부에서 Java를 사용했던 것과 달리 쉽게 이해할 수 있을 것이라 생각했다.

 

2부에서는 xUnit 사용 예시가 아닌, xUnit을 만드는 내용을 보여준다. 당연히 이를 만들 때도 TDD가 적용되어야 하는데, 테스트 프레임워크가 없는 상태이기 때문에 수동 검증이 필요하다.

 

상대적으로 2부에서는 설명들이 좀 어렵다. 졸린 상태에서 책을 읽엇떤 것인지, 정확히 무슨 말을 하는지 이해하는데 어려워서 여러 번 읽게 됐다. 게다가 테스트 프레임워크를 다 완성하지 않고 끝내는데, 너무 많이 남겨두고 끝내는 것 같아 찝찝했다.

당신이 뭔가를 배우고 싶다면, 한 번에 메서드 하나 이상을 수정하지 않으면서 테스트가 통과하게 만들 수 있는 방법을 찾아내려고 노력해라.

 

3부

3부는 TDD 패턴에 대한 모음으로, 상황과 팁들에 대해 작성됐다. TDD 트릭, 패턴, 리팩토링 등이 포함된다. 그러다보니 자연스럽게 3부가 정리된 내용이 많다. 정리된 내용은 또 정리해서 그나마 줄여야겠다.

 

평소에는 스트레스와 테스트의 관계는 죽음의 나선이다. 스트레스를 많이 받으면 테스트는 점점 뜸해질 것이다. 테스트가 뜸해지면 에러는 점점 많아질 것이다. 에러가 많아지면 더 많은 스트레스를 받게 된다.

이 죽음의 나선에서 빠져나오기 위해서는 새로운 요소를 도입하거나 기존 요소를 바꿔치기해야 한다. 이 경우에는 '테스트'를 '자동화된 테스트'로 치환하면 해결 가능하다.

테스트는 두려움을 지루함으로 바꿔주는 효험이 있다.

 

모든 테스트는 격리되어야 한다. 테스트는 전체 애플리케이션을 대상으로 하는 것보다 좀더 작은 스케일로 하는 게 좋다. 문제가 하나면 테스트도 하나만 실패하고, 문제가 둘이면 테스트도 두 개만 실패해야 한다.

 

격리된 테스트는 암묵적으로 테스트가 시행 순서에 독립적이어야 한다. 선행 테스트가 실행되지 않았다고 내가 고른 테스트가 실패하지 않아야 한다.

격리된 테스트를 위한 세팅을 하다보면 응집도를 높이고 결합도를 낮추는 방법에 대한 이해를 할 수 있게 되고 시스템에 반영될 것이다.

 

테스트 작성 시에는 assert를 제일 먼저 쓰고 시작하는 것이 좋다. assert를 씀으로써 작업을 단순하게 만드는 강력한 효과가 있다.

 

테스트 데이터는 읽을 때 쉽고 따라가기 좋은 데이터를 써야 한다. 테스트도 청중이 있고 화자가 있다고 생각하자. 데이터 값의 산발을 위해 데이터 값을 산발하지 말아야 한다. 데이터의 차이가 있다면, 실제 차이에 대한 의미가 있어야 한다. 데이터의 개념적 차이가 없다면 쉬운 값을 쓰면 된다.

 

할 일 목록에서 다음 테스트를 고를 때, 무언가 가르쳐 줄 수 있으며 구현할 수 있다는 확신이 드는 테스트를 선택해야 한다. 무언가 가르쳐줄 수 있을 것 같다는 것은 뻔한 구현이 아닐 것 같음을 말한다.

 

시작 테스트는 오퍼레이션이 아무 일도 하지 않는 것으로 선정하라. 오퍼레이션을 어디에 넣어야 할지 모를 때에는 테스트에 뭘 적어야 할지 모르는 것이다.

 

외부 소프트웨어에 대한 테스트도 작성할 필요가 있다. API의 이해도를 확인할 수도 있다. 이는 학습 테스트라 한다. 이때 외부 소프트웨어에 대한 테스트는 외부 패키지의 새로운 버전이 출시될 경우 현재 시스템에 대한 영향도 확인이 가능하다.

 

테스트 중 진행 주제와 무관한 아이디어는 언제나 환영하라. 하지만, 현재의 주의를 흩뜨리지 않는 것이 중요하다. 떠오르는 아이디어는 할 일 목록에 적어두자.

 

회귀 테스트는 선견지명이 필요하다. 이미 테스트가 작성되어있어야 할 내용이다. 그것에 실패했다면 이 회귀 테스트를 어떻게 먼저 생각할 수 있었을지에 대해 고민하라.

회귀  테스트를 작성하기 힘들다면 시스템의 장애를 격리하기 힘든 것이다. 이는 설계가 잘못된 것이기에 리팩토링이 필요하다.

 

휴식은 아이디어를 떠오르게 해주고 환기시켜 준다. TDD는 샤워 방법론이라고도 부르는 휴식을 고도화한 것이라 볼 수 있다. 키보드로 뭘 쳐야 할 지를 안다면 명백한 구현을 하고, 잘 모르겠다면 가짜 구현을 하면 된다. 올바른 설계가 명확하지 않다면 삼각측량 기법을 사용하면 된다. 그래도 모르겠다면 정말 샤워나 하면서 휴식하면 된다.

길을 잃은 것 같을 때에는 코드를 다 지워버리고 처음부터 다시 해보는 것도 합리적일 수 있다.

 

비용이 많이 들거나 복잡한 리소스에 의존하는 객체를 테스트하려면, 상수를 반환하게끔 속임수 버전의 리소스를 만들면 된다. 대표적으로 모의 객체(Mock Object)가 전통적인 예시다.

 

호출되지 않을 것 같은 에러 코드를 테스트할 때는 실제 작업 수행 대신 예외만 발생시키는 특수 객체를 만들어라. 테스트되지 않은 코드는 작동하는 것이 아니다.

그럼 수많은 에러 상황은 어떻게 테스트할 것인가? 다 테스트해야 할까? 정답은 작동하길 원하는 부분에 대해서만 테스트하면 된다.

객체 전체를 흉내낼 필요가 없다는 점을 제외하면 크래시 테스트 더미는 모의 객체와 유사하다.

 

혼자서 프로그래밍할 때는 마지막 테스트가 깨진 상태로 세션을 마치는 것이 좋다. 다음에 다시 개발할 때 반쪽짜리 코드를 보면 무슨 일을 하려 했는지 기억난다.

반대로 팀 프로그래밍을 한다면 모든 테스트가 성공한 상태로 끝내야 한다. 자신이 마지막 코딩한 다음으로부터 지금까지 무슨 일이 있었는지 세밀하게 알 수 없다. 따라서 안심되고 확신이 있는 상태에서 시작해야 한다. 체크인을 하기 전에 모든 테스트가 돌아가는 상태로 만들어 두어야 한다.

 

깨진 테스트가 있다면 빠르게 초록 막대 상태로 바꿔야한다. 그러기 위한 방법들이다.

  • 가짜 구현
    • 진짜로 만들기 전에 가짜 구현을 사용해도 좋다. 먼저 상수 반환으로 테스트를 통과하고, 단계적으로 상수를 변수를 사용하는 수식으로 변경하도록 하라.
    • 들어낼 것을 알고도 하는 이유는, 그래도 뭔가 돌아가는 것을 갖는 게 그러지 않은 것보다 좋기 때문이다. 심리적으로 초록 막대 상태일 때, 자신이 어느 위치에 있는지 알 수 있다. 거기서 확신을 갖고 리팩토링이 가능하다.
    • 가짜 구현을 통해 범위 조절이 가능하다. 쓰잘데기 없는 고민으로 이른 혼동을 하지 않도록 할 수 있다. 이전 테스트 작동이 보장됨을 알아야 다음 테스트 케이스에 집중이 가능하다.
  • 삼각측량
    • 오로지 예가 두 개 이상일 때만 추상화해야 한다.
    • 삼각측량이 매력적인 이유는 규칙이 매우 명확하기 때문이다. 가짜 구현은 추상을 끌어내기 위한 가짜 구현과 테스트 케이스 사이 중복에 대해 우리의 감각에만 의존한다.
    • 어떤 계산을 어떻게 해야 올바른 추상화가 가능한지 감잡기 어려울 때만 삼각측량을 사용해도 좋다. 그 외의 경우에는 명백한 구현이나 가짜 구현에 의존한다.
  • 명백한 구현
    • 단순 연산은 그냥 구현하라. 어떤 연산을 어떻게 구현할지 확신이 든다면 가짜 구현이나 삼각측량으로 작은 발걸음을 뗄 필요가 없다.
  • 하나에서 여럿으로
    • 객체 컬렉션을 다루는 연산이 필요하다면 컬렉션 없이 구현을 먼저 하라. 이후 컬렉션을 사용하자.

 

xUnit 계열 테스트 프레임워크를 위한 패턴은 따로 있다. 여기는 내용이 많아 개조식으로 적기는 무리가 있어 서술식으로 작성했다.

 

단언을 잘 작성하는 방법이 있다. 보통 0이 아닌 경우를 비교하는 것은 의미가 없다. 반환값이 50이어야 하면, 정확히 50인지를 확인하라.

테스트를 할 때는 공용(public) 프로토콜만을 이용해 모든 테스트를 작성해야 한다. 변수의 값, 전용(private) 변수 값까지 테스트하는 프레임워크(ex. JXUnit)가 있더라도 할 필요가 없다. 화이트박스 테스트를 바라는 것은 테스트의 문제가 아닌 설계 문제다. 코드 작동 상태를 확인하기 위한 변수가 필요하다면, 설계를 향상시킬 기회로 생각하자.

 

여러 테스트에서 공통으로 사용하는 객체를 생성할 때 지역 변수가 아닌 인스턴스 변수로 변경하라. setUp() 메서드에서 재정의해 인스턴스 변수를 초기화하면 된다.

모델 코드(제품 코드)에서 중복을 제거하길 원한다면, 테스트 코드에서도 중복을 없애야 한다. 테스트 픽스처로 중복을 줄이도록 하자.

픽스처 없이 순차적으로 읽는 것을 선호하는 사람도 있을 것이다. setUp()을 기억하지 않아도 되기 때문이다. 두 가지 모두 시도해 보고 맞는 것을 선택하면 된다. 하지만 중복을 없애는 것을 추천한다.

 

테스트 메서드는 의미 그대로 드러나는 코드로, 읽기 쉬워야 한다. 목표에 좀더 다가갈 수 있게 해주는 가장 짧은 테스트를 작성하자. 일부로 코드를 꼬는 일이 없어야 한다.

 

예외가 발생하는 것이 정상인 경우에 대한 테스트는 예외를 잡아서 확인하자. 예외가 발생하지 않은 경우는 실패하고 예외가 잡힌 경우는 성공시키면 된다.

 

간단한 메서드 호출보다 복잡한 형태의 계산 작업에 대한 호출이 필요하다면, 계산 작업에 필요한 객체(커맨드 객체)를 생성해 호출하면 된다.

복잡한 계산 작업 호출은 값비싼 메커니즘을 필요로 하는데, 거의 대부분 이런 복잡함을 요구하지 않으며 그런 비싼 값을 치르지 않는 게 좋다.

 

널리 공유해야하지만 동일성(identity)은 중요하지 않을 때, 객체가 생성될 때 상태를 설정 후 변하지 않도록 할 수 있다. 이 객체에 대해 수행되는 연산은 언제나 새로운 객체를 반환한다.

별칭 문제를 해결하기 위한 방법은 몇 가지 있다. 그중 하나는 현재 의존하는 객체에 대한 참조를 외부로 알리지 않는 것이다. 객체에 대한 복사본을 제공할 수도 있다. 메모리에 대해 비싼 해결책일 수도 있고, 공유 객체의 상태 변화를 공유할 수 없는 단점도 있다.

또 다른 방법은 옵저버 패턴이 있다. 의존하는 객체에 등록해 객체 상태의 변경을 통지받을 수 있다. 하지만 옵저버 패턴은 제어 흐름을 이해하기 어렵게 하고 의존성 설정 및 제거에 대한 로직이 지저분하다.

다른 방법으로는 객체를 덜 객체답게 취급하는 방법이 있다. 시간의 흐름에 따라 변하는 상태 자체를 제거하는 방법으로, 이것을 값 객체라 부른다. 값 객체에서는 동등성(equality)은 있어도 동일성(identity)은 없다. 5백원 짜리 동전 두 개는 동등해도 동일하지 않다.

 

객체의 특별한 상황을 표현할 때는 새로운 객체를 만들면 된다. 이 객체에 다른 정상적인 상황을 나타내는 객체와 동일한 프로토콜을 사용한다.

null을 반환하는 경우 일일이 조심스럽게 검사해야 한다. 안전하지만 지저분해진다. null 검사 대신 다른 방법으로 예외를 던지지 않는 새로운 클래스를 만드는 방법이 있다. 널 객체를 만드는 것이다.

예를 들어, 누군가 SecurityManager를 요청했는데 반환할 SecurityManager가 없다면 대신 새로운 LaxSecurity를 만들어 반환하면 된다. 싱글톤 패턴과 유사하게 객체 요청 시 생성 및 반환하면 된다. 이때 LaxSecurity는 SecurityManager와 같은 프로토콜(인터페이스)을 구현해 놓으면 된다. 동작은 다르겠지만 널 검사는 하지 않아도 된다.

 

작업 순서는 변하지 않지만 작업 단위에 대한 미래의 개선 가능성을 열어두고 싶다면, 다른 메서들을 호출하는 내용으로만 이루어진 메서드를 만들면 된다. 이를 템플릿 메서드라 한다.

객체지향 언어에서는 상속을 통해 범용적 순서를 표현하기 위한 간단한 방법을 제공한다. 상위 클래스에서 다른 메서드를 호출하는 내용으로만 이루어진 메서드를 만들고, 하위 클래스에서 이 메서드들을 서로 다른 방식으로 구현하면 된다.

예를 들어, JUnit에서 setUp > runTest > tearDown 순서를 정했다면, 하위 클래스에서 각각을 구현하면 된다.

 

변이를 표현할 때 가장 간단한 방법은 명시적 조건문을 사용하는 것이다. 하지만 명시적 의사 결정 코드는 소스 여러곳으로 퍼져나간다. TDD의 두 번째 규칙인 중복 제거는 이런 명시적 조건문의 전염도 포함한다. 같은 조건문을 여러 번 본다면 플러거블 객체를 꺼낼 차례다.

단지 중복 제거를 위한 플러거블 객체는 반직관적인 경우가 있다. 명시적 인터페이스를 사용하는 언어에서는 둘 이상의 플러거블 객체에 대해 동일한 인터페이스를 구현하게 해야 한다.

 

인스턴스별로 서로 다른 메서드가 동적으로 호출되게 하려면 메서드 이름을 저장하고 있다가 그 이름에 해당하는 메서드를 동적으로 호출하면 된다. 이는 플러거블 셀렉터라 한다.

메서드 하나만 구현하는 하위 클래스가 열 개 있다면 곤란하다. 상속을 작은 변이를 다루기에는 너무 무겁다. 한 가지 대안은 switch 문을 갖는 하나의 클래스를 만드는 것이다. 하지만, 메서드 이름이 세 곳(인스턴스 생성, switch 문, 메서드 자체)에 나뉘어 존재하게 된다. 새로운 종류가 추가될 때마다 메서드가 추가되며 switch 문도 수정되어야 한다.

플러거블 셀렉터 해법은 리플랙션을 이용해 switch 문을 없애고 동적 메서드를 호출하는 데 있다. 플러거블 셀렉터는 과용될 수 있다. 메서드 호출을 확인하기 위해 코드를 추적하는 것이 가장 큰 문제다. 직관적인 상황에서 코드를 정리할 때 사용해야 한다.

 

객체를 만들 때 유연성을 원하는 경우 생성자 대신 일반 메서드(팩토리 메서드)에서 객체를 생성한다. 팩토리 메서드의 단점은 간접성이다. 생성자처럼 생기지 않았지만, 객체를 생성하고 있다는 것을 기억해야 한다. 유연함이 필요할 때만 사용해야 하며, 그 외에는 생성자로도 충분하다.

 

기존 코드에 새로운 변이를 도입하기 위해 같은 프로토콜을 갖지만 구현은 다른 새로운 객체를 추가한다. 사칭 사기꾼(임포스터, imposter)을 만드는 것을 말한다.

절차적 프로그램에서는 변이 도입을 위해 조건문을 추가해야 했다. 하지만 이 조건문은 증식된다. TDD에서도 두 군데가 있다. 리펙토링 중 널 객체와 컴포지트가 그것이다. 리팩토링 중 사칭 사기꾼을 찾아내는 것은 중복 제거 작업을 통해 유도된다.

 

하나의 객체가 다른 객체 목록의 행위를 조합한 것처럼 행동하려 만드는 방법으로 컴포지트가 있다. 컴포지트 객체는 객체 집합임에도 단일 객체에 대한 사칭 사기꾼으로 만드는 것이다. 컴포지트 패턴은 프로그래머의 트릭이지 세상 사람들에게 일반적으로 받아들여지는 것은 아니다. 설계에서 얻는 이득이 엄청난 것이어서 이러한 개념적 단절은 그만한 가치가 있다.

 

여러 객체에 걸쳐 존재하는 오퍼레이션의 결과를 수집하기 위해서는 수집될 객체를 각 오퍼레이션의 매개변수로 추가하면 된다. 이를 수집 매개 변수라 한다. 수집 매개 변수의 추가는 컴포지터의 일반적 귀결이다.

 

TDD에서는 리팩토링을 특이한 방법으로 사용한다. 일반적인 리팩토링은 어떤 상황에서도 프로그램의 의미론을 변경해서는 안 된다. 하지만, TDD에서는 신경 쓰는 부분은 현재 이미 통과한 테스트뿐이다.

예를 들어 상수를 변수로 바꾸는 것도 TDD에서는 리팩토링이라 부른다. 행위가 통과하는 테스트의 집합에 아무 변화도 주지 않기 때문이다. 의미론이 유지되는 상황이란 사실 TDD에서는 테스트 케이스 하나를 의미하는 것이다.

 

비슷해 보이는 두 코드 조각을 합치기 위해서는 두 코드가 닮아가게끔 수정해야 한다. 완전히 동일해지면 둘을 합친다. 리팩토링은 모든 규모의 작업에서 발생한다.

  • 반복문 구조가 비슷하다면 동일하게 만들고 하나로 합친다.
  • 조건문에 의해 나눠지는 분기 코드가 비슷하면 둘을 동일하게 만들고 조건문을 제거하라.
  • 두 클래스가 비슷하면 둘을 동일하게 만들고 하나를 제거하라.

 

객체나 메서드 일부만 변경해야 한다면, 바뀔 부분을 먼저 격리해야 한다. 이후 바꾸는 작업을 수행할 경우 작업을 되돌리기도 매우 수월할 것이다.

격리 방법으로는 메서드 추출, 객체 추출, 메서드 객체 등이 있다.

728x90