이 장은 JUnit 프레임워크의 ComparisonCompactor를 뜯어본다. 두 문자열이 어떻게 다른지 시각적으로 보여주는 작은 모듈로, 켄트 벡과 에릭 감마가 짠, 이미 충분히 훌륭한 코드다. 저자는 그 코드조차 더 깨끗하게 만들 수 있음을 단계별로 보여준다. 요지는 비판이 아니라 — 세상에 리팩터링이 끝난 코드는 없다는 것.

어떤 것들을 고쳤나

원본도 잘 짜인 코드였지만, 다듬을 지점은 계속 나온다.

  • 멤버 변수 접두어 제거. fPrefix 같은 f 접두어는 오늘날 IDE 환경에서 잡음이다.
  • 조건문 캡슐화. if (expected == null || actual == null || areStringsEqual())처럼 의도가 묻히는 조건식은 canBeCompacted() 같은 이름의 메서드로 뽑아 의도를 드러낸다.
  • 이름에 부정 대신 긍정. shouldNotCompact()보다 canBeCompacted()가 읽기 쉽다.
  • 함수 이름으로 부수 효과를 드러내기. 압축하고 문자열까지 만드는 함수라면 compact()가 아니라 formatCompactedComparison()처럼 하는 일을 정직하게 말해야 한다.
  • 숨겨진 시간적 결합 드러내기. findCommonPrefix()가 먼저 실행돼야 findCommonSuffix()가 올바로 동작한다면, 그 순서를 하나의 함수로 묶어 결합을 한 곳에 가둔다.

멤버 접두어 제거는 가장 사소해 보이지만 원본 전반에 퍼져 있던 변경이다.

// before — f 접두어가 모든 필드에 붙어 잡음
private String fExpected;
private String fActual;
private int fContextLength;
 
// after — 접두어 없이도 스코프로 충분히 구별된다
private String expected;
private String actual;
private int contextLength;

판단 기준: 접두어가 정보를 더하지 않고 습관으로만 붙어 있으면 제거 대상이다. 함정: 다만 한꺼번에 지우려다 참조 하나를 놓치면 컴파일이 깨지므로, IDE의 rename 리팩터링에 맡기고 테스트로 확인한다.

스텝 플레이어: compact() 정리

원본의 compact()는 세 가지 문제를 한 몸에 안고 있다 — 의도가 묻힌 조건식, 하는 일을 감춘 이름, 그리고 findCommonPrefix()findCommonSuffix()의 숨은 순서 의존. 아래는 그것을 세 걸음으로 걷어내는 과정이다.

Refactoring Step 원본 — 묻힌 조건·감춘 이름·숨은 순서 의존
1/3
public class ComparisonCompactor {    private String expected;    private String actual;    private int prefix;    private int suffix;    public String compact(String message) {        if (expected == null || actual == null || areStringsEqual())            return Assert.format(message, expected, actual);   // 의도가 조건식에 묻힘        findCommonPrefix();        // 이 둘은 순서 의존 —        findCommonSuffix();        // prefix 가 먼저여야 suffix 가 맞다        String expected = compactString(this.expected);        String actual = compactString(this.actual);        return Assert.format(message, expected, actual);    }}

판단 기준: 첫 걸음(조건 캡슐화)과 마지막 걸음(순서 결합 격리)을 나눈 이유는, 각 변경이 서로 다른 냄새를 겨눈다는 데 있다 — 하나는 가독성, 하나는 시간적 결합. 섞으면 무엇이 무엇을 고쳤는지 흐려진다. 함정: findCommonPrefix()/findCommonSuffix()를 그대로 두면 다음 사람이 순서를 바꿔 호출해 조용히 깨뜨릴 수 있다. compactExpectedAndActual()로 묶어 순서를 한 곳에 봉인하면, 그 위험 지점이 하나로 준다.

인상적인 건 과정이다. 저자는 중간에 자신이 내린 결정을 스스로 뒤집기도 한다. 리팩터링은 직선이 아니라 시행착오의 연속이고, 한 번 고친 코드를 다시 원래대로 되돌리는 것도 과정의 일부다. 테스트가 초록불이니 되돌리는 데 두려움이 없다.

보이스카우트 규칙

모듈은 처음보다 조금 더 깨끗해졌다. 원래 저자들이 못나서가 아니다 — 시간이 지나면 기준이 올라가고, 어떤 코드든 다시 보면 개선점이 보인다. 체크아웃할 때보다 조금 더 깨끗하게 체크인하는 것. 이 장 전체가 그 규칙의 시연이다.

정리

  • 훌륭한 코드도 리팩터링 대상이다. “이미 좋은데”는 멈출 이유가 못 된다.
  • 이름 하나, 조건식 하나 수준의 작은 개선이 모여 모듈의 격을 바꾼다. compact() 정리는 조건 캡슐화 → 정직한 이름 → 순서 결합 격리의 세 걸음이었다.
  • 리팩터링 중 결정을 뒤집는 것을 두려워하지 않는다. 테스트가 있으니까.

다음장으로 16장