1장에서 손으로 겪은 것을 이번 장은 언어로 정리한다. 리팩터링이 정확히 무엇이고, 왜, 언제 하며, 언제 하지 말아야 하는지. 그리고 관리자에게 어떻게 설명할 것인가까지. 파울러가 반복해서 강조하는 건 리팩터링이 별도의 활동이 아니라 프로그래밍의 호흡 그 자체라는 점이다.

정의: 명사이자 동사

파울러는 리팩터링을 두 가지 품사로 나눠 정의한다. 이 구분이 은근히 중요하다.

  • 리팩터링(명사): 겉으로 드러나는 동작은 그대로 둔 채, 코드를 이해하고 수정하기 쉽게 만드는 방향의 정돈된 코드 변경 하나. 6~12장의 카탈로그에 이름 붙어 실려 있는 개별 기법들이 여기 해당한다.
  • 리팩터링하다(동사): 그런 작은 변경 여러 개를 연달아 적용해 소프트웨어를 재구성하는 행위.

핵심은 겉보기 동작 보존이다. 동작이 바뀌면 그건 리팩터링이 아니라 기능 변경이거나 버그다. 이 엄격함이 실용성의 근원이다. “동작이 안 바뀐다”는 보장이 있어야 언제든 부담 없이 코드에 손댈 수 있다. 그래서 리팩터링은 항상 자가 테스트 코드를 전제로 한다.

파울러가 “겉보기(observable)“라는 단어를 고른 것도 의도적이다. 내부 구현이 아무리 달라져도, 밖에서 관찰되는 결과가 같으면 리팩터링이다. 예컨대 캐시를 추가하면 성능은 달라지지만 반환값은 그대로다 — 이런 경계 사례가 있어 정의가 완전히 수학적으로 엄밀하진 않지만, 실무 감각으로는 “쓰는 쪽이 차이를 못 느끼면 리팩터링”이면 충분하다.

두 개의 모자

켄트 벡의 비유다. 소프트웨어를 개발할 때 우리는 두 가지 모자 중 하나를 쓴다.

  • 기능 추가 모자: 새 기능을 넣는다. 테스트를 추가하고, 그 테스트를 통과시킨다. 기존 코드의 구조는 건드리지 않는다.
  • 리팩터링 모자: 구조만 바꾼다. 기능을 추가하지 않으므로 새 테스트를 만들지 않고(기존 테스트만 통과하면 된다), 인터페이스 변경이 불가피할 때를 빼면 테스트도 손대지 않는다.

실제 작업에선 모자를 수시로 바꿔 쓴다. 기능을 넣다가 “이 구조면 힘드네” 싶어 리팩터링 모자로 갈아쓰고, 정리가 끝나면 다시 기능 모자로 돌아온다. 파울러가 요구하는 건 하나다 — 지금 어느 모자를 쓰고 있는지 항상 의식하라. 그래야 커밋도 섞이지 않는다. 기능 커밋과 리팩터링 커밋을 갈라두면, 리팩터링만 되돌리거나 리뷰하기가 쉬워진다.

두 모자를 오가는 전환은 조건이 분명하다. 어느 쪽에 있든 자기 안에서 도는 루프와, 상대 모자로 넘어가는 문턱을 함께 그리면 이렇다.

stateDiagram-v2
  feature: 기능 추가 모자
  refactor: 리팩터링 모자
  [*] --> feature
  feature --> refactor: 구조가 걸리적거리면
  refactor --> feature: 정리가 끝나 기능이 쉬워지면
  feature --> feature: 테스트 추가하고 통과시킴
  refactor --> refactor: 기존 테스트만 통과 유지

한 번에 한 모자만 쓰고, 넘어갈 때는 의식적으로 갈아쓴다 — 이 규율이 곧 커밋 분리로 이어진다.

왜 리팩터링을 하는가

파울러는 리팩터링을 “만병통치약”이 아니라 목적을 가진 도구로 못박는다.

  • 소프트웨어 설계가 좋아진다: 리팩터링을 안 하면 아키텍처는 서서히 부식한다. 당장 동작하는 것만 좇으면 큰 그림이 흐려지고, 중복이 쌓이고, 코드베이스가 부풀어 오른다.
  • 이해하기 쉬워진다: 오늘 짠 나도 내일이면 왜 이렇게 짰는지 잊는다. 머릿속 이해를 코드에 새겨두면 다음 사람(대개 미래의 나)이 덜 헤맨다.
  • 버그를 찾기 쉬워진다: 코드를 정리하다 보면 구조가 명확해지고, 명확한 구조에서는 버그가 숨을 곳이 줄어든다. “리팩터링은 코드를 깊이 이해하게 만들고, 그 이해가 곧 버그 발견으로 이어진다.”
  • 더 빨리 개발하게 된다: 이게 진짜 이유다. 아래 경제적 근거에서 이어간다.

언제 하는가

