1. 리팩터링: 첫번째 예시

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

  • 리팩터링할 코드 영역을 꼼꼼하게 검사해줄 테스트 코드들부터 마련해야 한다. 테스트를 작성하는 데 시간이 좀 걸리지만, 신경 써서 만들어두면 디버깅 시간이 줄어서 전체 작업 시간은 오히려 단축된다.
  • 한 가지를 수정할 때마다 테스트하면, 오류가 생기더라도 변경 폭이 작기 때문에 살펴볼 범위도 좁아서 문제를 찾고 해결하기가 훨씬 쉽다. 조금씩 변경하고 매번 테스트하는 것은 리팩터링 절차의 핵심이다.
  • 지역 변수를 제거해서 얻는 가장 큰 장점은 추출 작업이 훨씬 쉬워진다는 것이다. 유효범위를 신경 써야 할 대상이 줄어들기 때문이다.
  • 반복문이 중복되는 것을 꺼리는 이들이 많지만, 이 정도 중복은 성능에 미치는 영향이 미미할 때가 많다.
  • 리팩터링으로 인한 성능 문제에 대한 내 조언은 '특별한 경우가 아니라면 일단 무시하라'는 것이다. 리팩터링 때문에 성능이 떨어진다면, 하던 리팩터링을 마무리하고 나서 성능을 개선하자.
  • 간결함이 지혜의 정수일지 몰라도, 프로그래밍에서만큼은 명료함이 진화할 수 있는 소프트웨어의 정수다.

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

  • 코드는 명확해야 한다.
  • 리팩터링을 효과적으로 하는 핵심은, 단계를 잘게 나눠야 더 빠르게 처리할 수 있고, 코드는 절대 깨지지 않으며, 이러한 작은 단계들이 모여서 상당히 큰 변화를 이룰 수 있다는 사실을 깨닫는 것이다.

2. 리팩터링 원칙

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

  • 리팩터링하는 동안에는 코드가 항상 정상 작동하기 때문에 전체 작업이 끝나지 않았더라도 언제든 멈출 수 있다.
  • 사용자 관점에서는 달라지는 점이 없어야 한다.
  • 리팩터링의 목적은 코드를 이해하고 수정하기 쉽게 만드는 것이다. 프로그램 성능은 좋아질 수도, 나빠질 수도 있다.

두 개의 모자

  • 리팩터링할 때는 '리팩터링' 모자를 쓴 다음 기능 추가는 절대 하지 않기로 다짐한 뒤 오로지 코드 재구성에만 전념한다.
  • (앞 과정에서 놓친 테스트 케이스를 발견하지 않는 한) 테스트도 새로 만들지 않는다.
  • 부득이 인터페이스를 변경해야 할 때만 기존 테스트를 수정한다.

누군가 "리팩터링하다가 코드가 깨져서 며칠이나 고생했다"라고 한다면, 십중팔구 리팩터링한 것이 아니다.

리팩터링하는 이유

  • 리팩터링하면 소프트웨어 설계가 좋아진다. 같은 일을 하더라도 설계가 나쁘면 코드가 길어지기 십상이다. 중복 코드를 제거하면 모든 코드가 언제나 고유한 일을 수행함을 보장할 수 있으며, 이는 바람직한 설계의 핵심이다.
  • 리팩터링하면 소프트웨어를 이해하기 쉬워진다. 모듈화가 잘 되어 있으면 전체 코드 중 작은 일부만 이해하면 된다.
  • 리팩터링하면 버그를 쉽게 찾을 수 있다. 코드가 명확하면 버그를 만들 가능성도 줄고, 버그를 만들더라도 디버깅하기가 훨씬 쉽다.
  • 리팩터링하면 프로그래밍 속도를 높일 수 있다. 내부 설계가 잘 된 소프트웨어는 새로운 기능을 추가할 지점과 어떻게 고칠지를 쉽게 찾을 수 있다. 내부 품질이 뛰어난 코드베이스는 새 기능 구축을 돕는 견고한 토대가 된다.

designStaminaGraph

설계가 좋으면 기능을 추가하기 쉽다.

언제 리팩터링해야 할까?

