on
리팩토링 8장. 기능 이동에 대한 정리
‘리팩터링 2판 - 마틴 파울러’를 읽고 정리한 내용입니다.
8-1. 함수 옮기기
좋은 소프트웨어 설계의 핵심은 모듈화가 얼마나 잘 되어 있느냐다.
모듈성을 높이려면 서로 연관된 요소들을 함께 묶고, 요소 사이의 연결 관계를 쉽게 찾고 이해할 수 있도록 해야 한다.
모든 함수는 어떤 컨텍스트 안에 존재한다.
전역 함수도 있지만 대부분 특정 모듈에 속한다. (전역 함수도 전역 컨텍스트 안에 존재한다.)
어떤 함수가 자신이 속한 모듈 A의 요소들보다 다른 모듈 B의 요소들을 더 많이 참조한다면 모듈 B로 옮겨줘야 캡슐화가 좋아져서 나머지 부분들은 모듈 B의 세부사항에 덜 의존하게 된다.
호출자들의 현재 위치나, 다음 업데이트 때 바뀔 것이라고 예상되는 위치에 따라서도 함수를 옮겨야 할 수 있고, 다른 클래스로 옮겨두면 사용하기 더 편한 메서드도 있다.
함수를 옮길지 말지를 정하기란 쉽지 않은 데 그럴 땐 대상 함수의 현재 컨텍스트와 후보 컨텍스트를 둘러보면 도움이 된다.
- 대상함수를 호출하는 함수들은 무엇인지
- 대상 함수가 호출하는 함수들은 또 무엇이 있는 지
- 대상 함수가 사용하는 데이터는 무엇인지
위 3가지를 살펴봐야 한다.
함수 옮기기 절차
- 선택한 함수가 현재 컨텍스트에서 사용중인 모든 프로그램 요소를 살펴본다. 이 요소들 중에도 함께 옮겨야 할 게 있는 지 고민해본다.
- 선택한 함수가 다형 메서드 인지 확인한다.
- 선택한 함수를 타깃 컨텍스트로 복사한다. 타깃 함수가 새로운 터전에 잘 자리 잡도록 다듬는다.
- 정적 분석을 수행한다.
- 소스 컨텍스트에서 타깃 함수를 참조할 방법을 찾아 반영한다.
- 소스 함수를 타깃 함수의 위임 함수가 되도록 수정한다.
- 테스트 한다.
- 소스 함수를 인라인 할지 고민해본다.
class Account {
get overdraftCharge() { }
}
class AccountType {
get overdraftCharge() { }
}
8-2. 필드 옮기기
프로그램의 진짜 힘은 데이터 구조에서 나온다.
데이터 구조를 잘 못 선택하면 아귀가 맞지 않는 데이터를 다루기 위한 코드가 많아지고
이것은 이해하기 어려운 코드가 만들어지는 것 뿐 아니라 프로그램이 무슨 일을 하는 지 파악하기 어렵게 한다.
그래서 데이터 구조가 중요하지만 제대로 하기 어렵다.
오늘까지 올바른 코드가 다음주가 되면 잘못된 것으로 판명이 나기도 한다.
현재 데이터 구조가 적절하지 않음을 깨닫게 되면 곧바로 수정해야 한다.
예를 들어 함수에 어떤 레코드(혹은 클래스, 혹은 객체)를 넘길 때 마다 또 다른 레코드의 필드도 함께 넘기고 있다면 데이터 위치를 옮겨야 한다.
함수에 항상 함께 건네지는 데이터 조각들은 상호 관계가 명확하게 드러나도록 한 레코드에 담는 것이 좋다.
한 레코드를 변경하려고 할 때 다른 레코드의 필드까지 변경해야 한다면 필드의 위치가 잘못된 것이다.
필드 하나를 잘 옮기면 그 필드를 사용하던 많은 코드가 원래 위치보다 옮긴 위치에서 사용되는 것이 더 수월 할 수 있는 데 이처럼 필드 옮기기는 대체로 더 큰 변경의 일환으로 수행된다.
이럴 때는 호출 코드들까지도 모두 변경한다.
필드 옮기기 절차
- 소스 필드가 캡슐화되어 있지 않다면 캡슐화 한다.
- 테스트 한다.
- 타깃 객체에 필드(+ 접근자 매서드)를 생성한다.
- 정적 검사를 수행한다.
- 소스 객체에서 타깃 객체를 참조할 수 있는 지 확인한다.
- 접근자들이 타깃 필드를 사용하도록 수정한다.
- 테스트한다.
- 소스 필드를 제거한다.
- 테스트한다.
class Customer {
get plan() {
return this._plan;
}
get discountRate(){
return this._discountRate;
}
}
class Customer {
get plan() {
return this._plan;
}
get discountRate(){
return this.plan.discountRate;
}
}
8-3. 문장을 함수로 옮기기
중복 제거는 코드를 건강하게 관리하는 가장 효과적인 방법 중 하나다.
특정 함수를 호출할 때마다 그 앞이나 뒤에 같은 코드가 추가로 실행될 때 하나로 합쳐주고 추후 반복되는 부분에서 무언가 수정할 일이 생겼을 때 한곳만 수정하면 된다.
나중에 이 동작을 여러 예외처리를 해야 하는 순간이 오면 문장을 호출한 곳으로 옮기기를 적용해서 쉽게 뽑아낼 수 있다.
문장을 함수로 옮기기 위해서는 그 문장들이 피호출 함수의 일부라는 확신이 있어야 한다. 피호출 함수와 한몸은 아니지만 함께 호출되어야 한다면 해당 문장들과 피호출 함수를 통채로 하나의 함수로 추출한다.
문장을 함수로 옮기기 절차
- 반복 코드가 함수 호출 부분과 멀리 떨어져 있다면 문장 슬라이드 하기를 적용해 근처로 옮긴다.
- 타깃 함수를 호출하는 곳이 한 곳뿐이라면 단순히 소스 위치에서 잘라내어 피호출 함수로 복사하고 테스트한다. 이경우라면 나머지 단계는 무시한다.
- 호출자가 둘 이상이면 호출자 중 하나에서 타깃 함수 호출 부분과 그 함수로 옮기려는 문장들을 함께 다른 함수로 추출한다. 추출한 함수에는 기억하기 쉬운 임시 이름을 지어준다.
- 다른 호출자 모두가 방금 추출한 함수를 사용하도록 수정한다. 하나씩 수정할 때마다 테스트한다.
- 모든 호출자가 새로운 함수를 사용하게 되면 원래 함수를 새로운 함수 안으로 인라인한 후 원래 함수를 제거한다.
- 새로운 함수의 이름을 원래 함수의 이름으로 바꿔준다. (더 나은 이름이 있다면 수정한다.)
result.push(
`<p>제목: ${person.photo.title}</p>`
);
result.concat(
photoData(person.photo)
);
function photoData(aPhoto) {
return [
`<p>위치: ${aPhoto.location}</p>`, `<p>날짜: ${aPhoto.date.toDateString()}</p>`,
];
}
result.concat(
photoData(person.photo)
);
function photoData(aPhoto) {
return [
`<p>제목: ${aPhoto.title}</p>`,
`<p>위치: ${aPhoto.location}</p>`,
`<p>날짜: ${aPhoto.date.toDateString()}</p>`,
];
}
8-4. 문장을 호출한 곳으로 옮기기
함수는 프로그래머가 쌓아 올리는 추상화의 기본 빌딩 블록이다.
하지만 그 추상화라는 것이 그 경계가 애매하다. 그래서 코드베이스의 기능 범위가 달라지면 추상화의 경계도 움직이게 된다.
함수의 관점에서 보면 초기에는 응집도가 높고 한가지 일만 수행하던 함수가 어느새 둘 이상의 다른 일을 수행하게 바뀔 수 있다는 것이다.
달라지는 동작의 함수를 시작 혹은 끝으로 옮긴 다음, 문장을 호출한 곳으로 옮기기 리팩터링을 적용하면 필요할 때마다 독립적으로 수정할 수 있다.
문장을 호출한 곳으로 옮기기 절차
- 호출자가 한두 개 뿐이고 피호출 함수도 단순한 상황이면, 피호출 함수의 처음(혹은 마지막)줄을 잘라내어 호출자로 복사해 넣는다. 테스트만 통과하면 여기서 끝이다.
- 더 복잡한 상황에서는 이동하지 않길 원하는 모든 문장을 함수로 추출한 다음, 검색하기 쉬운 이름으로 지어준다.
- 원래 함수를 인라인한다.
- 추출된 함수의 이름을 원래 함수의 이름으로 변경한다. (더 나은 이름이 있다면 수정한다.)
emitPhotoData(outStream, person.photo);
function emitPhotoData(outStream, photo) {
outStream.write(`<p>제목: ${photo.title}</p>\n`);
outStream.write(`<p>위치: ${photo.location}</p>\n`);
}
emitPhotoData(outStream, person.photo);
outStream.write(`<p>위치: ${person.photo.location}</p>\n`);
function emitPhotoData(outStream, photo) {
outStream.write(`<p>제목: ${photo.title}</p>\n`);
}
8-5. 인라인 코드를 함수 호출로 바꾸기
함수는 여러 동작을 하나로 묶어준다. 그리고 함수의 이름이 코드의 동작 방식보다는 목적을 말해주기 때문에 함수를 활용하면 코드를 이해하기 쉬워진다.
또 함수는 중복을 없애는 데도 효과적이다. 중복을 없애면 여러번 수정하지 않고 하나의 코드만 수정하면 된다.
인라인 코드를 함수 호출로 바꾸기 절차
- 인라인 코드를 함수로 추출한다.
- 테스트한다.
함수 추출하기와 인라인 코드를 함수 호출로 바꾸기의 차이는 인라인 코드를 대체할 함수가 이미 존재하느냐 여부이다.
함수를 새로 만들어야 한다면 함수 추출하기를 적용하고 이미 존재한다면 인라인 코드를 함수로 바꿔 적용하면 된다.
let appliesToMass = false;
for(const s of states) {
if (s === "MA") appliesToMass = true;
}
appliesToMass = states.includes("MA");
8-6. 문장 슬라이드하기
관련 코드들이 가까이 모여 있다면 이해하기가 더 쉽다.
하나의 데이터 구조를 이용하는 문장들은 흩어져 있는 것 보다 모여있어야 좋다.
관련 코드끼리 모으는 작업은 함수 추출하기 같은 다른 리팩터링의 준비 단계로 자주 행해진다.
문장 슬라이드 하기 절차
- 코드 조각(문장들)을 이동할 목표 위치를 찾는다. 코드 조각의 원래 위치와 목표 위치 사이의 코드들을 훎어보면서 조각을 모으고 나면 동작이 달라지는 코드가 있는 지 살핀다.
- 코드조각을 원래 위치에서 잘라내어 목표 위치에 붙여 넣는다.
- 테스트 한다.
테스트가 실패한다면 더 작게 나눠서 시도해본다.
코드 조각을 슬라이드할 때는 1. 무엇을 슬라이드 할지, 2. 슬라이드 할 수 있는 지 두가지를 확인해야 한다.
다음과 같은 간섭이 있다면 이 리팩터링을 포기한다.
- 코드 조각에서 참조하는 요소를 선언하는 문장 앞으로 이동할 수 없다.
- 코드 조각을 참조하는 요소의 뒤로 옮길 수 없다.
- 코드 조각에서 참조하는 요소를 수정하는 문장을 건너뛰어 이동할 수 없다.
- 코드 조각이 수정하는 요소를 참조하는 요소를 건너뛰어 이동할 수 없다.
문장 교환하기라는 이름의 거의 같은 리팩터링도 있다. 문장 교환하기는 인접한 코드 조각을 이동하지만 문장 하나짜리 조각만 취급한다.
큰 슬라이드를 수행하기 어려울 때 한 문장씩 이동한다.
const pricingPlan = retrievePricingPlan();
const order = retreiveOrder();
let charge;
const chargePerUnit = pricingPlan.unit;
const pricingPlan = retrievePricingPlan();
const chargePerUnit = pricingPlan.unit;
const order = retreiveOrder();
let charge;
8-7. 반복문 쪼개기
종종 반복문 하나에서 두가지 일을 수행하는 모습을 볼 수 있는 데 이렇게 하면 반복문을 수정할 때 마다 두가지 일 모두를 이해하고 진행하고 테스트 해야 한다.
이것을 각각의 반복문으로 분리해두면 수정할 동작 하나만 이해하면 된다.
반복문을 분리하면 사용하기도 쉬워진다.
하지만 반복문을 두번 실행해야 하므로 이 리팩터링을 불편해하는 프로그래머도 많은 데 리팩터링과 최적화를 구분해야 한다.
최적화는 코드를 깔끔히 정리 한 후 수행해주는 것이 더 수월하다.
반복문 쪼개기 절차
- 반복문을 복제해 두 개로 만든다.
- 반복문이 중복되어 생기는 부수효과를 파악해서 제거한다.
- 테스트한다.
- 반복문을 함수로 추출할기 고민한다. 반복문 쪼개기의 묘미는 그 다음단계로 넘어가는 디딤돌 역할에 있다.
let totalSalary = 0;
for (const p of people) {
totalSalary += p.salary;
}
let averageAge = 0;
let totalSalary = 0;
for (const p of people) {
averageAge += p.age;
totalSalary += p.salary;
}
averageAge = averageAge / people.length;
let averageAge = 0;
for (const p of people) {
averageAge += p.age;
}
averageAge = averageAge / people.length;
8-8. 반복문을 파이프 라인으로 바꾸기
컬렉션 파이프 라인을 이용하면 처리 과정을 일련의 연산으로 표현할 수 있다.
대표적인 연산은 map
과 filter
다.
논리를 파이프라인으로 표현하면 이해하기 월씬 쉬워진다. 객체가 파이프라인을 따라 흐르며 어떻게 처리되는 지를 읽을 수 있기 때문이다.
반복문을 파이프라인으로 바꾸기 절차
- 반복문에서 사용하는 컬렉션을 가리키는 변수를 하나 만든다.
- 반복문의 첫 줄부터 시작해서 각각의 단위 행위를 적절한 컬렉션 파이프라인 연산으로 대체한다. 이때 파이프라인 연산은 1.에서 만든 컬렉션 변수에서 시작하고, 이전 연산의 결과를 기초로 연쇄적으로 수행한다. (하나씩 테스트 한다.)
- 반복문의 모든 동작을 대체했다면 반복문을 삭제한다.
const names = [];
for (const i of input) {
if (i.job === "programmer") names.push(i.name);
}
const names = input.filter(i => i.job === "programmer").map(i => i.name) ;
8-9. 죽은 코드 제거하기
쓰이지 않는 코드가 몇줄 있다고 해서 시스템의 성능에 영향을 주는 것은 아니다.
하지면 코드의 동작을 이해하기 위해, 그리고 코드를 수정했는 데도 기대한 결과가 나오지 않는 이유를 파악하기 위해 시간을 허비하게 되면서 큰 걸림돌이 될 수 있다.
한때는 죽은 코드를 주석 처리하는 방법이 널리 쓰였지만 버전 관리 시스템을 사용하는 요즘은 더이상 필요하지 않다.
죽은 코드 제거하기 절차
- 죽은 코드를 외부에서 참조할 수 있는 경우라면(함수가 통채로 죽었을 때) 혹시라도 호출하는 곳이 있는 지 확인한다.
- 없다면 죽은 코드를 제거한다.
- 테스트 한다.
Discussion and feedback