Chapter 3

코드에서 나는 악취

  • 3.1 기이한 이름
  • 3.2 중복 코드
  • 3.3 긴 함수
  • 3.4 긴 매개변수 목록
  • 3.5 전역 데이터
  • 3.6 가변 데이터
  • 3.7 뒤엉킨 변경
  • 3.8 산탄총 수술
  • 3.9 기능 편애
  • 3.10 데이터 뭉치
  • 3.11 기본형 집착
  • 3.12 반복되는 switch문
  • 3.13 반복문
  • 3.14 성의 없는 요소
  • 3.15 추측성 일반화
  • 3.16 임시 필드
  • 3.17 메시지 체인
  • 3.18 중개자
  • 3.19 내부자 거래
  • 3.20 거대한 클래스
  • 3.21 서로 다른 인터페이스의 대안 클래스들
  • 3.22 데이터 클래스
  • 3.23 상속 포기
  • 3.24 주석

리팩터링을 "언제" 해야 하는지, 정량적으로 딱 잘라 말할 수 있을까? 불가능해. 그래서 켄트 벡과 파울러가 쓰는 비유가 "냄새"야. 뭔가 꺼림칙한 느낌이 들면 들여다보라는 거지. 3장은 24가지 코드 악취를 정리한 카탈로그야. 리팩터링의 실전 가이드라고 보면 돼.

가장 흔하면서도 가장 중요한 악취는 기이한 이름이야. "이름만 잘 지어도 코드가 읽기 쉬워진다." 함수 이름을 보고 뭘 하는지 모르겠다면? 변수 이름이 의미를 전달하지 못한다면? 그게 악취야. 파울러는 이름 짓기가 프로그래밍에서 가장 어려운 일 중 하나라고 인정해. 하지만 그래서 더 중요하지. "이름이 적절하지 않은 건 설계가 잘못되었다는 신호일 수 있다." 이름을 잘 못 짓겠으면, 그 코드가 하는 일 자체가 명확하지 않은 거거든.

중복 코드는 같은 코드 구조가 두 곳 이상에 있으면 하나로 합쳐야 하는 거야. 함수 추출하기, 메서드 올리기, 문장 슬라이드하기 같은 기법으로 해결해. 긴 함수도 문제인데, "함수가 길수록 이해하기 어렵다." 핵심은 **"무엇을 하는지"**와 **"어떻게 하는지"**를 분리하는 거야. 코드 블록 앞에 주석이 달려 있다면, 그 블록은 함수로 추출할 후보지. 임시 변수가 많으면 질의 함수로 바꾸고, 매개변수가 많아지면 매개변수 객체를 만들어.

긴 매개변수 목록은 매개변수가 너무 많아서 그 자체로 문제가 되는 경우야. 다른 매개변수에서 질의해서 얻을 수 있는 값이면 매개변수를 없애고, 데이터 뭉치면 객체로 묶어. 전역 데이터는 코드베이스 어디서든 변경할 수 있고 누가 바꿨는지 추적하기 어렵잖아. 가장 악명 높은 악취 중 하나야. 변수 캡슐화하기로 접근 경로를 제한하고 감시해야 해. 가변 데이터도 마찬가지야. 데이터가 변경될 수 있으면 예상치 못한 곳에서 값이 바뀌어 버그가 생기거든. 변수 캡슐화, 변수 쪼개기, 세터 제거하기, 참조를 값으로 바꾸기 같은 방법으로 대응해.

뒤엉킨 변경은 **단일 책임 원칙(SRP)**이 위반된 상태야. 하나의 모듈이 서로 다른 이유로 변경되는 경우지. 새로운 데이터베이스를 지원할 때도, 새로운 금융 상품을 추가할 때도 같은 모듈을 수정해야 한다면 뒤엉켜 있는 거야. 반대로 산탄총 수술은 하나의 변경 사유가 여러 모듈에 영향을 미치는 경우야. 한 가지를 바꾸려면 여기저기 조금씩 수정해야 하는 거지. 이 둘은 동전의 양면이야.