지금 위치에서 동쪽으로 100km를 이동하려는데 그 사이를 숲이 가로막고 있다면, 좀 둘러가더라도 20km 북쪽에 있는 고속도로를 타는 편이 세 배나 빠를 수 있다. 다들 "직진!"을 외치더라도, 때로는 "잠깐, 지도를 보고 가장 빠른 경로를 찾아보자"고 말할 줄 알아야 한다. 준비를 위한 리팩터링이 바로 이런 역할을 한다.

  • 코드를 파악할 때마다 그 코드의 의도가 더 명확하게 드러나도록 리팩터링할 여지는 없는지 찾아본다.
  • 코드를 분석할 때 리팩터링을 해보면, 그렇지 않았더라면 도달하지 못했을 더 깊은 수준까지 이해하게 된다.
  • 간단히 수정할 수 있는 것은 즉시 고치고, 시간이 좀 걸리는 일은 짧은 메모만 남긴 다음, 하던 일을 끝내고 나서 처리한다.
  • 리팩터링의 멋진 점은 각각의 작은 단계가 코드를 깨뜨리지 않는다는 사실이다. 그래서 작업을 잘게 나누면 몇 달에 걸쳐 진행하더라도 그 사이 한 순간도 코드가 깨지지 않기도 한다.

보기 싫은 코드를 발견하면 리팩터링하자. 그런데 잘 작성된 코드 역시 수많은 리팩터링을 거쳐야 한다.

  • 뛰어난 개발자는 새 기능을 추가하기 쉽도록 코드를 '수정'하는 것이 그 기능을 가장 빠르게 추가하는 길일 수 있음을 안다.
  • 그동안 리팩토링에 소홀했다면, 따로 시간을 내서 새 기능을 추가하기 쉽도록 코드베이스를 개선할 필요가 있다.
  • 계획된 리팩터링을 하게 되는 일은 최소한으로 줄여야 한다. 리팩터링 작업 대부분은 드러나지 않게, 기회가 될 때마다 해야 한다.
  • 리팩터링해야 할 코드와 관련한 작업을 하게 될 때마다 원하는 방향으로 조금씩 개선하는 식이다.

무언가 수정하려 할 때는 먼저 수정하기 쉽게 정돈하고(만만치 않을 수 있다) 그런 다음 쉽게 수정하다.

  • 리팩터링은 다른 이의 코드를 리뷰하는 데도 도움된다.
  • 리팩터링은 코드 리뷰의 결과를 더 구체적으로 도출하는 데에도 도움된다.
  • 버그를 수정하려면 현재 소프트웨어의 동작 방식을 이해해야 한다. 이때도 리팩터링부터 하는 편이 가장 빠르다.
  • 프로 개발자에게 주어진 임무는 새로운 기능을 빠르게 구현하는 것이고, 가장 빠른 방법은 리팩터링이다. 그래서 리팩터링부터 한다.

리팩터링하지 말아야 할 때

  • 외부 API 다루듯 호출해서 쓰는 코드라면 지저분해도 그냥 둔다.
  • 리팩터링하는 것보다 처음부터 새로 작성하는 게 쉬울 때도 리팩터링하지 않는다.

리팩터링 시 고려할 문제

  • 무언가를 언제 어디에 적용할지 판단하려면 손익을 제대로 이해해야 한다.

새 기능 개발 속도 저하

  • 새 기능을 구현해넣기 편해지겠다 싶은 리팩터링이라면 주저하지 않고 리팩터링부터 한다.
  • 직접 건드릴 일이 거의 없거나, 불편한 정도가 그리 심하지 않다고 판단되면 리팩터링하지 않는 편이다.
  • 어떻게 개선해야 할지 확실히 떠오르지 않아서 리팩터링을 미루기도 한다.
  • 코드베이스가 건강하면 기존 코드를 새로운 방식으로 조합하기 쉬워서 복잡한 새 기능을 더 빨리 추가할 수 있다.
  • 리팩터링의 본질은 코드베이스를 예쁘게 꾸미는 데 있지 않다. 오로지 경제적인 이유료 하는 것이다.
  • 리팩터링은 개발 기간을 단축하고자 하는 것이다. 기능 추가 시간을 줄이고, 버그 수정 시간을 주여준다.
  • 리팩터링하도록 이끄는 동력은 어디까지나 경제적인 효과에 있다.

