데이터 구조는 프로그램에서 중요한 역할을 하는 만큼, 데이터에 집중한 리팩터링만 따로 모은 장이다. 이 장을 관통하는 관점 하나가 있다. 가변 데이터가 문제의 근원이라는 것. 하나의 변수가 여러 값을 오가고, 계산값을 저장해 두고, 참조가 여기저기 공유되면 버그가 스며들 틈이 생긴다. 그래서 이 장의 기법들은 대체로 “저장 대신 계산, 재할당 대신 새 변수, 가능하면 불변”을 향한다. 불변 데이터를 선호하는 이 감각은 Rust의 소유권/&mut 모델과도 자연스럽게 이어진다 — Rust는 “누가 이 데이터를 바꿀 수 있는가”를 타입 시스템으로 강제하는데, 파울러는 같은 규율을 설계 습관으로 권한다.
변수 쪼개기
변수 하나가 두 가지 값을 순차적으로 담으면(재할당) 읽는 사람은 매 지점에서 “지금 이 변수가 뭘 담고 있지”를 추적해야 한다. 역할 하나당 변수 하나가 원칙이다.
// before — temp 가 두 가지 의미로 재사용됨
let temp = 2 * (height + width)
console.log(temp) // 둘레
temp = height * width
console.log(temp) // 넓이
// after — 역할마다 변수 하나, const 로
const perimeter = 2 * (height + width)
console.log(perimeter)
const area = height * width
console.log(area)판단 기준: 누적(수집) 변수가 아닌데 두 번 이상 대입된다면 쪼갤 신호다. 반복문 카운터나 total += x 같은 누적 변수는 예외 — 그건 역할이 하나(계속 쌓기)다. 쪼갠 뒤에는 가능하면 const로 선언해 재할당을 컴파일 단계에서 막는다. 함정: 이름을 새로 지어야 하는데 억지스러우면(temp1, temp2) 사실 그 코드 자체가 무슨 값인지 불분명하다는 신호이므로 도메인 이름을 고민해 붙인다.
필드 이름 바꾸기
레코드 필드 이름은 프로그램 전체의 어휘다. 데이터 구조가 널리 쓰일수록 이름의 가치가 커진다. 이름이 실제 의미와 어긋나면 그 데이터를 만지는 모두가 오해를 물려받는다.
// before
const organization = { name: "애크미 구스베리", country: "GB" }
// after — name -> title 로, 하지만 널리 쓰이면 캡슐화부터
class Organization {
constructor(data) {
this._title = data.title
}
get title() {
return this._title
}
set title(arg) {
this._title = arg
}
}캡슐화돼 있으면 접근자 이름만 바꾸면 되므로 안전하다. 캡슐화가 안 돼 있고 널리 쓰이는 필드라면, 먼저 레코드를 캡슐화한 뒤 이름을 바꾼다. 좁은 범위에서만 쓰이면 바로 바꿔도 된다. 함정: 이름 바꾸기를 미루는 것은 프로그램 전체 어휘의 부채를 쌓는 것이다. 어긋난 이름 하나가 코드 읽는 모든 사람의 머릿속에 각주를 강요한다.
파생 변수를 질의 함수로 바꾸기
계산으로 얻을 수 있는 값을 필드에 저장해 두면, 원본이 바뀔 때마다 갱신을 챙겨야 하고 놓치면 불일치 버그가 난다. 저장하지 말고 계산한다.
// before — discountedTotal 을 저장, setter 마다 갱신
class ProductionPlan {
get production() {
return this._production
}
applyAdjustment(anAdjustment) {
this._adjustments.push(anAdjustment)
this._production += anAdjustment.amount // 갱신을 잊으면 불일치
}
}
// after — 저장하지 말고 매번 계산
class ProductionPlan {
get production() {
return this._adjustments.reduce((sum, a) => sum + a.amount, 0)
}
applyAdjustment(anAdjustment) {
this._adjustments.push(anAdjustment)
}
}이렇게 하면 _production이라는 가변 필드가 사라지고, “조정 목록”이라는 단일 진실만 남는다. 가변 데이터의 유효 범위를 줄이는 가장 효과적인 방법 중 하나다. 함정: 계산 비용이 정말 큰 경우(무거운 집계, 외부 호출)는 예외지만, 그 판단은 프로파일링 후에 한다. 대부분의 파생값은 그냥 계산하는 게 옳다.
참조를 값으로 바꾸기 / 값을 참조로 바꾸기
또 하나의 쌍이고, 기준이 아주 명확하다. “갱신을 공유해야 하는가.”
이 단 하나의 질문이 값과 참조를 가르는 결정 트리 전체다.
flowchart TD Q{"갱신을 공유해야 하는가?"} Q -->|아니오| V["값으로<br/>불변·복사 자유·동시성 안전"] Q -->|예| R["참조로<br/>같은 인스턴스 공유·갱신 즉시 반영"] V --> VN["함정: 공유가 필요한데 값이면<br/>갱신이 갈라진다"] R --> RN["함정: 공유가 불필요한데 참조면<br/>원격 변경에 시달린다"]
취향이 아니라 요구사항이 분기를 정한다는 점이 이 트리의 핵심이다.
값 객체는 불변이라 자유롭게 복사하고 여러 곳에 분산해도 안전하다. 내부를 통째로 새 객체로 교체하는 식으로 다룬다. 다루기 쉽고, 특히 동시성 상황에서 안전하다.
// 참조를 값으로 — 내부 객체를 통째로 교체(불변 취급)
class Person {
get telephoneNumber() {
return this._telephoneNumber
}
set officeAreaCode(arg) {
this._telephoneNumber = new TelephoneNumber(arg, this.officeNumber)
}
}
// TelephoneNumber 는 equals 로 값 비교, 필드 수정 불가반대로, 여러 곳에서 같은 객체를 공유하며 한 곳의 갱신이 나머지 모두에 즉시 보여야 하는 데이터라면 참조가 맞다. 이럴 때 값으로 복사해 뿌리면 갱신이 갈라져 버그가 된다.
// 값을 참조로 — 저장소를 두고 같은 인스턴스를 공유
let customer = customerRepository.get(order.customerId) // 항상 동일 인스턴스기준을 다시 정리하면: 갱신을 공유할 필요가 없다면 값(불변)이 다루기 쉽고, 하나의 갱신을 여러 곳이 함께 봐야 한다면 참조다. 취향이 아니라 요구사항이 정한다. 함정: 공유가 필요한 데이터를 값으로 복사하면 “왜 다른 화면엔 반영이 안 되지” 류의 버그가 생기고, 반대로 공유가 필요 없는데 참조로 두면 의도치 않은 원격 변경(action at a distance)에 시달린다.
매직 리터럴 바꾸기
코드에 박힌 숫자·문자열이 그 자체로 의미를 말해주지 않으면, 이름 있는 상수로 바꾼다.
// before
if (aValue > 9.80665) { ... }
// after
const STANDARD_GRAVITY = 9.80665;
if (aValue > STANDARD_GRAVITY) { ... }상수 이름이 “이 숫자가 표준 중력이다”를 설명하고, 나중에 값이 바뀌어도 한 곳만 고치면 된다. 과잉 적용 경고: 리터럴이 문맥에서 이미 명백하면 상수화가 오히려 잡음이다. for (let i = 0; ...)의 0을 ZERO로 바꾸거나, x * 2를 반으로 나눈다는 게 자명한데 TWO로 바꾸는 건 가독성을 해친다. 기준은 “숫자 자체가 의미를 못 말할 때만”. 값이 여러 곳에서 반복되거나, 값의 출처/의미가 코드만 봐선 알기 어려울 때가 진짜 대상이다.
정리
- 가변 데이터가 문제의 근원이라는 관점이 장 전체를 관통한다. 저장 대신 계산, 재할당 대신 새 변수, 가능하면 불변. 이 감각은 Rust의 소유권 모델과 곧장 이어진다.
- 변수 쪼개기: 누적 변수가 아닌데 두 번 대입되면 역할이 둘이라는 신호. 쪼갠 뒤
const로 못박는다. - 파생 변수를 질의 함수로: 계산할 수 있는 값은 저장하지 않는다. 저장은 갱신 누락이라는 버그 표면을 늘린다.
- 값이냐 참조냐는 취향이 아니라 “갱신을 공유해야 하는가”라는 명확한 기준이 있다.
- 매직 리터럴은 의미를 못 말하는 리터럴만 대상이다. 자명한
0,1까지 상수화하는 건 과잉이다.