리팩터링 전용 시간을 따로 빼지 않는다. 평범한 개발 흐름 속에 녹여 넣는다.

  • 3의 법칙: 처음엔 그냥 한다. 두 번째로 비슷한 걸 하며 중복이 보이면 눈살을 찌푸리되 일단 넘어간다. 세 번째엔 리팩터링한다. “세 번이면 삼진 아웃.”
  • 준비를 위한 리팩터링: 기능을 추가하기 직전이 최적의 타이밍이다. “이 구조라면 새 기능이 훨씬 쉬울 텐데”가 보이면, 먼저 구조를 그렇게 만들고 기능을 얹는다. 1장의 statement 예제가 정확히 이 패턴이었다.
  • 이해를 위한 리팩터링: 남의 코드(혹은 과거의 내 코드)를 읽고 파악한 내용을, 이름 변경과 함수 추출로 코드에 곧바로 새긴다. 이해가 머리에만 있으면 휘발되지만 코드에 넣으면 남는다. 파울러는 이를 “코드가 내 머릿속에서 다시 코드로 흘러나가게 하는 것”이라 표현한다.
  • 쓰레기 줍기 리팩터링: 코드를 읽다 지저분한 부분을 발견했을 때. 지금 고치기엔 시간이 없다면 작은 것만 고치고 큰 건 메모해둔다. 캠핑 규칙 — “처음 왔을 때보다 깨끗하게 두고 떠나라.”
  • 계획된 리팩터링: 위 방식으로 꾸준히 했다면 필요가 적다. 리팩터링을 위해 별도 일정을 크게 잡아야 하는 상황 자체가 그동안 방치했다는 신호다. “리팩터링은 대부분 눈에 안 띄게, 다른 일을 하는 김에 이뤄져야 한다.”

준비를 위한 리팩터링이 어떤 모습인지 한 조각으로 보자. 결제 수단이 카드 하나뿐인 함수에 계좌이체를 추가해야 한다고 하자. 지금 구조에 억지로 if를 하나 더 끼워 넣는 대신, 먼저 기존 로직을 옮기기 좋게 정리한다.

Refactoring Step 원본 — 기능을 추가하기 어렵게 얽혀 있다
1/3
// 금액 계산과 카드 결제가 한 함수에 뭉쳐 있다function charge(order) {  const amount = order.items.reduce((s, i) => s + i.price, 0)  cardGateway.pay(order.card, amount) // 카드에 강하게 묶여 있음}

두 번째 스텝까지는 기능(계좌이체)을 한 줄도 안 넣었다 — 오직 자리만 만들었다. 그리고 세 번째 스텝에서 bankGateway를 넘기는 것만으로 새 결제 수단이 얹힌다. “어려운 변경을 마주치면, 먼저 그 변경이 쉬워지도록 리팩터링한 다음, 쉬워진 변경을 한다”는 켄트 벡의 말 그대로다.

하지 말아야 할 때

파울러는 만능이 아니라고 분명히 선을 긋는다.

  • 그 코드를 다시 볼 일이 없을 때: 지저분해도 잘 돌아가고, 내부를 이해할 필요가 없는 API 너머의 코드라면 굳이 정리할 이유가 없다. 리팩터링의 가치는 이해와 수정을 쉽게 만드는 데 있는데, 이해할 필요가 없다면 가치도 없다.
  • 처음부터 다시 짜는 게 더 쉬울 때: 리팩터링으로 살리기보다 버리고 새로 쓰는 편이 나은 코드도 있다. 다만 이 판단은 경험이 필요하고, 대개는 리팩터링 쪽이 안전하다.

경제적 근거: 내부 품질 = 개발 속도

이 장의 심장부다. 리팩터링을 정당화하는 언어는 미학이 아니라 경제여야 한다. “깨끗한 코드”는 그 자체로 목적이 아니다. 진짜 명분은 이거다 — 잘 정돈된 코드베이스에서 우리는 더 빠르게 기능을 추가한다.

파울러는 이를 **설계 지구력 가설(Design Stamina Hypothesis)**로 설명한다. 설계에 신경 쓰지 않으면 초반엔 빠르지만 코드가 부패하면서 기능 추가 속도가 급격히 꺾인다. 반대로 내부 설계에 꾸준히 투자하면 속도가 오래 유지된다. 두 곡선이 교차하는 지점 이후로는 “좋은 설계”가 언제나 더 빠르다. 그리고 이 교차점은 우리 생각보다 훨씬 일찍 온다 — 몇 주 단위다.

그래서 파울러는 리팩터링을 관리자에게 굳이 보고하지 말라는 반쯤 농담을 던진다. 리팩터링은 “일을 잘하는 방법”의 일부지 협상 대상 항목이 아니기 때문이다. 기능을 빨리 내기 위해 하는 것이지, 코드 미화를 위해 시간을 얻어내는 협상이 아니다.