리팩터링의 궁극적인 목적은 개발 속도를 높여서, 더 적은 노력으로 더 많은 가치를 창출하는 것이다.

코드 소유권

  • 함수 이름을 변경할 때는 함수 이름 바꾸기를 적용하는한편, 기존 함수도 그대로 유지하되 함수 본문에서 새 함수를 호출하도록 수정한다. 인터페이스는 복잡해지지만 클라이언트에 영향을 주지 않기 위해서는 어쩔 수 없다.

브랜치

  • CI에 따르면 모든 팀원이 하루에 최소 한 번은 마스터와 통합한다. 이렇게 하면 다른 브랜치들과의 차이가 크게 벌어지는 브랜치가 없어져서 머지의 복잡도를 상당히 낮출 수 있다.
  • 마스터를 건강하게 유지하고, 거대한 기능을 잘게 쪼개는 법을 배우고, 각 기능을 끌 수 있는 기능 토클(feature toggle)을 적용하여 완료되지 않은 기능이 시스템 전체를 망치지 않도록 해야 한다.
  • 기능별 브랜치를 사용할 때, 브랜치를 자주 통합할 수만 있다면 문제가 발생할 가능성을 크게 줄일 수 있다.

테스팅

  • 리팩터링의 두드러진 특성은 프로그램의 겉보기 동작은 똑같이 유지된다는 것이다.
  • 핵심은 오류를 재빨리 잡는데 있다.
  • 리팩터링하기 위해서는 (대부분의 경우에) 자가 테스트 코드(self-testing code)를 마련해야 한다.
  • 자가 테스트 코드는 리팩터링을 할 수 있게 해줄 뿐만 아니라, 새 기능 추가도 훨씬 안전하게 진행할 수 있도록 도와준다.
  • 리팩터링 과정에서 버그가 생길 위험이 아주 크다는 불안을 해소할 수 있다.

레거시 코드

  • 프로그램에서 테스트를 추가할 틈새를 찾아서 시스템을 테스트해야 한다. 테스트 없이 진행하기 때문에 상당히 위험하지만 문제를 해결하기 위해서라면 감내해야 할 위험이다.
  • 서로 관련된 부분끼리 나눠서 하나씩 공략한다.
  • 코드의 한 부분을 훑고 넘어갈 때마다 예전보다 조금이라도 개선하려고 노력한다. 캠핑 규칙에 따라 처음 왔을 때보다 깨끗하게 치우는 것이다.
  • 코드를 훑게 되는 횟수가 많다는 말은 그 부분을 이해하기 쉽게 개선했을 때 얻는 효과도 그만큼 크다는 뜻이다.

데이터베이스

  • 변환을 수행하는 코드를 간단히 작성한 다음, 선언된 데이터 구조나 접근 루틴을 변경하는 코드와 함께 버전 관리 시스템에 저장한다. 그런 다음 데이터베이스를 다른 버전으로 이전할 때마다 현재 버전에서 원하는 버전 사이에 있는 모든 마이그레이션 스크립트를 실행한다.

리팩터링, 아키텍처, 애그니(YAGNI)

  • 리팩터링으로 기존 코드 설계를 개선할 수 있다.
  • 코딩 전에 아키텍처를 확정지으려 할 떄의 대표적인 문제는 소프트웨어 요구사항을 사전에 모두 파악해야 한다는 것이다.
  • 앞으로 어느 부분에 유연성이 필요하고 어떻게 해야 그 변화에 가장 잘 대응할 수 있을지 추측하지 않고, 그저 현재까지 파악한 요구사항만을 해결하는 소프트웨어를 구축한다. 단, 이 요구를 멋지게 해결할 수 있도록 설계한다. 진행하면서 사용자의 요구사항을 더 잘 이해하게 되면 아키텍처도 그에 맞게 리팩터링해서 바꾼다.
  • YAGNI(You Aren’t Going to Need It, 필요 없을거다)는 아키텍처와 설계를 개발 프로세스에 녹이는 또 다른 방식이며, 리팩터링의 뒷받침 없이는 효과를 볼 수 없다.
  • 나중에 문제를 더 깊이 이해하게 됐을 때 처리하는 쪽이 훨씬 낫다고 생각하는 편이다. 이러한 경향은 진화형 아키텍처(Evolutionary architecture) 원칙이 발전하는 계기가 됐다(진화형 아키텍처는 아키텍처 관련 결정을 시간을 두고 반복해 내릴 수 있다는 장점을 활용하는 패턴과 실천법을 추구한다).

