카탈로그의 첫 장이자 사용 빈도 1위 묶음이다. 저자가 “제일 먼저 익혀야 할 리팩터링”이라 부르는, 다른 모든 리팩터링의 재료가 되는 기본기들이다. 화려하지 않지만 하루에도 몇 번씩 손이 가는 기법들이라, 이 장의 기법들이 손에 붙으면 코드를 대하는 태도 자체가 바뀐다. 아래는 내가 자주 꺼내 쓰는 순서대로 정리한 노트다.
이 장의 기법들은 계열로 묶어 두면 외우기 쉽다. 특히 추출과 인라인처럼 방향이 반대인 쌍이 여럿이라, 지도로 보면 관계가 한눈에 잡힌다.
flowchart LR subgraph fn ["함수 계열"] ef1[함수 추출하기] <--> ef2[함수 인라인하기] end subgraph var ["변수 계열"] ev1[변수 추출하기] <--> ev2[변수 인라인하기] end subgraph name ["이름·시그니처"] d1[함수 선언 바꾸기] d2[변수 이름 바꾸기] d3[변수 캡슐화하기] end subgraph grp ["묶기 계열"] g1[매개변수 객체 만들기] g2[여러 함수를 클래스로 묶기] g3[변환 함수로 묶기] g4[단계 쪼개기] end
양방향 화살표가 말해 주듯, 방향은 상황이 정한다. 이제 계열별로 하나씩 짚어 보자.
함수 추출하기
코드 조각을 별도 함수로 빼고, 그 조각이 하는 일을 드러내는 이름을 붙인다. 기준은 길이가 아니라 목적과 구현의 분리다. 코드를 들여다보고 “무슨 일을 하는지”를 파악하는 데 시간이 걸린다면, 그 부분을 추출해서 이름으로 “무엇”을 말하고 본문에 “어떻게”를 남긴다. 한 줄짜리라도 이름이 본문보다 의도를 잘 드러내면 추출할 가치가 있다.
// before
function printOwing(invoice) {
printBanner()
let outstanding = calculateOutstanding()
console.log(`고객명: ${invoice.customer}`)
console.log(`채무액: ${outstanding}`)
}
// after
function printOwing(invoice) {
printBanner()
let outstanding = calculateOutstanding()
printDetails(invoice, outstanding)
function printDetails(invoice, outstanding) {
console.log(`고객명: ${invoice.customer}`)
console.log(`채무액: ${outstanding}`)
}
}절차 요점: 추출할 범위를 정하고, 새 함수로 옮기고, 원래 있던 지역 변수 중 함수 안에서만 쓰는 것은 함께 옮기고, 밖에서도 쓰는 것은 매개변수로 넘긴다. 함정은 변수 처리다. 옮길 코드가 지역 변수의 값을 수정하면 단순 추출이 깨진다. 이럴 땐 변수를 반환값으로 돌려주거나, 먼저 “변수 쪼개기”로 손을 본 뒤 추출한다.
함수 인라인하기
추출의 반대 방향. 함수 본문이 이름만큼(혹은 이름보다 더) 명확하면, 그 간접 계층은 오히려 방해물이다. 본문을 호출부에 도로 집어넣고 함수를 지운다. 잘못 쪼개 놓은 함수들을 일단 인라인으로 한 덩어리로 뭉친 뒤, 경계를 다시 그어 재추출하는 용도로도 쓴다. 추출과 인라인이 쌍으로 존재한다는 사실 자체가 이 책의 교훈이다 — 리팩터링에 일방통행은 없고, 방향은 상황이 정한다.
// before
function getRating(driver) {
return moreThanFiveLateDeliveries(driver) ? 2 : 1
}
function moreThanFiveLateDeliveries(driver) {
return driver.numberOfLateDeliveries > 5
}
// after
function getRating(driver) {
return driver.numberOfLateDeliveries > 5 ? 2 : 1
}변수 추출하기 / 변수 인라인하기
복잡한 표현식의 중간 결과에 이름을 붙이면 코드가 스스로를 설명한다. 디버거로 중단점을 걸 자리도 생긴다. 반대로 이름이 표현식보다 나을 게 없으면 인라인해서 걷어낸다.
// before
return (
order.quantity * order.itemPrice -
Math.max(0, order.quantity - 500) * order.itemPrice * 0.05 +
Math.min(order.quantity * order.itemPrice * 0.1, 100)
)
// after
const basePrice = order.quantity * order.itemPrice
const quantityDiscount = Math.max(0, order.quantity - 500) * order.itemPrice * 0.05
const shipping = Math.min(basePrice * 0.1, 100)
return basePrice - quantityDiscount + shipping주의: 변수 추출은 그 함수 안에서만 의미 있는 이름일 때 쓴다. 이름이 함수 경계를 넘어 여러 곳에서 값질 만하면, 변수가 아니라 함수(질의 함수)로 추출하는 편이 낫다.
함수 선언 바꾸기
함수의 이름이나 매개변수 목록을 바꾼다. 좋은 이름을 찾는 요령이 실용적이다. 함수에 달 주석을 한 줄 먼저 써 보고, 그 주석을 함수 이름으로 바꾼다. 이름이 잘 지어지지 않으면 대개 함수가 너무 많은 일을 하는 것이다.
호출자가 적으면 그냥 한 번에 바꾸는 “간단한 절차”로 끝낸다. 하지만 호출자가 많거나 외부에 공개된 API라면 마이그레이션 절차로 안전하게 옮긴다.
function circum(radius) { return 2 * Math.PI * radius}const track = circum(wheelRadius)const border = circum(tableRadius)
이 절차의 미덕은 어느 걸음에서 멈춰도 코드가 항상 동작한다는 것이다. 옛 함수와 새 함수가 잠시 공존하는 동안에도 초록불이 유지된다.
변수 캡슐화하기
데이터는 함수보다 다루기 어렵다. 함수는 옮겨도 옛 이름으로 리다이렉트해 두면 되지만, 데이터는 참조하는 모든 곳이 결합점이라 한 번에 옮기기 어렵다. 그래서 널리 쓰이는 데이터를 접근 함수(getter/setter) 뒤로 숨겨, 읽기·쓰기가 반드시 통과하는 관문을 만든다. 관문이 생기면 유효성 검증·로깅·나중의 데이터 구조 변경을 한곳에서 처리할 수 있다.
// before: 전역 데이터를 직접 참조
let defaultOwner = { firstName: "마틴", lastName: "파울러" }
// after: 접근 함수로 캡슐화
let defaultOwnerData = { firstName: "마틴", lastName: "파울러" }
export function defaultOwner() {
return defaultOwnerData
}
export function setDefaultOwner(arg) {
defaultOwnerData = arg
}이것만으로는 반환된 객체 내부를 밖에서 바꿀 수 있으니, 더 엄격히 가려면 getter가 복제본을 돌려주거나 레코드 자체를 클래스로 캡슐화한다. 변수 캡슐화는 이후 더 큰 리팩터링(데이터 이동)을 위한 준비 단계로 자주 쓰인다.
변수 이름 바꾸기
이름은 프로그래밍의 핵심이고, 잘못 지은 이름은 두고두고 오해를 부른다. 좁은 범위의 지역 변수는 그냥 바꾸면 되지만, 널리 쓰이는 변수라면 먼저 변수 캡슐화하기로 접근 함수를 만든 뒤 그 함수 이름을 바꾸는 식으로 파급을 통제한다. 이름 바꾸기와 캡슐화가 서로 맞물려 쓰인다는 게 포인트다.
매개변수 객체 만들기
데이터 몇 개가 여러 함수를 늘 함께 몰려다니면(데이터 뭉치, data clump), 그것들을 하나의 객체로 묶는다. 단순한 정리를 넘어, 이 새 구조가 새로운 추상화의 씨앗이 되는 경우가 많다. 예컨대 min/max 쌍을 묶어 NumberRange를 만들면, 그 범위와 관련된 동작(contains 등)이 자연스럽게 그 객체로 모여든다.
// before
function amountInvoiced(startDate, endDate) {
/* ... */
}
function amountReceived(startDate, endDate) {
/* ... */
}
// after
class DateRange {
constructor(start, end) {
this._start = start
this._end = end
}
get start() {
return this._start
}
get end() {
return this._end
}
}
function amountInvoiced(aDateRange) {
/* ... */
}
function amountReceived(aDateRange) {
/* ... */
}여러 함수를 클래스로 묶기
같은 데이터 덩어리를 두고 함께 작동하는 함수들이 흩어져 있으면, 그 데이터를 필드로 갖는 클래스로 모은다. 공통 인자를 매번 넘기던 함수들이 메서드가 되면서 인자 목록이 짧아지고, 객체를 다른 곳으로 넘기기도 쉬워진다. 4장의 Province/Producer가 바로 이 형태다 — 지역 데이터와 그에 딸린 계산들이 한 클래스 안에 모여 있다.
여러 함수를 변환 함수로 묶기
클래스로 묶기와 목적은 비슷하지만, 원본 데이터를 바꾸지 않고 읽기만 할 때 쓴다. 원본 레코드를 받아 파생 정보를 잔뜩 채운 새 레코드를 만들어 돌려주는 변환 함수(transform) 하나에 관련 계산을 모은다. 함수형 스타일에 잘 맞고, 파생값 계산 로직이 한군데로 모여 중복이 사라진다.
function enrichReading(original) {
const result = _.cloneDeep(original) // 원본 불변, 복제본에 파생값 추가
result.baseCharge = baseRate(result.month, result.year) * result.quantity
result.taxableCharge = Math.max(0, result.baseCharge - taxThreshold(result.year))
return result
}단계 쪼개기
서로 다른 두 가지 일을 하는 코드를 두 단계로 가른다. 흔한 예는 “입력 텍스트를 파싱해서 구조를 만드는 단계”와 “그 구조로 계산하는 단계”를 분리하는 것이다. 각 단계를 독립적으로 이해하고 수정할 수 있게 되고, 두 단계 사이는 중간 데이터 구조로 연결한다.
// before: 파싱과 계산이 뒤엉킴
function priceOrder(product, quantity, shippingMethod) {
/* 한 덩어리 */
}
// after: 중간 데이터로 두 단계 분리
function priceOrder(product, quantity, shippingMethod) {
const priceData = calculatePricingData(product, quantity) // 1단계: 상품 가격 계산
return applyShipping(priceData, shippingMethod, quantity) // 2단계: 배송비 적용
}정리
- 함수 추출의 기준은 줄 수가 아니라 “이름 붙일 가치가 있는 목적”이다. 목적과 구현을 가르는 게 본질이다.
- 추출과 인라인이 쌍이듯 모든 리팩터링은 되돌릴 수 있다. 방향은 상황이 정한다.
- 호출자가 많은 함수 선언 바꾸기, 이름 바꾸기, 캡슐화는 마이그레이션 절차로 옛것과 새것을 잠시 공존시켜 매 걸음 초록불을 유지한다.
- 변수 캡슐화는 이후 데이터 이동 같은 큰 리팩터링의 준비 단계로 자주 쓰인다.
- 매개변수 객체·클래스로 묶기·변환 함수로 묶기·단계 쪼개기는 흩어진 데이터와 로직에 구조를 부여하고, 종종 새 추상화의 씨앗이 된다.