한편 이 모든 것의 전제가 하나 있다 — 자가 테스트 코드. 테스트 없이 하는 리팩터링은 위험하다. 겉보기 동작이 보존됐는지 확인할 방법이 없으니까. 그래서 레거시 코드는 “테스트부터 붙이고 → 리팩터링”이 순서다. 마이클 페더스의 『레거시 코드 활용 전략』이 여기서 짝을 이룬다.

현실의 걸림돌들

파울러는 리팩터링을 예찬만 하지 않고, 실무에서 부딪히는 저항도 정직하게 다룬다.

  • “기능 개발이 늦어진다”는 오해: 리팩터링의 목적은 어디까지나 빠른 개발이다. 그런데 리팩터링을 남용해 지금 당장 불필요한 정리에 시간을 쏟으면 본말이 전도된다. 판단 기준은 늘 “이 정리가 곧 있을 작업을 실제로 쉽게 하는가”다.
  • 코드 소유권: 함수 옮기기나 이름 바꾸기는 그 함수를 호출하는 다른 팀·모듈을 깨뜨릴 수 있다. 공개 API라면 옛 이름을 한동안 남겨 완만히 전환하는(deprecate) 배려가 필요하다.
  • 브랜치와 병합: 기능 브랜치를 오래 들고 있으면, 그동안 쌓인 리팩터링과 병합할 때 충돌이 커진다. 짧게 자주 통합하는 CI(지속적 통합) 흐름이 리팩터링과 궁합이 좋다.
  • 테스트와 레거시: 테스트 없는 레거시에서 리팩터링은 외줄타기다. 답은 하나 — 이해되는 최소 지점부터 테스트를 붙여 발판을 넓혀가며 조금씩 정리한다.
  • 데이터베이스: 스키마 변경은 되돌리기 어렵다. 하지만 진화적 DB 설계와 마이그레이션 스크립트를 작은 단위로 쌓는 방식으로, DB도 점진적 리팩터링이 가능해졌다.

리팩터링과 성능

가장 흔한 반박이 성능이다. 함수로 잘게 쪼개고 반복문을 나누면 느려지지 않나? 파울러의 입장은 1장에서 이미 예고됐다. 성능이 정말 중요한 소프트웨어라도, 먼저 잘 구조화된 코드를 만들고 나중에 최적화하라는 것이다. 이유는 두 가지다.

  • 대부분의 프로그램에서 시간의 대부분은 코드의 아주 좁은 일부에서 소비된다. 골고루 최적화하는 건 90%의 헛수고다.
  • 잘 정돈된 코드는 성능 병목을 정확히 찾아내기 쉽고, 찾은 곳을 집중적으로 튜닝하기도 쉽다. 프로파일러로 진짜 병목을 측정한 뒤, 그 좁은 영역만 손대면 된다.

짐작으로 최적화하지 않는다. 명료한 구조가 오히려 빠른 성능으로 가는 지름길이라는 게 파울러의 결론이다.

YAGNI, 그리고 진화하는 아키텍처

리팩터링이 널리 자리 잡으면서 설계에 대한 사고방식도 바뀌었다. 예전엔 코딩 전에 미래의 모든 요구를 예측해 유연한 구조를 미리 넣어두려 했다(“추측성 일반화”). 하지만 미래는 대개 예측대로 오지 않고, 미리 넣은 유연성은 쓰이지도 않으면서 코드만 복잡하게 만든다.

리팩터링이 있으면 다른 길이 열린다 — 지금 필요한 만큼만 단순하게 짓고, 새 요구가 실제로 오면 그때 구조를 진화시킨다. 이것이 **YAGNI(You Aren’t Gonna Need It)**다. 리팩터링 능력이 YAGNI를 가능하게 하고, YAGNI가 설계를 가볍게 유지한다. 아키텍처를 미리 확정하는 대신 계속 진화시킬 수 있게 된 것이다.

정리

  • 리팩터링은 겉보기 동작을 보존하며 내부 구조만 바꾸는 것. 이 엄격한 정의가 곧 안전장치이자 실용성의 근원이다.
  • 기능 추가와 리팩터링은 서로 다른 모자다. 동시에 쓰지 말고, 지금 어느 모자를 쓰는지 늘 의식하며 커밋도 분리한다.
  • 최고의 타이밍은 “지금 하려는 일이 어려울 때”. 그 어려움 자체가 구조 개선이 필요하다는 신호다. 별도 일정으로 크게 잡아야 한다면 이미 늦은 것이다.
  • 명분은 미학이 아니라 속도다. 내부 품질이 곧 개발 속도이고, 그 교차점은 생각보다 일찍 온다 — 경제적 언어로 사고해야 흔들리지 않는다.
  • 성능도, 미래 대비도 리팩터링 앞에서 재정의된다. 먼저 단순·명료하게 짓고, 병목은 측정해서, 유연성은 필요할 때 진화시킨다(YAGNI).