함수 선언 바꾸기 (Change Function Declaration)
배경
함수 선언은 코드의 연결부
함수 선언은 단순히 이름과 매개변수를 정하는 게 아니다.
이건 소프트웨어의 연결부를 어떻게 설계할 것인지에 대한 결정이다.
그리고 그 연결부에서 가장 중요한 건 함수의 이름이다.
이름은 생각보다 큰 영향을 미친다. 코드를 읽다 보면 의미가 잘 와닿지 않는 함수 이름을 만나기도 한다.
그럴 때 많은 사람들이 "일단 넘어가자"는 유혹에 빠지기 쉽다. 하지만 파울러는 말한다.
“더 나은 이름이 떠오르면, 즉시 바꿔라.” 그래야 나중에 또 고민할 필요가 없다.
매개변수는 더 어렵다
함수 이름보다 더 골치 아픈 게 바로 매개변수 설계다.
매개변수는 단순히 데이터를 넘기는 수단이 아니라, 어떤 수준에서 기능을 연결할지 결정하는 중요한 단서다.
예를 들어, "대여한 지 30일이 넘었는지 판단하는 함수"가 있다고 하자.
매개변수로 지불 객체를 넘길 수도 있고, 마감일을 넘길 수도 있다.
- 지불 객체를 넘기면 여러 속성에 자유롭게 접근할 수 있다는 장점이 생긴다.
- 반면, 인터페이스에 결합되기 때문에 유연성이 떨어질 수 있다.
이처럼 정답은 없다. 상황은 계속 바뀌고, 어느 시점엔 지금보다 더 나은 연결 방식이 눈에 들어온다.
그때마다 조금씩 개선해나가는 게 중요하다.
절차
간단한 변경일 경우
- 함수 본문에서 제거하거나 바꿀 매개변수를 사용하는 곳이 없는지 확인한다.
- 메서드 선언을 바꾸고 싶은 형태로 수정한다.
- 해당 메서드를 호출하는 모든 코드를 새 선언 형태에 맞게 고친다.
- 테스트한다.
마이그레이션 절차 (점진적 리팩터링)
- 기존 함수 본문을 적절히 리팩터링한다.
- 그 본문을 새 함수로 추출한다.
- 필요하다면, 새 함수에 새로운 매개변수를 추가한다.
- 테스트한다.
- 기존 함수를 인라인하여 제거한다.
- 임시로 붙여놨던 함수 이름이 있다면, 이 시점에서 정식 이름으로 되돌린다.
- 다시 테스트한다.
복잡한 상황들
상속 구조 안에 있을 경우
상속 관계에 있는 메서드를 바꿀 때는 다형성 관계를 모두 고려해야 한다.
상위 클래스에서 메서드를 수정하면, 서브클래스나 인터페이스 구현체에도 영향을 준다.
이럴 때는 간접 호출을 도입해 리스크를 줄이는 방식이 유용하다.
예를 들어, 변경하고 싶은 메서드를 새로 정의하고, 기존 메서드는 그걸 호출하게 만드는 방식이다.
공개 API라면?
공개된 API의 경우, 바로 리팩터링하는 건 위험하다.
이럴 땐 새로운 함수를 만들어두고, 기존 함수는 @deprecated 처리해두는 식으로 점진적으로 이전시키는 방법이 안전하다.
모든 클라이언트 코드가 새 API를 사용하게 되면, 그때서야 기존 함수를 제거한다.
예시
간단한 변경일 경우
💥 Before
function circum(radius) {
return 2 * Math.PI * radius;
}
✅ After
function circumference(radius) {
return 2 * Math.PI * radius;
}
이후, circum()을 호출하는 곳을 모두 찾아서, circumference()로 수정한다. ( 물론 요즘에는 IDE가 이를 모두 지원하기는 하지만.. )
마이그레이션 절차 (점진적 리팩터링)
💥 초기 버전
function circum(radius) {
return 2 * Math.PI * radius;
}
🔧 새 함수로 추출
function circum(radius) {
return circumference(radius); // 새 함수로 위임
}
function circumference(radius) {
return 2 * Math.PI * radius;
}
- 기존 함수는 유지하면서, 새로운 이름의 함수로 기능을 위임
- 호출자는 여전히 circum()을 사용하지만, 내부는 새 함수 사용
- 이 시점에서 테스트를 통해 기능이 동일함을 확인
✅ 예전 함수 인라인화
function circumference(radius) {
return 2 * Math.PI * radius;
}
변수 캡슐화하기 (Encapsulate Variable)
배경
데이터란 녀석..
함수는 바꾸기 쉬운 존재다.
이름을 바꿔도, 모듈을 옮겨도, 심지어 원래 함수를 두고 forward용 함수를 하나 더 만들어도 별 문제 없이 잘 굴러간다.
반면에 데이터는 다르다.
데이터를 직접 사용하는 코드가 여기저기 흩어져 있으면, 그걸 바꾸는 순간 모든 코드가 영향을 받는다.
그래서 유효 범위가 넓은 변수일수록 관리가 어렵다. 전역 변수를 피하라는 말이 괜히 나오는 게 아니다.
캡슐화가 필요한 이유
변수를 쉽게 옮기고, 변경 로직을 추가하고, 결합도를 낮추려면 데이터를 직접 참조하지 말고 함수를 통해 접근하는 게 좋다.
이렇게 접근을 통제할 수 있는 지점이 생기면, 그 지점을 기준으로 코드를 통제하고 리팩터링하기 쉬워진다.
- 접근을 통제할 수 있음 → 변경 시 검증, 로깅, 보안 처리 등 추가 로직 삽입 가능
- 참조하는 코드가 많을수록 → 캡슐화 효과가 커짐
- 레거시 코드처럼 변수 접근이 복잡한 경우 → 더더욱 캡슐화가 중요
객체지향에서 "데이터는 무조건 private으로"라고 강조하는 이유도 여기 있다.
변경 지점을 하나로 모아야 안전하게 다룰 수 있기 때문이다.
※ 불변 데이터는 상황이 좀 다르다. 바뀌지 않는 값이기 때문에 굳이 캡슐화할 필요는 적다. 검증 로직이 들어갈 여지도 없기 때문.
절차
- 해당 변수에 접근하고 수정하는 함수를 만든다.
- 예: getAmount(), setAmount(value)
- 정적 검사를 수행해서 누락된 참조가 없는지 확인한다.
- 기존 코드에서 직접 변수에 접근하던 부분을 하나씩 함수 호출로 교체한다.
- 하나 바꿀 때마다 테스트
- 변수의 접근 범위를 private 혹은 최소한으로 줄인다.
- 다시 테스트
예시
💥 전역 변수에 중요한 데이터가 담겨있는 경우
let defaultOwner = {firstName: "Martin", lastName: "Fowler"};
💥 다른 영역에서 해당 변수를 사용
spaceship.owner = defaultOwner;
defaultOwner = {firstName: "Rebecca", lastName: "Parsons"};
🔧 변수 데이터를 읽고 쓰는 함수 정의
function getDefaultOwner() {return defaultOwner;}
function setDefaultOwner(arg) {defaultOwner = arg;}
🔧 defaultOwner를 참조하는 코드를 찾아서 새로 선언한 함수로 대체 (w/ 테스트)
spaceship.owner = getDefaultOwner();
setDefaultOwner({firstName: "Rebecca", lastName: "Parsons"});
✅ 변수의 가시범위 제한
// 변수는 모듈 스코프 내에 숨김
let defaultOwner = { firstName: "Martin", lastName: "Fowler" };
// getter 함수: 외부에서 값을 읽을 때 사용
export function getDefaultOwner() {
return defaultOwner;
}
// setter 함수: 외부에서 값을 설정할 때 사용
export function setDefaultOwner(arg) {
defaultOwner = arg;
}
그러나, 이런 캡슐화를 진행하더라도 객체의 필드에 대한 제어는 할 수 없다. 만약, 각 객체 내 담기는 내용을 변경하는 행위까지 제어하고 싶을 때는 (1) Getter가 데이터의 복제본을 반환해주거나 (2) 레코드 캡슐화 방법을 취하면 된다.
(1) Getter가 데이터의 복제본을 반환해주는 방법
let defaultOwnerData = { firstName: "Martin", lastName: "Fowler" };
// 외부에서 객체를 가져갈 수는 있지만, 복사본만 가져감 (원본 보호)
export function defaultOwner() {
return Object.assign({}, defaultOwnerData);
}
// 내부 상태 변경은 여전히 가능하되, 반드시 setter를 거쳐야 함
export function setDefaultOwner(arg) {
defaultOwnerData = arg;
}
(2) 레코드 캡슐화 방법
let defaultOwnerData = { firstName: "Martin", lastName: "Fowler" };
// Person 인스턴스를 생성해 반환 → 데이터 보호 및 메서드 활용 가능
export function defaultOwner() {
return new Person(defaultOwnerData);
}
export function setDefaultOwner(arg) {
defaultOwnerData = arg;
}
// 클래스 정의
class Person {
constructor(data) {
this._firstName = data.firstName;
this._lastName = data.lastName;
}
get firstName() {
return this._firstName;
}
get lastName() {
return this._lastName;
}
// 나중에 여기에 로직 메서드 추가 가능 (예: fullName 등)
}
변수 이름 바꾸기 (Rename Variable)
배경
이름 짓기의 중요성
이름을 잘 짓는 건 좋은 프로그래밍의 핵심이다.
특히 변수 이름은 프로그래머가 무슨 생각을 했는지를 가장 직접적으로 보여주는 단서다.
복잡한 로직도 변수 이름만 잘 지어도 갑자기 의미가 또렷해진다.
반대로 x, temp, data, value 같은 애매한 이름이 남아 있는 코드에선 맥락을 이해하려면 결국 구현을 전부 뜯어봐야 한다.
짧은 이름도 필요할 때가 있다
물론 모든 변수를 길고 상세하게 지을 필요는 없다.
예를 들어 한 줄짜리 람다식이나, 간단한 유틸 함수에서는 x, y 같은 변수명이 더 간결하고 직관적일 때도 있다.
문맥을 봤을 때 바로 이해된다면, 짧은 이름도 괜찮다.
하지만 로직이 조금만 복잡해지거나, 변수가 등장하는 범위가 넓어진다면 이름을 분명하게 바꿔주는 게 맞다.
절차
- 해당 변수가 너무 널리 쓰이고 있다면, 먼저 변수 캡슐화부터 고려한다.
- 그래야 리팩터링이 쉬워진다.
- 이름을 바꿀 변수를 참조하는 코드를 전부 찾아서 하나씩 변경한다.
- 단순한 치환은 위험할 수 있음. 반드시 맥락을 확인하면서 변경할 것.
- 테스트한다.
예시
💥 초기 버전
let tpHd = "untitled";
// 중간 생략...
result += `<h1>${tpHd}</h1>`;
// 중간 생략...
tpHd = obj['articleTitle'];
문제점:
- tpHd가 여기저기 직접 참조됨 → 데이터 구조가 바뀌면 전역 수정이 필요
- tpHd가 의미하는 바가 명확하지 않음 → "title"인지 "header"인지 혼동 가능
🔧 변수 캡슐화
// 내부 변수는 직접 접근 대신 캡슐화된 함수로 제어
let tpHd = "untitled";
// 읽을 때는 title()을 사용
result += `<h1>${title()}</h1>`;
// 쓸 때는 setTitle()로 설정
setTitle(obj['articleTitle']);
// 캡슐화 함수
function title() {
return tpHd;
}
function setTitle(arg) {
tpHd = arg;
}
✅ 변수 이름 변경
// 명확한 의미의 변수명으로 교체
let _title = "untitled";
// Getter / Setter도 그대로 유지
function title() {
return _title;
}
function setTitle(arg) {
_title = arg;
}
'Programming > Refactoring' 카테고리의 다른 글
기본적인 리팩터링 (1) (0) | 2025.04.10 |
---|---|
테스트 구축하기 (0) | 2025.03.20 |
코드에서 나는 악취 (1) | 2025.03.13 |
리팩터링: 첫 번째 예시 (0) | 2025.02.26 |
리팩터링 2판 공부를 시작하며 (0) | 2025.02.24 |