리팩터링과 소프트웨어 개발 프로세스

  • 애자일을 제대로 적용하려면 리팩터링에 대한 팀의 역량과 열정이 뒷받침되어 프로세스 전반에 리팩터링이 자연스럽게 스며들도록 해야 한다.
  • 리팩터링의 첫 번째 토대는 자가 테스트 코드다.
  • 팀으로 개발하면서 리팩터링을 하려면 각 팀원이 다른 사람의 작업을 방해하지 않으면서 언제든지 리팩터링할 수 있어야 한다.
  • 자가 테스트 코드 역시 지속적 통합의 핵심 요소다. 따라서 자가 테스트 코드, 지속적 통합, 리팩터링으로는 세 기법은 서로 강력한 상승효과를 발휘한다.
  • 리팩터링이 YAGNI의 토대인 동시에, YAGNI로 인해 리팩터링을 더욱 쉽게 할 수 있다.

리팩터링과 성능

  • 실제로 소프트웨어를 이해하기 쉽게 만들기 위해 속도가 느려지는 방향으로 수정하는 경우가 많다.
  • 리팩터링하면 소프트웨어가 느려질 수도 있는건 사실이다. 하지만 그와 동시에 성능을 튜닝하기는 더 쉬워진다. 하드 리얼타임(hard real-time) 시스템을 제외한 소프트웨어를 빠르게 만드는 비결은, 먼저 튜닝하기 쉽게 만들고 나서 원하는 속도가 나게끔 튜닝하는 것이다.
  • 성능을 개선하기 위해 코드를 수정하다 보면 프로그램은 다루기 어려운 형태로 변하기 쉽고, 결국 개발이 뎌뎌진다.

시스템에 대해 잘 알더라도 섣불리 추측하지 말고 성능을 측정 해봐야 한다. 그러면 새로운 사실을 배우게 되는데, 십중팔구 내가 잘못 알고 있었음을 깨닫게 된다.

  • 성능에 대한 흥미로운 사실은, 대부분 프로그램은 전체 코드 중 극히 일부에서 대부분의 시간을 소비한다는 것이다. 그래서 코드 전체를 고르게 최적화한다면 그중 90%는 효과가 거의 없기 때문에 시간 낭비인 셈이다.
  • 먼저 프로파일러로 프로그램을 분석하여 시간과 공간을 많이 잡아먹는 지점을 알아낸다.
  • 리팩터링할 때처럼 최적화를 위한 수정도 작은 단계로 나눠서 진행한다. 각 단계마다 컴파일과 테스트를 거치고 프로파일러를 다시 실행해본다. 성능이 개선되지 않았다면 수정 내용을 되돌린다. 이런 식으로 사용자가 만족하는 성능에 도달할 때까지 최적화 대상을 찾아서 제거하는 일을 계속한다.
  • 리팩터링은 성능 좋은 소프트웨어를 만드는데 기여한다. 단기적으로 보면 리팩터링 단계에서는 성능이 느려질 수도 있다. 하지만 최적화 단계에서 코드를 튜닝하기 훨씬 쉬워지기 때문에 결국 더 빠른 소프트웨어를 얻게 된다.

3. 코드에서 나는 악취

기이한 이름(Mysterious Name)

  • 코드를 명료하게 표현하는 데 가장 중요한 요소 하나는 바로 '이름'이다. 그래서 함수, 모듈, 변수, 클래스 등은 그 이름만 보고도 각각이 무슨 일을 하고 어떻게 사용해야 하는지 명확히 알 수 있도록 엄청나게 신경 써서 이름을 지어야 한다.
  • 마땅한 이름이 떠오르지 않는다면 설계에 더 근본적인 문제가 숨어 있을 가능성이 높다. 그래서 혼란스러운 이름을 잘 정리하다 보면 코드가 훨씬 간결해질 때가 많다.

