리팩토링2 스터디 - 003
3장 코드에서 나는 악취
리팩터링의 작동 원리를 아는 것 만큼 리팩터링 시점을 아는 것은 중요하다
이 책에서 리팩터링을 언제 해야하는지에 대한 명확한 기준을 제시하지는 않지만 문제의 징후를 제시해보겠다
기이한 이름 Mysterious Name
코드 이름은 단순하고 명료하게 작성되어야 하고, 무슨 일을 하고 어떻게 사용하는지 신경 써서 이름을 지어야 한다.
우리가 가장 많이 사용하는 리팩터링이 바로
- ‘함수 선언 바꾸기’
- ‘변수 이름 바꾸기’
- ‘필드 이름 바꾸기’
처럼 이름을 바꾸는 리팩터링이다.
이름만 잘 지어도 나중에 문맥을 파악하느라 헤매는 시간을 절약할 수 있다.
마땅한 이름이 떠오르지 않는다면 설계에 더 근본적인 문제가 숨어 있을 가능성이 높다.
중복 코드 Duplicated Code
코드가 중복되면 각각을 볼 때마다 서로 차이점은 없는지 주의 깊게 살펴봐야 한다.
그 중 하나를 수정할 때는 다른 비슷한 코드들도 모두 살펴보고 수정해야하는 문제가 생긴다.
- 두 메서드가 똑같은 표현식을 사용하면 ‘함수 추출하기’
- 코드가 비슷한데 똑같지 않으면 ‘문장 슬라이스 하기’
- 서브 클래스들에 코드가 중복되어 있다면 ‘메서드 올리기’
를 적용해 본다.
긴 함수 Long Function
오랜 기간 잘 활용되는 프로그램들은 하나같이 짧은 함수로 구성되어 있다.
짧은 함수는 코드를 이해하고, 공유하고, 선택하기 쉬워진다.
함수가 길수록 이해하기 어렵다.
물론 짧은 함수는 코드를 읽는 사람 입장에서는 함수가 하는 일을 파악하기 위해 왔다 갔다 해야 하므로 여전히 부담스러울 수 있다.
짧은 함수로 구성된 코드를 이해하기 쉽게 만드는 방법은 좋은 이름이다.
좋은 이름을 가진 함수는 본문 코드를 볼 이유가 사라진다.
함수 이름은 동작 방식이 아닌 ‘의도’가 드러나게 짓는다.
- 함수 본문에서 따로 묶어 빼내면 좋을 코드 덩어리를 찾아 새로운 함수로 만드는 ‘함수 추출하기’
- 임시 변수의 수를 줄이기 위한 ‘임시 변수를 질의 함수로 바꾸기’, ‘매개변수 객체 만들기’, ‘객체 통째로 넘기기
- 임시 변수와 매개변수를 줄이기 위한 큰 방법으로 ‘함수를 명령으로 바꾸기’
- 조건문은 ‘조건문 분해하기’
- 같은 조건을 기준으로 나뉘는 swtich 문은 ‘조건부 로직을 다형성으로 바꾸기’
- 반복문은 ‘반복문 쪼개기’
를 이용하여 긴 함수를 줄이도록 한다.
긴 매개변수 목록 Long Parameter List
암적 존재인 전역 변수가 늘어나는 사태를 막기 위해 함수에 필요한 것들을 모조리 매개변수로 전달하라곤 했다.
- 매개변수에서 값을 얻어올 수 있는 매개변수가 있을 수 있는데, 이런 경우 ‘매개변수를 질의 함수로 바꾸기’로 제거한다.
- 사용 중인 데이터 구조에서 값들을 뽑아 매개변수를 전달하는 코드라면 ‘객체 통째로 넘기기’로 원본 데이터 구조를 그대로 전달
- 항상 전달되는 매개변수들은 ‘매개변수 객체 만들기’로 묶어주기
- 함수의 동작 방식을 정하는 플래그 역할의 매개변수는 ‘플래그 인수 제거하기’로 없애기
- 여러 함수에서 사용하는 값들을 클래스의 필드로 정의하는 ‘여러 함수를 클래스로 묶기’
전역 데이터 Global Data
전역 데이터(전역 변수)는 우리가 겪을 수 있는 악취 중에 가장 지독한 축에 속한다.
전역 변수의 문제는 코드 어디에서든 건드릴 수 있고 값을 누가 바꿨는지 찾아낼 메커니즘이 없다는 것이다.
- 다른 코드에서 오염시킬 가능성이 있는 데이터를 함수로 감싸는 ‘변수 캡슐화하기’
전역 변수가 조금뿐이라면 감당할 수 있겠지만, 많아지만 걷잡을 수 없게 된다.
그래야 소프트웨어가 진화하는 데 따른 변화에 대처할 수 있다.
가변 데이터 Mutable Data
데이터를 변경했더니 예상치 못한 결과가 골치 아픈 버그로 이어지는 경우가 종종 있다.
아주 드문 조건에서만 발생한다면 원일 알아내기가 매우 어렵다.
- 정해놓은 함수를 거쳐야만 값을 수정할 수 있는 ‘변수 캡슐화하기’
- 하나의 변수에 용도가 다른 값들을 저장하느라 값을 갱신한다면 ‘변수 쪼개기’
- 갱신 로직을 다른 코드와 떨어뜨려 놓기 위한 ‘문장 슬라이드하기’, ‘함수 추출하기’
- API 를 만들 때는 부작용이 있는 코드를 호출 할 수 없도록 ‘질의 함수와 변경 함수 분리하기’
- 세터를 호출하는 클라이언트를 찾는 수고를 더는 ‘세터 제거하기’
- ‘파생 변수를 질의 함수로 바꾸기’
- 변수를 갱신하는 코드들의 유효범위를 제한하는 ‘여러 함수를 클래스로 묶기’, ‘여러 함수를 변환 함수로 묶기’
- 내부 필드에 데이터를 담고 있는 변수라면 ‘참조를 값으로 바꾸기’로 구조체를 통째로 교체
뒤엉킨 변경 Divergent Change
소프트웨어의 구조는 변경하기 쉬운 형태로 조직해야 한다.
뒤엉킨 변경은 단일 책임 원칙 Sing Responsibility Principle 이 지켜지지 않을 때 나타난다.
하나의 모듈이 서로 다른 이유들로 인해 변경되는 일이 많을 때 발생한다.
- 순차적으로 실행되는게 자연스러운 맥락이라면 ‘단계 쪼개기’
- 전체 처리 과정 중에 각기 다른 맥락의 함수를 호출한다면 ‘함수 옮기기’
- 여러 맥락의 일에 관여하는 함수나 클래스가 있다면 ‘함수 추출하기’, ‘클래스 추출하기’
산탄총 수술 Shotgun Surgery
코드를 수정할 때마다 자잘하게 여러 군데를 수정해야 한다.
- 한 모듈로 변경 대상을 이동시키는 ‘함수 옮기기’, ‘필드 옮기기’
- 비슷한 데이터를 다루는 함수가 많다면 ‘여러 함수를 클래스로 묶기’
- 데이터 구조를 변환하거나 보강하는 함수들에는 ‘여러 함수를 변환 함수로 묶기’
- 묶은 함수들의 출력 결과를 단계별로 분리할 수 있다면 ‘단계 쪼개기’
- 어설프게 분리된 로직이 있다면 인라인 리팩터링으로 하나로 합치는 ‘함수 인라인하기’, ‘클래스 인라인하기’
기능 편애 Feature Envy
프로그램을 모듈화할 때는 코드를 여러 영역으로 나눈 뒤 영역 안의 상호작용은 늘리고, 영역 간의 상호작용은 줄이기에 주력한다.
기능 편애는 자기가 속한 모듈의 함수/데이터보다 다른 모듈의 함수/데이터를 더 많이 상호작용할 때 발생한다.
- 함수를 상호작용을 많이 하는 모듈로 옮기는 ‘함수 옮기기’
- 함수의 일부만 상호작용이 많다면 ‘함수 추출하기’ 후 ‘함수 옮기기’
- 함수가 이동할 곳이 마땅하지 않을 경우에는 ‘함수 추출하기’
가장 기본이 되는 원칙은 ‘함께 변경할 대상을 한데 모으는 것’ 이다
데이터 뭉치 Data Clumps
- 데이터 뭉치를 하나의 객체로 묶어주는 ‘클래스 추출하기’
- 매개변수의 수를 줄일 수 있는 ‘매개변수 객체 만들기’, ‘객체 통째로 넘기기’
데이터 뭉치 중 일부만 사용하더라도 객체로 만드는 편이 훨씬 가독성이 증가한다.
데이터 뭉치를 판단하는 방법은 값을 하나 삭제했을 때 나머지 데이터가 무의미해지면 데이터 뭉치
상당한 중복을 없애고 레코드 구조를 클래스로 바꿀 수 있다면 개발 생산성에 기여하게 될 것이다.
기본형 집착 Primitive Obsession
자신에게 주어진 문제에 딱 맞는 기초 타입을 직접 정의하기 꺼려하는 사람이 많다.
금액을 그냥 숫자형으로 계산하거나, 물리량을 계산할 때 단위를 무시하기도 한다.
전화번호를 단순히 문자 집합으로만 표현하기엔 아쉬움이 많다.
- 기본형을 자료형으로 바꿀 수 있는 ‘기본형을 객체로 바꾸기’
- 기본형으로 표현된 코드가 조건부 동작을 제어하는 타입 코드로 쓰였다면 ‘타입 코드를 서브클래스로 바꾸기’, ‘조건부 로직을 다형성으로 바꾸기’
반복되는 switch 문 Repeated Switches
- switch 문은 모조리 ‘조건부 로직을 다형성으로 바꾸기’
다형성이 널리 자리 잡아서 단순히 switch 문을 썼다고 해서 자동으로 검토 대상이 되지는 않는다.
중복된 switch 문은 문제가 되는 이유는 조건절을 하나 추가할 때마다 다른 switch 문들도 모두 찾아서 함께 수정해야 하기 때문이다.
반복문 loops
반복문은 프로그래밍의 핵심요소이지만 지금은 시대에 걸맞지 않는다.
일급 함수를 통해 반복문을 제거할 수 있다.
- ‘반복문을 파이프라인으로 바꾸기’
필터나 맵같은 파이프라인 연산을 사용하면 코드에서 각 원소들이 어떻게 처리되는지 파악할 수 있다.
성의 없는 요소 Lazy Element
우리는 코드의 구조를 잡을 때 클래스, 함수, 인터페이스 등으로 구조를 잡는다.
하지만 리팩토링 등으로 무의미한 클래스, 함수가 생길 수 있다.
- ‘함수 인라인하기’, ‘클래스 인라인하기’, ‘계층 합치기’
추측성 일반화 Speculative Generality
나중에 필요할 것 같다는 생각으로 모든 종류의 후킹 포인트나 특이한 케이스 처리 로직을 작성한 바람에 이해하거나 관리하기 어려워진 코드가 있다.
당장 걸리적거리는 코드는 눈앞에서 치워버려야 한다.
- 하는 일이 없는 추상 클래스는 ‘계층 합치기’
- 쓸데없이 위임하는 클래스는 ‘함수 인라인하기’, ‘클래스 인라인하기’
- 본문에서 사용하지 않는 매개변수는 ‘함수 선언 바꾸기’
- 테스트 코드에서만 사용하는 함수나 클래스는 ‘죽은 코드 제거하기’
임시 필드 Temporary Field
특정 상황에서만 값이 설정되는 필드가 있을 수 있다.
하지만 객체를 가져올 때는 당연히 모든 필드가 채워져 있으리라 기대한다.
임시 필드를 갖도록 작성하면 코드를 이해하기 어렵다.
- 덩그러니 떨어져 있는 필드는 ‘클래스 추출하기’
- 임시 필드와 관련된 코드는 ‘함수 옮기기’
- 필드가 유효하지 않을 때를 위한 대안 클래스를 만드는 ‘특이 케이스 추가하기’
메시지 체인 Message Chains
메시지 체인이란 클라이언트가 한 객체를 통해 다른 객체를 얻은 뒤 또 다른 객체를 요청하는 식을 말한다.
이는 클라이언트가 객체 내비게이션 구조에 종속되었음을 의미한다.
- 메시지 체인의 다양한 연결점에 적용할 수 있는 ‘위임 숨기기’
- 최종 결과 객체를 사용하는 코드 일부를 따로 빼내는 ‘함수 추출하기’, ‘함수 옮기기’
// 메시지 체인 예시
managerName = aPerson.department.manager.name;
// 위임 숨기기 예시
managerName = aPerson.managerName;
// 함수 추출하기 예시
managerName = getManagerName(aPerson);
function getManagerName(aPerson) {
return aPerson.department.manager.name;
}
중개자 Middle Man
객체의 대표적인 기능인 캡슐화와 그에 활용되는 위임이 있다.
- 클래스가 제공하는 메서드 중 절반이 다른 클래스에 구현을 위임하고 있다면 ‘중개자 제거하기’
- 위임 메서드를 제거한 후 호출하는 쪽으로 인라인 하는 ‘함수 인라인하기’
내부자 거래 Insider Trading
모듈 가의 데이터 거래가 많으면 결합도가 높아진다고 한다.
- 은밀히 데이터를 주고 받는 모듈들이 있다면 ‘함수 옮기기’, ‘필드 옮기기’
- 여러 모듈이 같은 관심사를 공유한다면 공통 부분을 제 3의 모듈로 새로 만드는 ‘위임 숨기기’
- 상속 구조에서 자식 클래스가 부모 클래스에 많이 의존한다면 ‘서브클래스를 위임으로 바꾸기’, ‘슈퍼클래스를 위임으로 바꾸기’
거대한 클래스 Large Class
한 클래스가 너무 많은 일을 하면 필드 수가 상당히 늘어나고, 중복 코드가 발생하기 쉽다.
- 같은 컴포넌트에 모아 두는 것이 합당한 필드들을 따로 묶는 ‘클래스 추출하기’
- 분리된 컴포넌트가 원래 클래스와 상속 관계로 판단되면 ‘슈퍼클래스 추출하기’, ‘타입 코드를 서브클래스로 바꾸기’
필드가 많은 클래스와 마찬가지로 코드량이 많은 클래스도 혼동을 일으킬 여지가 크다.
클래스 안에서 기능 그룹을 분석하여 자체적으로 중복을 제거해야 한다.
- 각각의 기능 그룹을 ‘클래스 추출하기’, ‘슈퍼클래스 추출하기’, ‘타입 코드를 서브클래스로 바꾸기’
서로 다른 인터페이스의 대안 클래스들 Alternative Classes with Different Interfaces
언제든 다른 클래스로 교체할 수 있는 클래스의 장점은 인터페이스가 같을 때 가능하다.
- 메서드 시그니처를 교환하려면 ‘함수 선언 바꾸기’
- 인터페이스가 같아질 때까지 함수를 옮기는 ‘함수 옮기기’
- 클래스들 사이에 중복 코드가 생기면 ‘슈퍼클래스 추출하기’
데이터 클래스 Data Class
데이터 클래스는 데이터 필드와 게터/세터 메서드로만 구성된 클래스이다.
이런 데이터 클래스는 다른 클래스에 깊이 관여될 수 있다.
- 필드를 ‘레코드 캡슐화하기’
- 변경되면 안되는 필드는 ‘세터 제거하기’
- 다른 클래스의 메서드가 데이터 클래스의 게터/세터를 사용한다면 해당 메서드를 데이터 클래스로 ‘함수 옮기기’
- 메서드의 일부에서만 게터/세터를 사용한다면 ‘함수 추출하기’
다른 함수를 호출해 얻은 결과 레코드는 함수를 넣을 이유가 없다.
이런 결과 레코드는 게터를 통할 필요없이 필드 자체를 공개하면 된다.
상속 포기 Refused Bequest
서브클래스에서 부모의 데이터와 메서드를 사용하지 않을 때가 있다.
예전에는 새로운 서브클래스를 만들어서 물려받지 않을 부모 코드를 넘겼다.
혹은 부모 클래스를 추상 클래스로 선언했다.
이제는 실무 관점에서 유용하기 때문에 상속을 활용하는 편이다
서브클래스가 부모의 동작은 필요로 하지만 인터페이스를 따르고 싶지 않을 때가 있다.
이것은 상속 메커니즘이 아니다.
- 상속 메커니즘을 벗어나기 위한 ‘서브클래스를 위임으로 바꾸기’, ‘슈퍼클래스를 위임으로 바꾸기’
주석 Comments
주석이 장황한 이유는 코드가 잘못 작성되었을 경우가 많다.
- 특정 블럭의 일을 주석으로 남기고 싶다면 ‘함수 추출하기’
- 이미 추출된 함수에 주석을 남기고 싶다면 ‘함수 선언 바꾸기’
- 시스템이 동작하기 위한 선행조건을 주석으로 남기고 싶다면 ‘어서션 추가하기’
주석을 남겨야겠다는 생각이 들면, 가장 먼저 주석이 필요 없는 코드로 리팩터링해본다.