😀 책에서 기억하고 싶은 내용을 써보세요.
배경
- 함수는 프로그램을 작은 부분으로 나누는 주된 수단이다.
- 함수 선언은 각 부분이 서로 맞물이는 방식을 표현하며, 실질적으로 소프트웨어 시스템의 구성 요소를 조립하는 연결부 역할을 한다.
- 건축과 마찬가지로 소프트웨어도 이러한 연결분에 상당히 의존한다.
- 연결부를 잘 정의하면 시스템에 새로운 부분을 추가하기가 쉬워지는 반면, 잘못 정의하면 지속적인 방해 요인으로 작용하여 소프트웨어 동작을 파악하기 어려워지고 요구사항이 바뀔 때 적절히 수정하기 어렵게 한다.
- 다행히 소프트웨어는 소프트하기 때문에 연결부를 수정할 수 있다.
- 이러한 연결부에 가장 중요한 요소는 함수의 이름이다.
- 이름이 좋으면 함수의 구현 코드를 살펴볼 필요 없이 호출문만 보고도 무슨 일을 하는지 파악할 수 있다.
- 이름이 잘못된 함수를 발견하면 더 나은 이름이 떠오르는 즉시 바꾸라는 명령으로 받아들인다.
- 좋은 이름을 떠올리는 데 효과적인 방법이 하나 있다.
- 바로 주석을 이용해 함수의 목적을 설명해 보는 것이다. 그러다 보면 주석이 멋진 이름으로 바뀌어 되돌아올 때가 있다.
- 함수의 매개변수도 마찬가지다.
- 매개변수는 함수가 외부 세계와 어우러지는 방식을 정의한다.
- 매개변수는 함수를 사용하는 문맥을 설정한다.
절차
함수 선언 바꾸기는 ‘간단한 절차' 만으로 충분할 때도 많지만, 더 세분화된 ‘마이그레이션 절차' 가 훨씬 적합한 경우도 많다. 따라서 리팩터링을 할 때는 먼저 변경 사항을 살펴보고 함수 선언과 호출문들을 단번에 고칠 수 있을지 가늠해본다.
간단한 절차
- 매개변수를 제거하려거든 먼저 함수 본문에서 제거 대상 매개변수를 참조하는 곳은 없는지 확인한다.
- 메서드 선언을 원하는 형태로 바꾼다.
- 기존 메서드 선언을 참조하는 부분을 모두 찾아서 바뀐 형태로 수정한다.
- 테스트한다.
변경할 게 둘 이상이면 나눠서 처리하는 편이 나을 때가 많다. 따라서 이름 변경과 매개변수 추가를 모두 하고 싶다면 각각을 독립적으로 처리하자(그러다 문제가 생기면 작업을 되돌리고 ‘마이그레이션 절차'를 따른다.)
마이그레이션 절차
- 이어지는 추출 단계를 수월하게 만들어야 한다면 함수의 본문을 적절히 리팩터링한다.
- 함수 본문을 새로운 함수로 추출한다. → 새로 만들 함수 이름이 기존 함수와 같다면 일단 검색하기 쉬운 이름을 임시로 붙여둔다.
- 추출한 함수에 매개변수를 추가해야 한다면 ‘간단한 절차'를 따라 추가한다.
- 테스트한다.
- 기존 함수를 인라인한다.
- 이름을 임시로 붙여뒀다면 함수 선언 바꾸기를 한 번 더 적용해서 원래 이름으로 되돌린다.
- 테스트한다.
다형성을 구현한 클래스, 즉 상속 구조 속에 있는 클래스의 메서드를 변경할 때는 다형 관계인 다른 클래스들에도 변경이 반영되어야 한다. 이때, 상황이 복잡하기 때문에 간접 호출 방식으로 우회(혹은 중간 단계로 활용) 하는 방법도 쓰인다. 먼저 원하는 형태의 메서드를 새로 만들어서 원래 함수를 호출하는 전달메서드로 활용하는 것이다.
단일 상속 구조라면 전달 메서드를 슈퍼클래스에 정의하면 해결된다.
예시: 함수 이름 바꾸기(간단한 절차)
function circum(radius) {
return 2 * Math.PI * radius;
}
리팩터링 후
function circumference(radius) {
return 2 * Math.PI * radius;
}
- 정적 타입 언어와 뛰어난 IDE의 조합이라면 함수 이름 바꾸기를 자동으로 처리할 수 있고, 그 과정에서 오류가 날 가능성도 거의 없다.
예시: 함수 이름 바꾸기(마이그레이션 절차)
function circum(radius) {
return 2 * Math.PI * radius;
}
[2] 먼저 함수 본문 전체를 새로운 함수로 추출한다.
리팩터링 후 1
function circum(radius) {
return circumference(radius);
}
function circumference(radius) {
return 2 * Math.PI * radius;
}
[4] 수정한 코드를 테스트 한 뒤 [5] 예전 함수를 인라인한다. 그러면 예전 함수를 호출하는 부분이 모두 새 함수를 호출하도록 바뀐다. [7] 하나를 변경할 때마다 테스트하면서 한 번에 하나씩 처리하자.
- 리팩터링 대상은 대부분 직접 수정할 수 있는 코드지만, 함수 선언 바꾸기만큼은 공개된 API, 다시 말해 직접 고칠 수 없는 외부 코드가 사용하는 부분을 리팩터링하기에 좋다.
예시: 매개변수 추가하기
상황: 도서 관리 프로그램의 Book 클래스에 예약 기능이 구현되어 있다고 하자.
addReservation(customer) {
this._reservations.push(customer);
}
추가 요구사항: 예약 시 우선순위 큐를 지원
addReservatiom(customer) {
this.zz_addReservation(customer);
}
zz_addReservation(customer) {
this._reservations.push(customer);
}
[2] 먼저 addReservation()의 본문을 새로운 함수로 추출한다.
addReservation(custoemr) {
this.zz_addReservation(customer, false);
}
zz_addReservation(customer, isPriority) {
this._reservations.push(customer);
}
[3] 그런 다음 새 함ㅅ두의 선언문과 호출문에 원하는 매개변수를 추가한다.
zz_addReservation(customer, isPriority) {
assert(isPriority === true || isPriority === false);
this._reservations.push(customer);
}
- 저자는 자바스크립트로 프로그래밍한다면, 호출문을 변경하기 전에 어서션을 추가하여 호출 하는 곳에서 새로 추가한 매개변수를 실제로 사용하는지 확인한다. 이렇게 해두면 호출문을 수정하는 과정에서 실수로 새 매개변수를 바뜨린 부분을 찾는 데 도움된다.
[5] 이제 기존 함수를 인라인하여 호출 코드들이 새 함수를 이용하도록 고친다. 호출문은 한번에 하나씩 변경한다.
[6] 다 고쳤다면 새 함수의 이름을 기존 함수의 이름으로 바꾼다.
예시: 매개변수를 속성으로 바꾸기
상황: 고객이 뉴잉글랜드에 살고 있는지 확인하는 함수가 있다고 하자.
function inNewEngland(aCustomer) {
return ["MA", "CT", "ME", "VT", "NH", "RI"].includes(aCustomer.address.state);
}
다음은 이 함수를 호출하는 코드 중 하나다.
// 호출문...
const newEnglanders = someCustomers.filter(c => inNewEngland(c));
inNewEngland() 함수는 고객이 거주하는 주 이름을 보고 뉴잉글랜드에 사는지 판단한다.
저자는 이 함수가 주 식별 코드를 매개변수로 받도록 리팩터링 할 것이라고 한다.
⇒ 그러면 고객에 대한 의존성이 제거되어 더 넓은 문맥에 활용할 수 있기 때문이다.
리팩터링 후 1
function inNewEngland(aCustomer) {
const stateCode = aCustomer.address.state;
return ["MA", "CT", "ME", "VT", "NH", "RI"].includes(stateCode);
}
[1] 저자는 함수 선언을 바꿀 때 함수 추출부터 하는 편이라고 한다.
하지만 이번 코드는 함수 본문을 살짝 리팩터링해두면 이후 작업이 더 수월해질 터라 우선 매개변수로 사용할 코드를 변수로 추출해둔다.
리팩터링 후 2
function inNewEngland(aCustomer) {
const stateCode = aCustomer.address.state;
return xxNEWinNewEngland(stateCode);
}
function xxNEWinNewEngland(stateCode) {
return ["MA", "CT", "ME", "VT", "NH", "RI"].includes(stateCode);
}
[2] 함수 추출하기로 새 함수를 만든 것
리팩터링 후 3
function inNewEngland(aCustomer) {
return xxNEWinNewEngland(aCustomer.address.state);
}
새 함수의 이름을 나중에 기존 함수 이름으로 바꾸기 쉽도록 검색하기 좋은 이름을 붙여둔다.
그런 다음 위와 같이 기존 함수 안에 변수로 추출해둔 입력 매개변수를 인라인한다.
// 호출문...
const newEnglanders = someCustomers.filter(c => xxNEWinNewEngland(c.address.state);
[5] 함수 인라인하기로 기존 함수의 본문을 호출문들에 집어넣는다. 실질적으로 기존 함수 호출문을 새 함수 호출문으로 교체하는 셈이다. 이 작업은 한 번에 하나씩 처리한다.
리팩터링 후 4
// 호출문...
const newEnglanders = someCustomers.filter(c => inNewEngland(c.address.state));
// 최상위...
function inNewEngland(stateCode) {
return ["MA", "CT", "ME", "VT", "NH", "RI"].includes(stateCode);
}
🤔 오늘 읽은 소감은? 떠오르는 생각을 가볍게 적어보세요.
- 생각해보면 자바 혹은 C#등 다양한 언어들에 이미 구현된 함수의 함수명은 상당히 이해하기 쉬웠습니다.
🔎 궁금한 내용이 있거나, 잘 이해되지 않는 내용이 있다면 적어보세요.
- 아직 리팩터링에 대해 모르는게 많다는 걸 알게되었고 이해가 안되는 부분이 꽤 있었습니다.