켄트 백 할머님의 원칙 냄새 나면 당장 갈아라.
이제 리팩토링을 어떻게 작동하는지 감이 왔을 것이다.
하지만 리팩토링 적용 방법을 아는 것과 제때 적용할 줄 아는 것은 다르다. (제때 적용하는 것에 대해서 3의 법칙을 설명하지 않았나? 그리고 기능 추가전에 코드를 분석해보면서 더 좋은 구조로 만들고 하라고 했었다고도 했고, 애초에 수시로 리팩토링을 하라고 했었던것 같다. 이렇게 설명을 해줬는데 리팩토링을 어느 시점에 적용해야 하는 지에 대해서 이 챕터에서 조금 더 자세하게 다루는 것인가?)
리팩토링을 언제 시작하고 언제 끝내는 지를 아는 것도 매우 중요하다.
여기서 딜레마는 리팩토링의 기법 중 인스턴스 변수를 삭제하고 상속 계층을 만드는 것을 설명하기는 쉽지만 이런 일들을 언제 적용 하는지
에 대한 명확한 규칙은 없다.
나 같은 경우는 프로그래밍의 미학이라는 애매모호한 개념에 기대서 이유를 설명하는 경우가 많은데 이도 적합 하지는 않다.
그러므로 구체적인 적용 시점에 대해 논의를 해보자.
이 책의 초판을 집필히면서 켄트 백을 만나러 갔었는데 그 당시의 켄트 백은 갓 태어난 딸을 돌보고 있었다.
딸의 기저귀 냄새에 민감했던 켄트 백은 이 경험을 빗대어서 리팩토링을 적용할 시점을 악취 라는 표현으로 설명을 했다.
그렇다면 냄새_ 라은 표현이 미학 이라는 표현보다 나은가? 라고 물으면 그렇다.
나와 켄트는 수 많은 프로젝트를 경험하면서 많은 코드를 봐왔다.
그 과정 속에서 리팩토링이 필요한 코드들은 일정한 패턴이 있다라는 사실을 발견했다. (이 부분이 핵심이겠네. 이 패턴을 기점으로 리팩토링을 언제 적용하면 되는지 기준을 세우면 되겠다.)
(이 장에서는 켄트와 내가 같이 작업을 한 것이므로 우리라는 표현을 사용하겠다.)
하지만 리팩토링을 언제 멈춰야 하는지에 대한 정확한 기준을 세우지는 않을 것이다.
왜냐하먼 우리 경험상 숙련된 프로그래머의 경험만큼 정확한 기준은 없기 때문이다. (이게 이유구나. 그냥 자신이 할 수 있는 역량만큼. 그리고 개발 일정에 맞춰서. 라는 기준이 중요하지 않나 라는 생각이 든다. 내가 생각할 때 이 정도의 리팩토링 기법을 적용할 수 있겠지만 그러면 너무나 많은 일정이 소요되므로 구조만 바꿔 놓는다던지. )
인스턴스 변수는 몇 개가 적당한지, 메소드는 몇 줄이 적당한지 등을 감을 통해서 늘려나가야 한다. (그리고 애초에 좋은 코드의 기준이라는 게 클린 코드에서 있으니까. 이것들을 기준으로 삼으면 되겠네.)
(코드 퀄리티의 어느 정도의 끝은 존재하니까. 이거 기준으로 이것을 넘도록 하지는 않으면 되겠다. 한계치를 정하는 거지. YAGNI 라는 원칙을 적용한다고 생각해보면 되겠다.)
어떤 리팩토링 기법을 적용해야 할 지 모르겠다면 이 장의 내용과 함께 부록B 의 내용을 참고해보자. (부록의 내용을 봤는데 해당 악취에 따른 적용할 수 있는 기법이 매핑되어 있다. 해당 기법의 이름만 보고도 어떤 기법인지 명확하게 정리할 수 있어야 한다고 생각했고 얻는 이점과 놓치는 이점에 대해서도 정리가 필요하다고 생각한다.)
추리 소설이라면 무슨 일이 전개되는지 궁금 할 수 있지만 코드에선 아니다.
코드는 단순하고 명료해야한다. (이게 코드의 본질적인 목적이라고 생각한다.)
코드를 명료하게 표현하는데 가장 크게 기여하는 것 중 하나는 이름이다.
그래서 함수, 모듈, 변수, 클래스 이름만 보고도 무슨 일을 하는지 어떻게 사용하는지를 명확히 알 수 있어야 한다. (무슨 일을 하는지에 대해서는 늘 생각을 하는데 어떻게 사용하는지도 이름만 보고 알 수 있을까? 에 대한 고민은 크게 안했던 것 같다. 이것도 체크 리스트 중 하나.)
하지만 이름 짓기는 프로그래밍에서 가장 어렵다고 알려진 두 가지 방법 중 하나다. (나머지 하나는 캐시 무효화다.)
그 때문에 우리가 자주 사용하는 리팩토링도 함수 선언 바꾸기 (6.5절), 변수 이름 바꾸기 (6.7절), 필드 이름 바꾸기 (9.2절) 같은 리팩토링이다.
이름 바꾸기는 단순히 표현을 위한 방법이 아니다. 이름은 명확하게 무엇인지 드러내는 것으로 명확한 이름이 떠오르지 않는다면 설계가 잘못된 것일 수도 있다. 이를 명심해두자.
똑같은 코드 구조가 여러 곳에서 반복된다면 하나로 통합해서 더 나은 프로그램으로 변경 할 수 있다. (엔지니어링 원칙 DRY. 변경 포인트를 하나로 바꾸도록 하는 것.)
중복 코드를 해결 할 땐 서로 차이점이 없는지 살펴보는게 중요하다. (차이점이 있다면 이를 해결하는 디자인 패턴의 방법으로 템플릿 메소드 패턴, 전략 패턴. 등이 있다.)
가장 간단한 코드 중복의 예로 하나의 클래스 안에서 두 메소드가 똑같은 표현을 한다면 함수 추출하기 (6.1절) 를 적용할 수 있다.
코드가 비슷한데 완적 똑같지는 않다면 문장 슬라이스 (8.6절) 로 비슷한 부분을 모우고 함수 추출하기 (6.1절) 로 빼낼 수 있다.
같은 부모로 부터 파생된 서브 클래스에서 중복된 메소드가 있다면 각자 따로 만드는게 아니라 부모의 메소드를 통해서 호출하도록 하는 메소드 올리기 (12.1절) 방법도 있다.
우리의 경험에 비추어보면 오랜 기간 잘 활용되는 함수들은 대체로 길이가 짧았다. (함수의 재사용성 측면을 본 것인듯. 긴 함수는 대체로 특정한 일을 위해 작성된 경우가 많으니까. 재사용 되기는 힘들겠지. 그렇다면 긴 함수를 짧은 함수의 조합으로 구성된다면? )
짦은 함수들로 구성된 코드베이스를 훑어보면 자기가 계산하는 구조보다 위임하는 구조로 보인다.
이런 위임하는 구조는 코드를 이해하고 공유하기 쉽다.
짧은 함수로 만든다는 건 하나의 긴 함수처럼 모든 계산을 하는 것이 아니라 짧은 함수 여러개를 호출하는 구조이다.
이런 구조 때문에 성능 상으로 떨어진다는 생각을 할 수 있지만 요즘 프로그래밍 언어에서 이는 문제가 되지 않는다.
그리고 짧은 함수와 좋은 이름의 조합 은 짧은 함수의 구현을 보지 않아도 어떠한 일을 하는지 명확하게 알 수 있어서 코드를 이해하기가 훨씬 쉬어진다.
이를 위해서는 함수 자체를 적극적으로 짧게 구성하도록 해야한다.
긴 함수에서 우리가 주석으로 달아야 하는 부분이 있다면 그 부분은 함수로 빼내자. (적용 기법.)
그리고 그 함수의 이름은 동작 방식에 집중하지 말고 의도에 집중하자. (주석도 의도가 중요하죠.)
함수의 이름은 의도가 중요하고 이 의도와 실제 구현과의 괴리감이 얼마나 있느냐가 잘 짓는 기준이다.
즉 무엇을 하는지 설명해주지 못한다면 함수의 이름을 잘 지은게 아니다.
함수를 짧게 만드는 방법의 99% 는 함수 추출하기 (6.1절) 로 이뤄진다.
함수가 매개변수와 임시 변수를 많이 사용한다면 추출하기 어렵다.
많은 임시 변수는 임시 변수를 질의 함수로 바꾸기 (7.4절) 기법을 사용하면 되고 많은 매개변수는 매개 변수를 매개변수 객체 만들기 (6.8절) 와 객체 통째로 떠넘기기 (11.4절) 에 집중한다. (요즘 객체를 만들때 SOLID 원칙의 S 와 D 를 많이 신경쓰는게 중요하다고 생각한다.)
이런 리팩토링 기법을 적용해도 여전히 임시 변수와 매개 변수가 많다면 함수를 명령으로 만들기 (11.9절) 기법을 사용해도 좋다.
조건문이나 반복문도 추출의 대상이 된다.
조건문 분해하기 (10.1절) 로 대응하거나 switch 문을 구성하는 case 문 마다에 있는 내용을 함수로 추출하는 함수 추출하기 (6.1절) 를 사용하거나 같은 조건을 기준으로 나뉘는 switch 문이 여러개라면 조건문을 다형성으로 바꾸기 (10.4절) 를 적용하면 된다.
반복문도 그 안의 코드와 함께 추출해서 함수로 만드는 것도 가능하다.
추출한 반복문에 마땅한 이름이 떠오르지 않는다면 성격이 다른 두 가지의 일을 하는 것일 수도 있다.
이는 반복문 쪼개기 (8.7절) 를 사용하자.
우리가 프로그래밍을 시작하는 시절에는 함수에 필요한 모든 데이터를 함수 파라미터로 넘기라는 말을 들었다.
그래야 암적 존재인 전역 데이터의 사용을 줄일 수 있기 때문이다.
하지만 매개변수의 목록이 길어지면 그 자체로 이해하기 어렵다.
종종 다른 매개변수에서 값을 얻어올 수 있는 매개변수가 있다면 이런 매개변수는 매개 변수를 질의 함수로 바꾸기 (11.5절) 를 적용할 수 있다. (뭔 뜻이지?)
사용 중인 데이터 구조에서 값들을 각각 뽑아서 매개변수로 넘기는 구조라면 객체 통째로 넘기기 (11.4절) 를 적용할 수 있다.
항상 함께 전달되는 매개변수들이 있다면 이들을 묶는 방법인 매개변수 객체로 만들기 (6.8절) 를 적용할 수 있다.
함수의 동작을 제어하는 플래그 역할을 하는 매개변수는 플래그 인수 제거하기 (11.3절) 로 없애 줄 수 있다.
클래스를 통해서 매개변수를 줄일 수 있다. 만약에 여러 함수에서 공통적으로 사용하는 파라미터들이 있다면 이를 하나의 클래스 안으로 옮기면 파라미터 자체를 많이 줄일 수 있다.
이 기법은 여러 함수를 클래스로 묶기 (6.9절) 기법을 통해서 가능하다.
전역 데이터를 주의해야 한다는 점은 우리가 소프트웨어 개발을 처음 배우기 시작할 때부터 들었다.
전역 데이터를 악취 중에 가장 독한 악취 중의 하나다.
전역 데이터가 악취인 이유는 코드 베이스 어디에서나 변경이 가능하니까 이 변경 시점을 추적하는게 힘들기 때문이다.
그래서 버그는 끊임없이 발생하는데 이 원인이 되는 코드를 찾기가 힘들다.
전역 데이터의 대표적인 형태는 클래스 전역 변수와 싱글톤 객체가 있다.
이를 방지하기 위해 우리가 자주 사용하는 방법은 변수 캡슐화하기 (6.6절) 이다.
이를 통해 접근 포인트를 제어할 수 있다. 같은 패키지 내에서만 접근할 수 있도록 한다던지.
전역 데이터는 가변일 때 특히나 더 까다롭고 변경되지 않는다고 하면 그나마 안전한 편이다.
데이터를 변경했더니 예상치 못한 결과로 이어지는 경우가 종종 있다.
이런 문제는 아주 드물게 발생하지만 만약에 발생한다면 이를 찾기는 굉장히 어렵다.
이러한 이유로 함수형 프로그래밍에서는 데이터를 변경하지 않고 변경하는 경우라면 원래의 데이터의 복제본을 만들어서 사용하는 경우가 많다.
하지만 함수형 프로그래밍 자체를 사용하는 경우도 적고 변수 바꾸기를 지원하는 언어는 많다.
그렇다고 해서 불변성이 주는 장점을 포기할 필요는 없고 불변성을 주는 방법은 다양하게 있다.
가령 변수 캡슐화하기 (6.6절) 를 통해서 정해놓은 함수를 거쳐서만 변수에 접근할 수 있도록 하는 방법이 있다. 이를 통해 변수의 수정 포인트를 제한할 수 있고 수정되는 과정을 디버깅 하기도 쉽다.
하나의 번수에 용도가 다른 값들을 매번 갱신하는 경우가 있다면 변수 쪼개기 (9.1절) 를 통해서 독립된 변수로 만들어서 갱신되는 변수만 따로 고립시켜 놓는 방법도 있다.
이를 통해 갱신이 될 수 있는 변수만 따로 빼냈기 때문에 문제를 해결하기가 더 쉬워진다. (갱신되는 변수를 다른 코드로 분리시켜 놨으니 추적하기가 쉬워진다.)
그리고 갱신 로직 자체를 별도의 다른 메소드로 빼내서 코드를 분리하는 기법도 있다. 이는 함수 추출하기 (6.1절) 와 문장 슬라이스하기 (8.6절) 기법을 통해서 가능하다.
API 를 만들 때는 질의 함수와 변경 함수 분리하기 (11.1절) 를 구별시켜 놓는게 좋다. (이렇게 분리시켜 놓는 이유가 뭐였지? 역할이 넘 큰 탓인가? 클린 코드에서 본 것 같은데)
그리고 세터 제거하기 (11.7절) 를 통해서 애초에 변경될 여지를 막아놓는 것도 좋다. (애초에 변경될 여지를 막는 것이니.)
값을 다른 곳에서 설정할 수 있는 가변 데이터가 풍기는 악취는 고약하다. (이런것 같은데 다른 서비스에서 그 객체를 분해해서 설정하는 것. 메소드를 통해 행동시키는게 아니라.)
이럴 때는 파생 변수를 질의 함수로 바꾸기 (9.3절) 를 이용해 코드 전체에 골고루 뿌려준다.
변수의 유효 범위가 넓어질수록 위험도 덩달아 커진다. (변수를 바꿀 수 있는 범위를 말하는 듯. 특정 범위에서만 바꿀 수 있도록 하면 그렇게 큰 문제가 아니듯.)
따라서 여러 함수를 클래스로 묶기 (6.9절) 나, 여러 함수를 변환 함수로 묶기 (6.10절) 를 활용해서 특정 변수를 갱신하는 코드를 제한시켜 놓는게 좋다.
구조체처럼 내부 필드에 데이터를 담고 있는 구조라면 일반적으로 참조를 값으로 바꾸기 (9.4절) 를 적용해 내부 필드를 수정하지 말고 구조체를 통째로 교체하는 편이 좋다. (뭔 뜻이지?)
우리는 소프트웨어를 변경하기 쉬운 구조로 바꾼다.
이 말은 변경 포인트를 한 군데로 고립시킨다는 말을 뜻한다.
이렇게 할 수 없다면 뒤엉킨 변경과 산탄총 수술 중 하나 때문에 그렇다. (두 패턴 모두 악취를 말한다.)
뒤엉킨 변경은 SRP 가 제대로 지켜지지 않을 때 발생하는 원칙이다.
즉 하나의 모듈이 여러 가지 원인 때문에 변경될 때를 말한다.
예컨대 하나의 기능을 지원하기 위해 고쳐야 하는 메소드가 여러 군데라면 이는 뒤엉킨 변경이 발생한 것이다.
예를 들어 새로운 데이터베이스를 지원하려고 하는데 함수 세 개를 바꿔야하고, 금융상품이 하나씩 추가될 때마다 또 다른 함수 네 개를 바꿔야 한다면 이는 뒤엉킨 변경이 발생한 것이다.
데이터베이스 연동과 금융 상품 처리는 다른 맥락 에서 이뤄지므로 독립된 모듈로 분리되어 있어야 한다. (맥락별로 모듈을 만들어 놓는게 중요하다고 생각한다. 이런 모듈을 잘 만드는게 SRP 를 지키는게 아닐까.)
그래야 무언가를 수정할 때 해당 맥락의 코드만 봐도 진행하는게 가능하다.
데이터베이스에서 데이터를 가져오고 금융 상품에서 처리하는 구조로 맥락이 잡혀 있다면 이렇게 맥락을 분리하기 위해서 단계를 분리하는 단계 쪼개기 (6.11절) 기법을 적용하는게 좋다.
전체 처리 과정에서 각기 다른 맥락의 함수를 호출하고 있다면 맥락을 가진 모듈을 만들어주고 관련 함수들을 모우는 함수 옮기기 (8.1절) 기법을 사용하는게 좋다.
이때 여러 맥락을 드나드는 함수가 있다면 이를 분리시키기 위해 함수 추출하기 (6.1절) 기법을 사용한다.
모듈이 클래스 단위라면 클래스 추출하기 (7.5절) 기법이 맥락별로 분리하는데 기여해줄 것이다.
산탄총 수술은 뒤엉킨 변경과 비슷하면서도 정반대다. (무슨 뜻? 아. 산탄총 수술은 흩어진 맥락을 모우는 용도. 뒤엉킨 변경은 하나의 거대한 맥락을 작은 맥락으로 나누는 용도.)
이 냄새는 코드를 변경할 때마다 자잘하게 수정해야 하는 클래스가 많을 때 풍긴다.
변경할 부분이 코드 전반에 흩어져 있다면 찾기가 어렵다. 그리고 수정할 부분을 놓치기 쉽다 (변경 포인트를 제한하라 라는 뜻 같은데)
이럴 때는 함께 변경되는 대상들을 함수 옮기기 (8.1절) 와 필드 옮기기 (8.2절) 로 모두 한 모듈로 묶어두는게 좋다.
비슷한 데이터를 다루는 함수가 많다면 여러 함수를 클래스로 묶기 (6.9절) 기법을 통해서 모듈로 만들 수 있다.
구조를 변환하거나 보강하는 함수들이 많다면 여러 함수를 변환 함수로 묶기 (6.10절) 를 적용하면 된다. (이게 뭐지)
이렇게 묶은 함수들을 다음 단계로 전달하는 구조라면 단계 쪼개기 (6.11절) 를 적용하면 된다.
어설프게 분리된 로직을 함수 인라인 하기 (6.2절) 나 클래스 인라인 하기 (7.6절) 와 같은 리팩토링 기법을 적용하는 것도 좋다.
프로그램을 모듈화 할 때 고려해야 하는 사항으로는 같은 모듈에서는 상호작용을 최대한 늘리고 모듈과 다른 모듈 사이의 상호 작용은 최대한 줄여야 한다.
기능 편애는 흔히 어떤 모듈의 함수가 자신이 속한 모듈의 함수나 데이터와의 상호 작용보다 다른 모듈과의 상호작용이 더 많을 때 풍기는 냄새다.
흔히 Getter 메소드를 여러번 호출하면서 다른 데이터에 접근해서 상호작용할 때 풍기는 냄새로 해결하기는 쉽다.
그 데이터 근처로 함수를 추출해서 옮겨주면 된다.
때로는 함수 안에서 함수의 일부가 다른 모듈의 기능을 편애할 수 있다. 그런 경우도 마찬가지로 함수를 추출하기 (6.1절) 옮겨주면 된다.
어디로 옮겨야 할 지 명확하지 않은 경우가 있는데 이 경우에는 관련 데이터가 가장 많은 모듈로 옮기는게 적합하다. 이는 함수 옮기기 (8.1절) 를 이용하면 된다.
한편 앞의 두 문단에서 설명한 규칙을 거스르는 패턴이 있는데 전략 패턴(Strategy Pattern) 과 방문자 패턴(Visitor Pattern) 이 그 경우에 해당한다.
켄트 백의 자기 위임(Self Delegation) 도 여기에 속한다. (자기 위임 패턴과, 방문자 패턴이 뭔지 알아보고 전략 패턴이 머가 다른지를 생각해봐야겠다.)
이들이 활용되는 이유로는 뒤엉킨 변경 냄새 를 해결하기 위해 이런 것으로 함께 변경할 대상들을 모우는 용도로 사용했기 때문에 기능 편애가 일어난 것이다.
데이터와 이를 활용하는 동작은 원래 한 곳에 있어야 맞지만 항상 예외는 있다.
그럴 때는 같은 데이터를 다루는 로직은 한 곳으로 모우는게 좋다.
전략 패턴과 방문자 패턴을 이용하면 오버라이딩 해야하는 특정 동작들을 각각 클래스로 격리시켜주므로 수정하기가 쉬워진다.
데이터 항목들은 어린아이 같은 면이 있다. 서로 어울려 노는 것을 좋아한다.
그래서 데이터끼리 같이 몰려다니는 경우가 많다. 특정 클래스의 필드 끼리 몰려다닐 수 있고 한 메소드에서 파라미터로 같이 몰려다닐 수 있다.
이를 해결하는 방법으로는 보금자리를 만들어 주는 것이다.
클래스의 특정 필드끼리 몰려다닌다면 클래스 추출 하기 기법 (7.5절) 을 통해서 하나의 객체로 묶어줄 수 있고
메소드의 파라미터 끼리 몰려다닌다면 매개변수 객체 만들기 기법 (6.8절) 이나 객체 통째로 넘기기 (11.4절) 기법을 통해서 가능하다.
데이터 뭉치인지 확인하기 위해서는 값 하나를 지워본다고 가정해보자.
그랬을 때 나머지의 값들이 의미가 없다면 이들은 뭉쳐다니는 것이다.
대부분의 프로그래밍 언어에서는 기본형 타입을 지원한다. 정수, 부동소수점 수, 문자열과 같은 다양한 기본형을 제공해준다.
그래서인지 모르겠지만 프로그래머들은 이런 기본형들을 그냥 사용하는걸 선호하지 자신의 문제를 해결하기 위한 클래스로 만들어서 사용하는걸 꺼려한다. (예로 화폐, 전화번호, 좌표 등)
그리고 기본형을 통해 그냥 if 절에서 계산하는 걸 볼 수 있다. 이것 보다는 클래스의 메소드로 정의하는게 훨씬 좋은데.
이 냄새는 특히 문자열 변수에서 특히 심한데 전화번호를 단순 문자열 표현이라고만 생각하는 경우가 많다.
전화번호가 제공해주는 기능이 꽤 있는데도 말이다.
그래서 이런 기본형을 객체로 바꿔주는 작업을 토해서 문명 사회로 이끌어줘야 한다. (갑자기 생각난건데 기본형의 동작이 필요한 순간에 클래스로 감싸는 건 어떨까? 그리고 기본형을 클래스로 감싸면 Validation 작업을 추가로 넣을 수 있다는 이점이 있다.)
또 기본형으로 표현된 코드가 조건절에서 타입을 이용하는 코드로 사용하고 있다면 이는 타입 코드를 서브 클래스로 바꾸기 (12.6절) 기법과 조건부 로직을 다형성으로 바꾸기 (10.4절) 기법을 사용하는것도 가능해진다.
추가로 자주 몰려다니는 기본형 타입이 있다면 이들을 한 클래스로 묶어주는 클래스 추출하기 기법도 필요하다. (7.5절)
옛날에 다형성의 가치를 모르던 시절에는 Switch 문의 남발로 인해 Switch 문을 본면 조건부 로직을 다형성으로 바꾸기 (10.4절) 기법을 사용하는 걸 추천했다.
하지만 요즘에는 다형성을 잘 알고 있으므로 그 정도까지 검토하지는 않겠다. (그러면 Switch 문을 언제 다형성으로 바꿔야 할까?)
Switch 문이 진짜 나쁜 경우는 중복해서 사용하고 있을 경우다.
이 경우에 하나의 케이스가 추가되기만 하면 모든 Switch 문을 찾아서 넣어줘야 하는 작업이 필요해진다.
이렇게 중복이 발생하고 있다면 조건부 로직을 다형성으로 바꿔주자.
반복문은 프로그래밍 언어가 등장할 때부터 함께 한 핵심 프로그래밍 요소다.
예전에는 반복문의 대안이 없었지만 현재는 일급 함수(First-class Function) 을 지원하는 프로그래밍 언어가 많아지면서 반복문을 파이프라인으로 바꾸기 (8.8절) 를 적용해서 시대에 맞지 않는 반복문을 제거하는게 가능해졌다.
Filter 나 Map 같은 파이프라인 연산을 사용하면 원소들이 어떻게 처리되는지 쉽게 파악할 수 있다. (그렇다면 모든 반복문이 파이프라인으로 바꾸는게 좋은가? 예전 클린코드에서 반복문이나 조건문 같은 로직은 딱 한줄로만 처리하는게 가장 알아보기 쉽다고 했다. 그렇게 처리할 수 있는게 아니라면 파이프라인으로 가야하나)
우리는 코드의 구조를 잡을 때 프로그래밍 적 요소를 활용한다. (요소는 여기서 클래스, 메소드, 인터페이스와 같은 걸 말한다.)
이렇게 프로그래밍 적 요소를 활용하면 재활용 할 수 있는 여건과 함께 의미있는 이름을 가질 수 있기 떄문이다.
그치만 이렇게 구조를 잡지 않아도 되는 경우가 있다. (재활용의 여지가 없는 메소드, 이름을 가지지 않아도 충분히 이해할만한 동작들, 죽은 객체인 경우)
이런 경우에 구조를 분해해서 없애버리는 게 좋다. 그 기법으로는 함수를 인라인하기 기법 (6.2절) 이나 클래스 인라인 하기 (7.6절) 로 처리할 수 있다.
또 상속을 사용했다면 계층 합치기 (12.9절) 를 이용하는게 좋다.
추측성 일반화는 나중에 이게 필요할 것이라는 이유로 당장은 필요없는 코드로 인해 풍기는 악취다. (Yagni 원칙과도 관련이 있네.)
이런 이유는 물론 이해하지만 코드 관리가 어렵다면 쓸데없는 낭비일 뿐이다.
당장 걸리적 거리는 코드는 모두 지워버리자.
하는 일이 거의 없는 클래스는 계층 합치기 (12.9절) 로 제거하고 쓸데 없이 위임하는 코드는 함수 인라인 하기 (6.2절) 과 클래스 인라인 하기 (7.6절) 로 삭제하자.
본문에서 사용하지 않는 매개변수는 함수 선언 바꾸기 (6.5절) 로 제거하자.
추측성 일반화는 주로 테스트 코드 말고는 사용하는 경우가 없는데 이런 경우에는 테스트 케이스부터 삭제한 다음 죽은 코드 제거하기 (8.9절) 로 날려버리자.
간혹 클래스 필드에서 특정한 상황에서만 그 필드에 값이 들어가는 경우가 있다.
이런 필드는 대부분의 상황에서는 값이 들어가 있지 않기 때문에 다른 사람들이 그 클래스를 보면 이해하기가 어려워진다.
원래 클래스라는 것 자체가 모든 필드가 다 들어있다는 전제하에 객체를 만든다.
그러므로 이런 임시 필드가 있다면 이런 필드만 따로 뽑는 클래스 추출하기 (7.5절) 기법을 통해서 제 살 곳을 찾아줘야한다.
그 다음 함수 옮기기 (8.1절) 기법으로 임시 필드와 관련 있는 함수들을 모두 클래스로 옮겨주자.
또 기존 클래스에 임시 필드의 여부와 관련해서 동작하는 메소드가 있다면 특이 케이스 추가하기 (10.5절) 기법을 통해서 해결할 수 있다.
메시지 체인은 클라이언트가 한 객체를 통해 다른 객체를 얻은 후 연속적으로 객체를 찾아나가는 과정을 말한다.
가령 getSomething() 과 같은 메소드를 연속적으로 호출해서 객체를 찾아나가는 걸 말한다.
이는 클라이언트가 객체 내비게이션 구조에 종속됐음을 말한다. 중개자의 역할만 하는 객체는 딱히 의미는 없다.
이와 같은 문제는 위임 숨기기 (7.7절) 로 해결하는게 가능하다.
그치만 이 기법을 많이 사용하면 중간 객체가 중재자와 같은 역할을 하게 되므로 결국에 찾는 최종 객체에게 어떠한 행동을 바라는지를 알아보고 그 행동을 함수 추출하기 (6.1절) 로 추출한 후 함수 옮기기 (8.1 절) 로 옮기는 걸 생각해보자.
객체의 대표적인 기능 중 하나로 캡슐화 (encapsulation) 이라는 기능이 있다.
캡슐화를 통해서 객체는 다른 객체에게 작업을 위임하고 구현을 몰라도 된다.
예로 팀장과 미팅을 잡는다고 하면 팀장은 일정을 조율하고 답을 줄 것이다. 근데 일정을 잡는 과정에서 다이어리를 쓰든, 캘린더를 쓰든, 비서를 쓰든 그건 알바가 아니다.
이렇게 캡술화를 사용하면 구현을 몰라도 원하는 바를 얻을 수 있는 장점이 있다.
그치만 이게 지나치면 문제가 되는데 클래스의 메소드의 절반이 다른 객체에게 위임하고 있는 구조라면 그 객체는 Middle Man 이다.
그 객체를 제거하는 중개자 제거하기 (7.8절) 기법을 통해서 객체와 직접적으로 소통하도록 하자. (근데 이런 중재자를 만드는 Mediator 패턴도 있는 걸로 아는데 이 경우에는 왜 쓰는걸까? 디자인 패턴은 리팩토링의 예외를 잡는 부분인 것 같기도 하고.)
소프트웨어 개발자는 모듈 사이에 벽을 두껍게 세우기를 좋아한다.
이 말은 모듈 사이의 필요없는 결합은 줄이고 싶어한다는 말을 의미한다.
만약에 은밀하게 모듈 끼리 데이터를 주고 받는 일이 있다면 필드 옮기기 (8.2절) 기법이나 함수 옮기기 (8.1절) 기법을 사용해서 결합을 줄이는게 좋다.
여러 모듈이 같은 관심사를 공유해서 결합하는 일이 많다면 제 3의 모듈을 만드는 기법이나 위임 숨기기 (7.7절) 기법을 이용해서 다른 모듈이 중간자의 역할을 하도록 한다.
상속 구조에서 부모와 자식 클래스간의 결합이 많아 진다고 하면 헤어져야 한다. 이 경우에는 서브 클래스를 위임으로 바꾸기 (12.10절) 기법이나 부모 클래스를 위임으로 바꾸기 (12.11절) 을 이용하면 된다.
한 클래스가 너무 많은 일을 하려다 보면 필드 수가 늘어난다.
필드 수가 많아지면 필드의 이해가 떨어져서 중복 코드가 발생할 확률이 높아진다.
이럴 떈 클래스 추출하기 (7.5절) 기법으로 관련있는 필드끼리 묶는다.
주로 접두어가 같은 필드끼리 묶어서 클래스로 만든다고 생각하면 된다.
이렇게 클래스로 추출할 일이 있을 수 있고 상속관계로 만드는게 나을 수도 있다. 이 경우에는 슈퍼 클래스 추출하기 (12.8절) 기법을 사용하는 방법도 있고 타입 코드가 있고 이를 서브 클래스로 추출하는 타입 코드를 서브 클래스로 바꾸기 (12.6절) 을 이용할 수 도 있다.
목표는 클래스가 항시 모든 필드를 사용하도록 하자.
또 거대 클래스 중에서 코드량이 많다면 중복 코드가 있을 확률이 높다. 그 클래스안에서 중복 코드를 제거하는 함수 추출하기 (6.1절) 을 이용하자.
클래스의 장점은 언제든 필요에 따라 클래스를 교체할 수 있다는 것이다.
이를 위해서는 클래스의 타입이 인터페이스와 같아야 한다.
따라서 인터페이스에서 선언한 메소드와 같아지도록 함수 선언 바꾸기 (6.5절) 기법을 통해서 메소드 시그니처를 같도록 바꿔야 한다. (애초에 다른 인터페이스의 사용으로 인해 다르니까 하나로 바꾸는 과정을 진행하는 듯.)
이 과정에서 인터페이스와 같아질 때까지 필요한 동작들을 클래스로 옮겨야 한다. 이 기법으로는 함수 옮기기 (8.1) 를 이용하자.
여기서 대안 클래스들 사이에서 중복 코드가 생긴다면 슈퍼 클래스 추출하기 (12.8절) 을 적용할지 고민해보자.
데이터 클래스란 클래스가 필드와 Getter/Setter 만 가지고 있는 클래스다.
이런 클래스는 다른 클래스에서 멋대로 변경하고 사용할 여지가 있으므로 캡슐화 하는게 좋다.
그러므로 public 필드가 있다면 레코드 캡슐화하기 (7.1절) 로 숨기고 변경하면 안되는 데이터가 있다면 Setter 제거하기 (11.7절) 로 제거하자.
다른 클래스에서 데이터 클래스의 Getter/Setter 를 이용해서 동작하는 메소드가 있다면 이를 데이터 클래스로 옮길 수 있는지 알아보자. 함수 옮기기 (8.1절) 로 옮기고 함수의 일부만 그렇다면 함수 추출하기 (6.1절) 로 추출한 후 옮기자.
이렇게 데이터 클래스가 있다는 뜻은 데이터 클래스의 동작들이 다른 곳에 있다는 말 일수도 있다.
예외적으로 데이터 클래스가 있어야 하는 경우도 있는데 이 경우는 단계 쪼개기 (6.11절) 을 통해서 만들어진 중간 결과인 경우다.
이 경우는 이 객체가 수정될 여지가 없으므로 Getter 도 필요없고 바로 필드로 접근해도 좋다.
서브 클래스는 부모로부터 메소드와 데이터를 물려받는다.
그치만 일부만 필요하고 일부는 필요없다면 어떻게 할까?
예전에는 이를 설계를 잘못했기 때문이라고 생각했다.
그래서 메소드 내리기 (12.4절) 기법과 필드 내리기 (12.5절) 기법들을 이용해서 부모에는 진짜 공통적인 부분만 남기려고 했다.
지금은 이 방식을 무조건 권하지는 않는다. 하지만 대게는 공통적인 속성을 남겨놔서 재활용하는 이 기법은 유용하다.
상속을 포기하는 시점은 자식 클래스가 부모 클래스의 동작은 필요하지만 인터페이스를 따를 필요는 없다고 생각되는 시점이다.
이 경우에 서브 클래스를 위임으로 바꾸기 (12.10절) 기법을 이용하거나 슈퍼 클래스를 위임으로 바꾸기 (12.11절) 을 활용해 상속 메커니즘에서 벗어날 수 있다.
주석을 달지 마라고 말하지는 않곘다.
올바른 주석은 향기를 불러일으킨다.
다만 주석을 탈취제의 목적처럼 사용하지는 말자.
주석이 너무 많다는건 악취를 만드는 코드가 많다는 뜻이다.
특정 코드 블록이 하는 일에 주석을 남기고 싶다면 함수 추출하기 (6.1절) 를 적용하고
이미 추출되어 있는 함수에서도 주석을 달려고 한다면 함수 선언 바꾸기 (6.5절) 로 함수 이름을 좀 더 명확히 하는 일이 뭔지 설명할 수 있도록 바꾸자.
시스템이 동작해야 하는 선행 조건을 명시하고 싶다면 Assertion 추가하기 (10.6절) 을 추가하자.
즉 주석을 남겨야겠다면 주석이 필요 없는 코드로 리팩토링을 먼저 해보자.
뭘 할지 모르겠다면, 확실하지 않다면 그런 경우에 대해서 주석을 남겨놓는게 좋다.