긴 함수(Long Function)

  • 간접 호출(indirection)의 효과, 즉 코드를 이해하고, 공유하고, 선택하기 쉬워진다는 장점은 함수를 짧게 구성할 때 나오는 것이다.
  • 주석을 달아야 할 만한 부분은 무조건 함수로 만든다. 그 함수 본문에는 원래 주석으로 설명하려던 코드가 담기고, 함수 이름은 동작 방식이 아닌 '의도(intention)'가 드러나게 짓는다.
  • 핵심은 함수의 길이가 아닌, 함수의 목적(의도)과 구현 코드의 괴리가 얼마나 큰가다. 즉, '무엇을 하는지'를 코드가 잘 설명해주지 못할수록 함수로 만드는 게 유리하다.
  • 조건문이나 분복문도 추출 대상의 실마리를 제공한다.
  • 반복문도 그 안의 코드와 함께 추출해서 독립된 함수로 만든다.

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

  • 클래스는 매개변수 목록을 줄이는 데 효과적인 수단이기도 하다. 특히 여러 개의 함수가 특정 매개변수들의 값을 공통으로 사용할 때 유용하다.

전역 데이터(Global Data)

  • 전역 데이터의 대표적인 형태는 전역 변수지만 클래스 변수와 싱글톤에서도 같은 문제가 발생한다.
  • 이런 데이터를 함수로 감싸는 것만으로도 데이터를 수정하는 부분을 쉽게 찾을 수 있고 접근을 통제할 수 있게 된다.
  • 접근자 함수들을 클래스나 모듈에 집어넣고 그 안에서만 사용할 수 있도록 접근 범위를 최소로 줄이는 것도 좋다.

가변 데이터(Mutable Data)

  • 코드의 다른 곳에서는 다른 값을 기대한다는 사실을 인식하지 못한 채 수정해버리면 프로그램이 오작동한다.
  • 함수형 프로그래밍에서는 데이터는 절대 변하지 않고, 데이터를 변경하려면 반드시 (원래 데이터는 그대로 둔 채) 변경하려는 값에 해당하는 복사본을 만들어서 반환한다는 개념을 기본으로 삼고 있다.
  • 값을 다른 곳에서 설정할 수 있는 가변 데이터가 풍기는 악취는 특히 고약하다.

뒤엉킨 변경(Divergent Change)

  • 뒤엉킨 변경은 단일 책임 원칙(Single Responsibility Principle, SRP)이 제대로 지켜지지 않을 때 나타난다. 즉, 하나의 모듈이 서로 다른 이유들로 인해 여러 가지 방식으로 변경되는 일이 많을 때 발생한다.

산탄총 수술(Shotgun Surgery)

  • 이 냄새는 코드를 변경할 때마다 자잘하게 수정해야 하는 클래스가 많을 때 풍긴다.
  • 변경할 부분이 코드 전반에 퍼져 있다면 찾기도 어렵고 꼭 수정해야 할 곳을 지나치기 쉽다.
  • 이럴 때는 함께 변경되는 대상들을 모두 한 모듈에 묶어두면 좋다.
  뒤엉킨 변경 산탄총 수술
원인 맥락을 잘 구분하지 못함 맥락을 잘 구분하지 못함
해법(원리) 맥락을 명확히 구분 맥락을 명확히 구분
발생 과정(현상) 한 코드에 섞여 들어감 여러 코드에 흩뿌려짐
해법(실제 행동) 맥락별로 분리 맥락별로 모음

기능 편애(Feature Envy)

  • 프로그램을 모듈화할 때는 코드를 여러 영역으로 나눈 뒤 영역 안에서 이뤄지는 상호작용은 최대한 늘리고, 영역 사이에서 이뤄지는 상호작용은 최소로 줄이는 데 주력한다.
  • 기능 편애는 흔히 어떤 함수가 자기가 속한 모듈의 함수나 데이터보다 다른 모듈의 함수나 데이터와 상호작용 할 일이 더 많을 때 풍기는 냄새다.
  • 이럴 때는 그 부분만 독립 함수로 빼낸 다음 원하는 모듈로 보내준다.
  • 디자인 패턴 중 전략 패턴(Strategy pattern)과 방문자 패턴(Visitor Pattern)이 있다. 켄트 벡의 자기 위임(Self-Delegation)도 여기 속한다.
  • 이들은 모두 뒤엉킨 변경 냄새를 없앨 때 활요하는 패턴들로, 가장 기본이 되는 원칙은 '함께 변경할 대상을 한데 모으는 것'이다.

