chapter3 에서는 리팩터링을 언제 시작해야 하는지에 대해 다룬다.
총 24개의 악취 요소를 소개한다.
코드를 명료하게 표현하는 데 가장 중요한 요소는 바로 '이름'이다. 그래서 함수, 모듈, 변수, 클래스 등은 그 이름만 보고도 무슨일을 하고 어떻게 사용해야 하는지 명확히 알 수 있도록 엄청나게 신경 써서 이름을 지어야 한다 .
이름만 잘 지어도 나중에 문맥을 파악하느라 헤매는 시간을 절약할 수 있다.
만약 마땅한 이름이 떠오르지 않는다면 설계에 더 근본적인 문제가 숨어있을 가능성이 높다. 그래서 혼란스러운 이름을 잘 정리하다 보면 코드가 훨씬 간결해질 때가 많다.
코드가 중복되면 각각을 볼 때 서로 차이점은 없는지 주의 깊게 살펴봐야 하는 부담이 생긴다.
ex. 한 클래스에 딸린 두 메서드가 똑같은 표현식을 사용하는 경우
함수 추출하기 를 통해 양 쪽 모두 추출된 함수를 호출하도록 바꾸면 된다.
ex. 코드가 비슷하긴 한데 완전히 똑같지는 않다면?
문장 슬라이스하기 로 비슷한 부분을 한곳에 모아 함수를 추출한다.
ex. 같은 부모로부터 파생된 서브 클래스에 코드가 중복되어 있다면?
각자 따로 호출되지 않도록 메서드 올리기를 적용해 부모 클래스로 옮겨준다.
간접 호출_indirection 의 효과, 즉 코드를 이해하고, 공유하고, 선택하기 쉬워진다는 장점은 함수를 짧게 구성할 때 나오는 것이다.
예전 언어는 서브루틴을 호출하는 비용이 컸기 때문에 짧은 함수를 꺼렸다. 하지만 요즘 언어는 프로세스 안에서의 함수 호출 비용을 거의 없애버렸다.
주석을 달만한 부분은 무조건 함수로 만든다. 함수 본분에 원래 주석으로 설명하려던 부분에 코드가 담기고, 함수 이름은 동작 방식이 아닌 '의도'가 드러나게 짓는다. 원래 코드가 길어지더라도 함수로 뽑는다. ( 단, 함수 이름에 코드의 목적을 드러내야 한다. )
함수가 매개변수와 임시 변수를 많이 사용한다면 함수 쪼개기 작업에 방해가 된다. 이런 경우 추출된 함수에도 매개변수가 너무 많아지게 되므로 임시 변수를 질의 함수로 바꾸기 , 매개변수 객체 만들기, 매개변수 통째로 넘기기 를 통해 매개변수의 수를 줄일 수 있다.
처음 프로그래밍을 시작하면 전역 데이터를 줄이기 위해 함수에 필요한 것들은 모조리 매개변수로 전달하라고 배운다. 하지만 매개변수 목록이 길어지면 그 자체로 이해하기 어려울 때가 많다.
"전역 데이터는 우리가 겪을 수 있는 악취 중 가장 지독한 축에 속한다" 라고 표현할 만큼 전역 데이터는 주의해야한다.
전역 데이터의 형태 : 전역 변수, 클래스 변수, 싱글톤
접근자 함수들을 클래스나 모듈에 집어넣고 그 안에서만 사용할 수 있도록 접근 범위를 최소로 줄이는 것도 좋다.
변경할 데이터가 다른 곳에서는 다른 값을 기대한다는 사실을 인지하지 못하고 수정해버리면 골치 아픈 버그로 이어지는 경우가 있다. 이러한 경우 원인을 알아내기가 매우 어렵다.
( 이런 이유로 함수형 프로그래밍에서는 데이터는 절대 변하지 않고, 데이터를 변경하려면 반드시 원래 데이터는 그대로 둔 채 변경하려는 값에 해당 복사본을 만들어서 반환한다는 개념을 기본으로 삼는다. )
javascript 에서의 var
, const
의 차이
뒤엉킨 변경은 단일 책임 원칙_Single Responsibilty Principle(SRP) 가 제대로 지켜지지 않을 때 나타난다. 즉, 하나의 모듈이 서로 다른 이유들로 인해 여러 가지 방식으로 변경되는 일이 많을 때 발생한다.
개발 초기에는 맥락 사이의 경계를 명확히 나누기 어렵고 소프트웨어 시스템의 기능이 변경되면서 이 경계도 끊임없이 움직이기 때문에 맥락에 따른 분리를 항상 염두해둬야 한다.
산탄총 수술은 뒤엉킨 변경과 비슷하면서도 정반대다.
이 문제는 코드를 변경할 때마다 자잘하게 수정해야 하는 클래스가 많을 때 생긴다. 변경할 부분이 코드 전반에 퍼져 있다면 찾기도 어렵고 꼭 수정해야 할 곳을 지나치기 쉽다.
`
뒤엉킨 변경 | 산탄총 수술 | |
---|---|---|
원인 | 맥락을 잘 구분하지 못함 | |
해법 | 맥락을 명확히 구분 | |
발생 과정 | 한 코드에 섞여 들어감 | 여러 코드에 흩뿌려짐 |
해법_행동 | 맥락별로 분리 | 맥락별로 모음 |
프로그램을 모듈화할 때는 코드를 여러 영역으로 나눈 뒤 영역 안에서 이뤄지는 상호작용은 최대한 늘리고 영역 사이에서 이뤄지는 상호작용은 최소로 줄이는 데 주력한다. 기능 편애는 흔히 어떤 함수가 자기가 속한 모듈의 함수나 데이터보다 다른 모듈의 함수나 데이터와 상호작용할 일이 더 많을 때 생기는 문제이다.
함수를 추출하거나 기존 함수를 더 적합한 모듈로 옮기면 쉽게 해결이된다. 하지만 복잡한 패턴의 경우 '디자인 패턴'중 '전략 패턴', '방문자 패턴'이 있다. 가장 기본이 되는 원칙은 '함께 변경할 대상을 한데 모으는 것'이다.
함께 몰려다니는 데이터 뭉치는 보금자리를 따로 마련해줘야 마땅하다
필드 형태의 데이터 뭉치 -> 클래스 추출하기
메서드 시그니처에 있는 데이터 뭉치 -> 매개변수 객체 만들기/객체 통째로 넘기기
데이터 뭉치 판별 법
값 하나를 삭제했을때 나머지 데이터만으로는 의미가 없다면? 데이터 뭉치이다.
프로그래밍 언어는 문자열, 정수 등 다양한 기본 type을 제공한다. 이러한 '기본형'에 집착하며 자신에게 주어진 문제에 딱 맞는 타입(화폐, 좌표, 구간 등)을 직접 정의하기 꺼리는 사람들이 많다. ( 금액을 그냥 숫자형으로 계산하거나, 물리량을 계산할 때도 mm, inch와 같은 단위를 무시하고 )
중복된 switch문의 문제는 조건문을 추가할 때마다 다른 switch문들도 모두 찾아서 함께 수정해야 하기 때문이다.
지금은 일급함수_First-class Function 를 지원하는 언어가 많어졌기 때문에 반복문을 파이프라인으로 바꾸기를 적용할 수 있다. filter
, map
과 같은 파이프라인 연산을 사용하면 코드에서 각 원소들이 어떻게 처리되는지 쉽게 파악할 수 있다.
함수, 클래스, 인터페이스 등 코드 구조를 잡는 데 활용되는 요소들은 구조를 변형하거나 재활용하기 위해 잘 사용되어진다. 하지만 실질적으로 메서드를 하나만 가지고 있는 클래스와 같이 나중을 위해 분리해놨지만 사용하지 못하는 결과들이 있다. 또는 원래 풍성했던 클래스가 리팩터링을 거치면서 역할이 줄어들 수 있는데 이럴 경우 프로그램 요소는 삭제해주는 것이 좋다.
'나중에 필요할 거야'라는 생각으로 당장은 필요 없는 모든 종류의 후킹 포인트와 특이 케이스 처리 로직을 미리 작성해둔 코드에서 추측성 일반화의 냄새가 풍긴다
추측성 일반화는 결과를 이해하거나 관리하기 어려워진 코드이다. 미래를 대비해 작성한 대부분은 실제로 사용하게 되면 다행이지만 그렇지 않다면 쓸데없는 낭비일 뿐이다.
특정 상황에서만 값이 설정되는 필드를 가진 클래스가 있을 수 있다. 그런데 객체를 가져올 때는 당연히 모든 필드가 채워져 있으리라 기대하는 게 보통이라 임시 필드를 갖도록 작성하면 코드를 이해하기 어렵다.
이러한 필드들은 클래스로 추출하여 임시 필드와 관련된 코드를 모조리 새 클래스에 몰아넣어 준다.
getSomething()과 같은 getter가 꼬리에 꼬리를 물고 이어지거나 임시 변수들이 줄줄이 나열되는 코드가 있는데 이는 클라이언트가 객체 내비게이션 구조에 종속됐음을 의미한다.
(127p 참고)
객체의 대표적인 기능 중 하나로 외부로부터 세부사항을 숨겨주는 캡슐화가 있다. 캡슐화는 그 과정에서 위임이 자주 활용된다.
클래스가 제공하는 메서드 중 절반이 다른 클래스에 구현을 위임하고 있다면 중개자 제거하기를 활용하여 실제로 일을 하는 객체와 직접 소통하게 해야한다. 위임 메서드를 정리한 후 남는 일이 거의 없다면? 호출하는 쪽으로 인라인 하자.
모듈 사이의 데이터 거래가 많으면 '결합도'가 높아지는데, 일이 돌아가게 하려면 거래가 필요하지만 그 양을 최소로 줄이고 모두 투명하게 처리해야 한다.
한 클래스가 너무 많은 일을 하려다 보면 필드 수가 상당히 늘어난다. 그리고 클래스에 필드가 너무 많으면 중복 코드가 생기기 쉽다.
클래스를 사용할 때의 가장 큰 장점은 필요에 따라 언제든 다른 클래스로 교체할 수 있다느 것이다. 단, 교체하려면 인터페이스가 같아야한다.