기능 편애는 어떤 함수가 자기가 속한 모듈의 데이터보다 다른 모듈의 데이터를 더 많이 다루는 경우야. 다른 클래스의 getter를 떡칠하고 있으면 이 냄새지. 해결책은 그 함수를 데이터가 있는 곳으로 옮기는 거야. 판단 기준은 **"함께 변경되는 것을 함께 두는 것"**이야. 데이터 뭉치는 데이터 항목 서너 개가 늘 함께 다니는 경우고, 그 뭉치에서 하나를 빼면 나머지가 의미 없어진다면 객체로 만들 가치가 있어.

기본형 집착은 돈을 숫자로만, 전화번호를 문자열로만 다루는 거야. "기본형(primitive)을 객체로 바꿔라." 타입 코드가 조건문을 제어한다면 서브클래스로 바꾸기와 다형성을 활용해. 반복되는 switch문은 1판에서는 switch 자체를 악취로 봤는데, 2판에서는 입장이 바뀌었어. **"같은 조건 분기가 여러 곳에서 반복되면 그때 문제"**라고 봐. 한 곳에만 있는 switch는 괜찮아. 파울러는 반복문보다 파이프라인(filter, map, reduce)을 선호하는데, 각 단계가 뭘 하는지 명확해지거든.

성의 없는 요소는 본문 코드를 그대로 쓰는 것과 차이가 없는 함수, 실질적으로 클래스가 아닌 클래스야. 구조를 잡기 위해 만들었는데 실제로 불필요한 경우지. 추측성 일반화는 "나중에 이런 기능이 필요할지도 몰라"라는 생각으로 만들어둔 코드야. YAGNI 원칙에 어긋나는 거지. 임시 필드는 특정 상황에서만 값이 설정되는 필드인데, 객체의 필드가 항상 값을 가져야 한다고 기대하는 상황에서 혼란을 줘.

메시지 체인a.getB().getC().getD() 같은 코드야. 클라이언트가 객체 내부 구조를 너무 많이 알게 돼. 위임 숨기기를 적용하는데, 과하면 중개자 냄새가 나니까 균형이 필요해. 클래스가 하는 일의 대부분이 다른 클래스에 위임하는 것뿐이라면 존재 의의가 없는 거거든. 내부자 거래는 모듈 사이에서 데이터를 지나치게 주고받는 경우야. 결합도가 높다는 신호지.

거대한 클래스는 필드가 너무 많으면 중복 코드가 생기기 쉬워. 클래스 추출하기로 분리해. 서로 다른 인터페이스의 대안 클래스들은 같은 역할을 하는 클래스들인데 인터페이스가 다른 경우야. 메서드 시그니처를 맞추고 충분히 비슷해지면 슈퍼클래스를 추출할 수 있어. 데이터 클래스는 getter와 setter만 있는 클래스인데, 다른 클래스가 그 데이터를 다루는 동작을 가지고 있을 가능성이 높아. 그 동작을 데이터 클래스 안으로 옮겨야 해.

상속 포기는 부모 클래스의 메서드나 데이터가 서브클래스에 맞지 않는 경우야. 상속 계층이 잘못 설계된 신호지. 그리고 마지막으로 주석. "주석을 달아야겠다는 느낌이 들면, 먼저 주석이 필요 없는 코드로 리팩터링해보라." 주석 자체가 악취는 아니야. 좋은 주석은 가치 있어. 하지만 주석이 코드의 나쁜 구조를 가리는 탈취제로 쓰이고 있다면 문제야. "주석을 남겨야 할 시점은 '무엇을 해야 하는지'가 아니라 '왜 이렇게 했는지' 모르겠을 때다."


정리

3장 읽고 기억할 거 세 가지:

  1. "코드 악취는 정량적 기준이 아니라 직감이다." 뭔가 꺼림칙하면 들여다봐
  2. "뒤엉킨 변경과 산탄총 수술은 동전의 양면이다." 하나는 한 모듈이 여러 이유로 바뀌는 것, 다른 하나는 한 이유가 여러 모듈을 바꾸는 것
  3. "주석이 필요하다고 느끼면 먼저 리팩터링을 시도하라." 좋은 코드는 스스로 설명해