데이터 뭉치(Data Clumps)

  • 데이터 뭉치인지 판별하려면 값 하나를 삭제해보자. 그랬을 때 나머지 데이터만으로 의미가 없다면 객체로 환생하길 갈망하는 데이터 뭉치라는 뜻이다.
  • 클래스를 이용하면 좋은 향기를 흩뿌릴 기회가 생긴다.
  • 이러한 연계 과정은 종종 상당한 중복을 없애고 향후 개발을 가속하는 유용한 클래스를 탄생시키는 결과로 이어지기도 한다.

기본형 집착(Primitive Obsession)

  • 최소한 사용자에게 보여줄 때는 일관된 형식으로 출력해주는 기능이라도 갖춰야 한다.
  • 기본형을 객체로 바꾸기를 적용하면 기본형만이 거주하는 구석기 동굴을 의미 있는 자료형들이 사는 최신 온돌식 코드로 탈바꿈시킬 수 있다.

반복되는 switch문(Repeated Switches)

  • 중복된 switch 문이 문제가 되는 이유는 조건절을 하나 추가할 때마다 다른 switch문들도 모두 찾아서 함께 수정해야 하기 때문이다. 이럴 때 다형성은 반복된 switch문이 내뿜는 사악한 기운을 제압하여 코드베이스를 최신 스타일로 바꿔주는 세련된 무기인 셈이다.

반복문(Loops)

  • 일급 함수(first-class function)을 지원하는 언어가 많아졌기 때문에 반복문을 파이프라인으로 바꾸기를 적용해서 시대에 걸맞지 않은 반복문을 제거할 수 있게 됐다.

성의 없는 요소(Lazy Element)

  • 우리는 코드의 구조를 잡을 때 프로그램 요소(프로그램 언어가 제공하는 함수, 클래스, 인터페이스 등 코드 구조를 잡는데 활용되는 요소)를 이용하는걸 좋아한다. 그래야 그 구조를 변경하거나 재활용할 기회가 생기고, 혹은 단순히 더 의미 있는 이름을 가졌기 떄문이다. 그렇지만 그 구조가 필요 없을 때도 있다.

추측성 일반화(Speculative Generality)

  • '나중에 필요할 거야'라는 생각으로 당장은 필요 없는 모든 종류의 후킹(hooking) 포인트와 특이 케이스 처리 로직을 작성해둔 코드에서 풍긴다.
  • 미래를 대비해 작성한 부분을 실제로 사용하게 되면 다행이지만, 그렇지 않는다면 쓸데없는 낭비일 뿐이다. 당장 걸리적거리는 코드는 눈앞에서 치워버리자.

임시 필드(Temporary Field)

  • 간혹 특정 상황에서만 값이 설정되는 필드를 가진 클래스도 있다. 하지만 객체를 가져올 때는 당연히 모든 필드가 채워져 있으리라 기대하는 게 보통이라, 이렇게 임시 필드를 갖도록 작성하면 코드를 이해하기 어렵다.
  • 특이 케이스 추가하기로 필드들이 유효하지 않을 때를 위한 대안 클래스를 만들어서 제거할 수 있다.

메시지 체인(Message Chains)

  • 메시지 체인은 클라이언트가 한 객체를 토앻 다른 객체를 얻은 뒤 방금 얻은 객체에 또 다른 객체를 요청하는 식으로, 다른 객체를 요청하는 작업이 연쇄적으로 이어지는 코드를 말한다.
  • 이 문제는 위임 숨기기로 해결한다. 이 리팩터링은 메시지 체인의 다양한 연결점에 적용할 수 있다.
  • 최종 결과 객체가 어떻게 쓰이는지부터 살펴보는게 좋다.
