Chapter 03 - 코드에서 나는 악취
인상깊은 문장, 코드들
리팩터링을 언제 시작하고 언제 그만할지를 판단하는 일은 리팩터링의 작동 원리를 아는 것 못지않게 중요하다.
인스턴스 변수를 삭제하거나 상속 계층을 만드는 방법을 설명하기는 쉽다. 이런 일들은 간단한 문제에 속한다. 하지만 이런 일들을 '언제' 해야 하는지에 대해서는 명확하게 정립된 규칙이 없다.
리팩터링을 언제 멈춰야 하는지를 판단하는 정확한 기준을 제시하지는 않을 것이다. 경험에 따르면 숙련된 사람의 직관만큼 정확한 기준은 없다. 종료 기준보다는 리팩터링하면 해결할 수 있는 문제의 징후를 제시하겠다.
3.1 기이한 이름
- 추리 소설이라면 무슨 일이 전개되는지 궁금증을 자아낼수록 좋지만, 코드는 아니다. 세계적인 기인이라는 느낌을 풍기고 싶더라도 꾹 참고 코드는 단순하고 명료하게 작성해야 한다.
- 코드를 명료하게 표현하는 데 가장 중요한 요소 하나는 바로 '이름'이다. 그래서 함수, 모듈, 변수, 클래스 등은 그 이름만 보고도 각각이 무슨 일을 하고 어떻게 사용해야 하는지 명확히 알 수 있도록 엄청나게 신경 써서 이름을 지어야 한다.
- 하지만 아쉽게도 이름 짓기는 프로그래밍에서 가장 어렵기로 손꼽히는 두 가지중 하나다. (다른 하나는 캐시 무효화다.)
- 이름 바꾸기는 단순히 이름을 다르게 표현하는 연습이 아니다. 마땅한 이름이 떠오르지 않는다면 설계에 더 근본적인 문제가 숨어 있을 가능성이 높다. 그래서 혼란스러운 이름을 잘 정리하다 보면 코드가 훨씬 간결해질 때가 많다.
3.2 중복 코드
- 똑같은 코드 구조가 여러 곳에서 반복된다면 하나로 통합하여 더 나은 프로그램을 만들 수 있다. 코드가 중복되면 각각을 볼 때마다 서로 차이점은 없는지 주의 깊에 살펴봐야 하는 부담이 생긴다. 그중 하나를 변경할 때는 다른 비슷한 코드들도 모두 살펴보고 적절히 수정해야 한다.
3.3 긴 함수
- 짧은 함수들로 구성된 코드베이스를 얼핏 훑으면 연산하는 부분이 하나도 없어 보인다. 코드가 끝없이 위임하는 방식으로 작성되어 있기 때문이다. 하지만 이런 프로그램을 수년 동안 다루다보면 이 짧은 함수들이 얼마나 중요한지 깨닫게 된다. 간접 호출의 효과, 즉 코드를 이해하고, 공유하고, 선택하기 쉬워진다는 장점은 함수를 짧게 구성할 떄 나오는 것이다.
- 예전 언어는 서브루틴을 호출하는 비용이 컸기 때문에 짧은 함수를 꺼렸다. 하지만 요즘 언어는 프로세스 안에서의 함수 호출 비용을 거의 없애버렸다. 물론 코드를 읽는 사람 입장에서는 함수가 하는 일을 파악하기 위해 왔다 갔다 해야 하므로 여전히 부담이 된다. 다행히 함수 호출부와 선언부 사이를 빠르게 이동하거나 호출과 선언을 동시에 보여주는 개발 환경을 활용하면 이 부담이 줄어들지만, 짧은 함수로 구성된 코드를 이해하기 쉽게 만드는 가장 확실한 방법은 좋은 이름이다.
- 주석을 달아야 할 만한 부분은 무조건 함수로 만든다. 본문에는 원래 주석으로 설명하려던 코드가 담기고, 함수 이름은 동작 방식이 아닌 '의도'가 드러나게 짓는다.
- 함수를 짧게 만드는 작업의 99%는 함수 추출하기가 차지한다.
- 조건문이나 반복문도 추출 대상의 실마리를 제공한다.
- 반복문도 그 안의 코드와 함께 추출해서 독립된 함수로 만든다. 추출한 반복문 코드에 적합한 이름이 떠오르지 않는다면 성격이 다른 두 가지 작업이 섞여 있기 때문일 수 있다. 이럴 때는 과감히 반복문 쪼개기를 적용해서 작업을 분리한다.
3.4 긴 매개변수 목록
- 종종 다른 매개변수에서 값을 얻어올 수 있는 매개변수가 있을 수 있는데, 이런 매개변수는 매개변수를 질의 함수로 바꾸기로 제거할 수 있다.
- 사용 중인 데이터 구조에서 값들을 뽑아 각각을 별개의 매개변수로 전달하는 코드라면 객체 통째로 넘기기를 적용해서 원본 데이터 구조를 그대로 전달한다.
- 함수의 동작 방식을 정하는 플래그 역할의 매개변수는 플래그 인수 제거하기로 없애준다.
3.5 전역 데이터
- 전역 데이터를 주의해야 한다는 말은 우리가 소프트웨어 개발을 시작한 초창기부터 귀가 따갑게 들었다. 심지어 전역 데이터는 이를 함부로 사용한 프로그래머들에게 벌을 주는 지옥 4층에 사는 악마들이 만들었다는 말이 돌 정도였다.
- 이를 방지하기 위해 우리가 사용하는 대표적인 리팩터링은 변수 캡슐화하기다. 다른 코드에서 오염시킬 가능성이 있는 데이터를 발견할 때마다 이 기법을 가장 먼저 적용한다. 이런 데이터를 함수로 감싸는 것만으로도 데이터를 수정하는 부분을 쉽게 찾을 수 있고 접근을 통제할 수 있게 된다.
3.6 가변 데이터
- 무분별한 데이터 수정에 따른 위험을 줄이는 방법은 얼마든지 있다. (변수 캡슐화하기)
- 값을 다른 곳에서 설정할 수 있는 가변 데이터가 풍기는 악취는 특히 고약하다. 혼동과 버그와 야근을 부를 뿐만 아니라, 쓸데없는 코드이기도 하다. 이럴 때는 파생 변수를 질의 함수로 바꾸기에 식초 농축액을 섞어서 코드 전체에 골고루 뿌려준다.
3.7 뒤엉킨 변경
- 코드를 수정할 때는 시스템에서 고쳐야 할 딱 한 군데를 찾아서 그 부분만 수정할 수 있기를 바란다. 이렇게 할 수 없다면 (서로 밀접한 악취인) 뒤엉킨 변경과 산탄총 수술 중 하나가 풍긴다.
- 뒤엉킨 변경은 단일 책임 원칙(SRP)이 제대로 지켜지지 않을 때 나타난다. 예컨대 지원해야 할 데이터베이스가 추가될 때마다 함수 세 개를 바꿔야 하고, 금융 상품이 추가될 때마다 또 다른 함수 네 개를 바꿔야 하는 모듈이 있다면 뒤엉킨 변경이 발생했다는 뜻이다.
- 물론 데이터베이스와 금융 상품 여러 개를 추가하고 나서야 이 악취가 느껴지는 경우도 많다. 개발 초기에는 맥락 사이의 경계를 명확히 나누기가 어렵고 소프트웨어 시스템의 기능이 변경되면서 이 경계도 끊임없이 움직이기 때문이다.
3.8 산탄총 수술
- 산탄총 수술은 뒤엉킨 변경과 비슷하면서도 정반대다.
뒤엉킨 변경 | 산탄총 수술 | |
원인 | 맥락을 잘 구분하지 못함 | |
해법(원리) | 맥락을 명확히 구분 | |
발생 과정(현상) | 한 코드에 섞여 들어감 | 여러 코드에 흩뿌려짐 |
해법(실제 행동) | 맥락별로 분리 | 맥락별로 모음 |
- 이 냄새는 코드를 변경할 때마다 자잘하게 수정해야 하는 클래스가 많을 때 풍긴다. 변경할 부분이 코드 전반에 퍼져 있다면 찾기도 어렵고 꼭 수정해야 할 곳을 지나치기 쉽다.
- 이럴 때는 함께 변경되는 대상들을 함수 옮기기와 필드 옮기기로 모두 한 모듈에 묶어두면 좋다.
3.9 기능 편애
- 기능 편애는 흔히 어떤 함수가 자기가 속한 모듈의 함수나 데이터보다 다른 모듈의 함수나 데이터와 상호작용할 일이 더 많을 때 풍기는 냄새다.
- 실행 과정에서 외부 객체의 게터 메서드 대여섯 개를 호출하도록 작성된 함수를 수없이 봤다. 다행히 해결하기는 쉽다. 이 함수가 데이터와 가까이 있고 싶어 한다는 의중이 뚜렷이 드러나므로 소원대로 데이터 근처로 옮겨주면 된다.
3.11 기본형 집착
- 자신에게 주어진 문제에 딱 맞는 기초 타입(화폐, 좌표, 구간 등)을 직접 정의하기를 몹시 꺼리는 사람이 많다. 그래서 금액을 그냥 숫자형으로 계산하거나, 물리량을 계산할 때도 밀리미터나 인치 같은 단위를 무시하고, 범위도
if (a < upper && a > lower)
처럼 처리하는 코드를 수없이 봤다. - 기본형을 객체로 바꾸기를 적용하면 기본형만이 거주하는 구석기 동굴을 의미 있는 자료형들이 사는 최신 온돌식 코드로 탈바꿈시킬 수 있다.
느낀 점
- 앞선 리팩토링 방법 내용을 읽으면서 궁금했던 '언제 리팩토링을 해야할까'의 상세한 지점들이 있어 이번 장을 읽으면서 그 부분들을 해소할 수 있었다. '언제'와 '어떻게'가 모두 중요할 것이기 때문에 이번 장 내용을 잘 기억해두어야겠다.