// 체인을 구성하는 모든 객체에 위임 숨기기를 적용할 수 있다.
// => 부서장 이름을 바로 반환하는 메서드를 사람 클래스에 추가할 수도 있고, 부서 클래스에 추가할 수도 있다는 뜻
// => 혹은 부서장을 얻는 메서드를 사람 클래스에 추가할 수도 있다.
managerName = person.department.managerName // 관리자 객체(manager)의 존재를 숨김
managerName = person.manager.name           // 부서 객체(department)의 존재를 숨김
managerName = person.managername            // 부서 객체와 관리자 객체 모두의 존재를 숨기

중개자(Middle Man)

  • 클래스가 제공하는 메서드 중 절반이 다른 클래스에 구현을 위임하고 있다면 어떤가?
  • 이럴 때는 중개자 제거하기를 활용하여 실제로 일을 하는 객체와 직접 소통하게 하자.

내부자 거래(Insider Trading)

  • 모듈 사이의 데이터 거래가 많으면 결합도(coupling)가 높아진다. 일이 돌아가게 하려면 거래가 이뤄질 수 밖에 없지만, 그 양을 최소로 줄이고 모두 투명하게 처리해야 한다.
  • 여러 모듈이 같은 관심사를 공유한다면 공토 ㅇ부분을 정식으로 처리하는 제 3의 모듈을 새로 만들어가 위임 숨기기를 이용하여 다른 모듈이 중간자 역할을 하게 만든다.

거대한 클래스(Large Class)

  • 한 클래스가 너무 많은 일을 하려다 보면 필드 수가 상당히 늘어난다. 그리고 클래스에 필드가 너무 많으면 중복 코드가 생기기 쉽다.
  • 이럴 때는 클래스 추출하기로 필드들 일부를 따로 묶는다. 같은 컴포넌트에 모아두는 것이 합당해 보이는 필드들을 선택하면 된다.
  • 필드가 너무 ㅁ낳은 클래스와 마찬가지로 코드량이 너무 많은 클래스도 중복 코드와 혼동을 일으킬 여지가 크다.
  • 가장 간단한 해법은 그 클래스 안에서 자체적으로 중복을 제거하는 것이다.

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

  • 클래스를 사용할 떄의 큰 장점은 필요에 따라 언제든 다른 클래스로 교체할 수 있다는 것이다. 단, 교체하려면 인터페이스가 같아야 한다.

데이터 클래스(Data Class)

  • 데이터 클래스란 데이터 필드와 게터/세터 메서드로만 구성된 클래스를 말한다.
  • 변경하면 안 되는 필드는 세터 제거하기로 접근을 원천 봉쇄한다.
  • 데이터 클래스는 필요한 동작이 엉뚱한 곳에 정의돼 있다는 신호일 수도 있다. 이런 경우라면 클라이언트 코드를 데이터 클래스로 옮기기만 해도 대폭 개선된다.
  • 불변(immutable) 필드는 굳이 캡슐화할 필요가 없고, 불편 데이턱로부터 나오는 정보는 게터를 통하지 않고 그냥 필드 자체를 공개해도 된다.

상속 포기(Refused Bequest)

  • 먼저 같은 계층에 서브 클래스를 하나 새로 만들고, 메서드 내리기와 필드 내리기를 활용해서 물려받지 않을 부모 코드를 모조리 새로 만든 서브클래스로 넘긴다.
  • 상속 포기 냄새는 서브클래스가 부모의 동작은 필요로 하지만 인터페이스는 따르고 싶지 않을 때 특히 심하다.
  • 구현을 따르지 않느 것은 이해할 수 있지만 인터페이스를 따르지 않는다는 것은 상당히 무례한 태도다.

주석(Comments)

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

  • 특정 코드 블록이 하는 일에 주석을 남기고 싶다면 함수 추출하기를 적용해본다.
  • 뭘 할지 모를 때라면 주석을 달아두면 좋다. 현재 진행 상황뿐만 아니라 확실하지 않은 부분에 주석을 남긴다.
  • 코드를 지금처럼 작성한 이유를 설명하는 용도로 달 수도 있다.

4. 테스트 구축하기

자가 테스트 코드의 가치

모든 테스트를 완전히 자동화하고 그 결과까지 스스로 검사하게 만들자.

  • 테스트를 작성하기 가장 좋은 시점은 프로그래밍을 시작하기 전이다.
  • 테스트를 모두 통과한 시점이 바로 코드를 완성한 시점이다.

테스트 스위트(test suite)는 강력한 코드 검출 도구로, 버그를 찾는 데 걸리는 시간을 대폭 줄여준다.

  • TDD(Test-Driven Development)에서는 (처음에는 통과하지 못할) 테스트를 작성하고, 이 테스트를 통과하게끔 코드를 작성하고, 결과 코드를 최대한 깔끔하게 리팩터링하는 과정을 짧은 주기로 반복한다.
  • 간혹 테스트가 갖춰지지 않은 코드를 리팩터링해야 할 때도 있다. 그럴 떄는 곧바로 리팩터링하지 않고, 먼저 자가 테스트 코드부터 작성한다.
  • "최근 변경을 취소하고 마지막으로 모든 테스트를 통과했던 상태로 돌아가라"라고 전하고 싶을 때는 "초록 막대로 되돌려라"라고 말한다.

테스트 추가하기

완벽하게 만드느라 테스트를 수행하지 못하느니, 불완전한 테스트라도 작성해 실행하는게 낫다.

  • 테스트는 위험 요인을 중심으로 작성해야 한다!
  • 테스트의 목적은 어디까지나 현재 혹은 향후에 발생하는 버그를 찾는 데 있다.
  • 잘못될까봐 가장 걱정되는 영역을 집중적으로 테스트하는데, 이렇게 해서 테스트에 쏟는 노력의 효과를 극대화하는 것이다.
describe('province', function() {
  // Bad
  const asia = new Province(sampleProvinceData())
  it('shortfall', function() {
    expect(asia.shortfall).equal(5)
  })
  it('profit', function() {
    expect(asia.profit).equal(230)
  })
})
  • 테스트 관련 버그 중 가장 지저분한 유형인 '테스트끼리 상호작용하게 하는 공유 픽스처'를 생성하는 원인이 된다. 테스트를 실행하는 순서에 따라 결과가 달라질 수 있다.
describe('province', function() {
  // Good
  let asia ;
  beforeEach(function () {
    asia = new Province(sampleProvinceData())
  })

  it('shortfall', function() {
    expect(asia.shortfall).equal(5)
  })
  it('profit', function() {
    expect(asia.profit).equal(230)
  })
})
  • beforeEach 구문을 활용해서 개별 테스트를 실행할 때마다 픽스처를 새로 만들면 모든 테스트를 독립적으로 구성할 수 있어서, 결과를 예측할 수 없어 골치를 썩는 사태를 예방할 수 있다.

픽스처 수정하기

  • 설정-실행-검증(setup-exercise-verify), 조건-발생-결과(given-when-then), 준비-수행-단언(arrange-act-assert)
  • 일반적으로 it 구문 하나당 검증도 하나씩만 하는게 좋다.

경계 조건 검사하기

문제가 생길 가능성이 있는 경계 조건을 생각해보고 그 부분을 집중적으로 테스트하자.

  • 의식적으로 프로그램을 망가뜨리는 방법을 모색한다.
  • 실패(failure)란 검증 단계에서 실제 값이 예상 범위를 벗어났다는 뜻이다. 에러(error)와 성격이 다르다.
  • 위험한 부분에 집중하는게 좋다.
  • 테스트가 모든 버그를 걸러주지는 못할지라도, 안심하고 리팩터링할 수 있는 보호막은 되어준다.

끝나지 않은 여정

버그 리포트를 받으면 가장 먼저 그 버그를 드러내는 단위 테스트부터 작성하자.

  • 단위 테스트(unit test)란 코드의 작은 영역만을 대상으로 빠르게 실행되도록 설계된 테스트다.
  • 단위 테스트는 자가 테스트 코드의 핵심이자, 자가 테스트 시스템은 대부분 단위 테스트가 차지한다.
  • 기능을 새로 추가할 때마다 테스트도 추가하는 것은 물론, 기존 테스트도 다시 살펴본다.
  • 기존 테스트가 충분히 명확한지, 테스트 과정을 더 이해하기 쉽게 리팩터링할 수는 없는지, 제대로 검사하는지 등을 확인한다.
  • 버그를 발견하는 즉시 발견한 버그를 명확히 잡아내는 테스트부터 작성하는 습관을 들이자.

Reference