[ Programming > Refactoring ]
[리팩터링] 기능 이동
[리팩터링] 기능 이동
지금 까지는 프로그램 요소를 생성 혹은 제거하거나 이름을 변경하는 리팩터링을 했다. 그리고 리팩터링 저자는 여기에 더해 요소를 다른 컨텍스트 (클래스 또는 모듈)로 옮기는 일도 중요한 축이라 한다. 그래서 "기능 이동" 글에서는 함수 옮기기, 필드 옮기기와 같은 리팩터링 방법을 소개하려고 한다.
1. 기능 이동 리팩터링 방법
1.1 함수 옮기기
소프트웨어 핵심의 설계는 모듈성이다. 모듈성은 프로그램을 수정할 때 해당 기능의 작은 일부만 이해해도 가능하게 해주는 능력이다.
리팩터링 저자에 따르면 모듈성을 높이는 방법은 "서로 연관된 요소들을 함께 묶고, 요소 사이의 연결 관계를 쉡기 찾고 이해할 수 있도록 만들기"이다. 그리고 프로그램을 얼마나 이해했느냐에 따라서 모듈성을 더 잘 높일 가능성이 증가하며 요소들을 이리 저리 옮겨야할 수 도 있다.
함수 옮기는 Case
어떤 함수가 속한 모듈 A의 요소들보다 모듈 B의 요소들을 더 많이 참조한다면, 이 함수를 모듈 B로 옮기면 이 소프트웨어는 모듈 B의 세부사항에 덜 의존하게 된다. (캡슐화가 개선된다.)
도우미 역할로 정의된 함수 중 독립적으로 고유한 가치가 있는 것은 접근하기 더 쉬운 장소로 옮긴다. (또는 다음 업데이트 때 바뀌리라 예상되는 위치에 따라서 옮긴다.)
사실 함수를 옮길지 말지를 정하기는 쉽지 않다. 저자에 따르면 그럴 땐 대상 함수의 현재 컨텍스트와 후보 컨텍스트를 둘러보면 도움이 된다. 그리고 함수 이동 뿐만이 아니라 연관된 함수들은 클래스 묶기나 클래스 추출하기로 해결하기도 한다.
그리고 함수의 최적의 장소를 찾기 어려운 경우에는 한 컨텍스트에 두고 작업해보는 것도 좋다. 그러면 얼마나 적합한지 점점 깨달아 가고, 그 판단에 따라 언제든 옮길 수 있다. 그리고 이제 그 예시를 코드를 통해 알보도록 하겠다.
// 중첩 함수를 최상위로 옮기기// Before: calculateDistance 함수가 trackSummary 함수 내부에 중첩으로 선언되어 있다.// 목표: calculateDistance 함수를 최상위로 옮겨 추적 거리를 다른 정보와 독립적으로 계산하고 싶다.function trackSummary(points) { const totalTime = calculateTime(); const totalDistance = calculateDistance(); const pace = totalTime / 60 / totalDistance; return { time: totalTime, distance: totalDistance, pace: pace } function calculateDistance() { let result = 0; for (const i = 1; i < points.length; i++) { result += distance(points[i-1], points[i]); } return result; } function distance(p1, p2) { ... } function radians(degrees) { ... } function calculateTime() { ... } }// 목표 달성 후function trackSummary(points) { const totalTime = calculateTime(); const pace = totalTime / 60 / totalDistance(points); return { time: totalTime, distance: totalDistance(points), pace: pace };}function totalDistance(points) { ... } // totalDistance내부에 distance와 radians를 중첩 함수로 선언할 수도 있다.function distance(p1, p2) { ... }function radians(degrees) { ... }function calculateTime() { ... }
위 코드를 보면 중첩함수들을 최상위로 끌어올려 추적 거리에 대한 계산을 trackSummay 컨텍스트와 독립적으로 분리했다. 그리고 totalDistance 함수에서 distance와 radians 함수가 사용되는데, 가시성을 줄이기 위해 totalDistance 내부에 중첩 함수로 선언하는 것도 고려할 수 있다. 하지만 저자는 중첩 함수를 사용하다 보면 숨겨진 데이터끼리 상호 의존하기가 쉬우니 되도록 사용하지 말자고 주장한다.(그리고 언어에 따라 중첩 함수 선언에 대한 한계가 있어서, 언어에 따라 방법이 달라질 수 있다.)
이번에는 최상위 컨텍스트로 함수를 빼는 것이 아닌 다른 클래스로 옮기는 예시이다.
// 다른 클래스로 옮기기 예시// Before : Account에 계좌 종류에 따라 이자 책정 알고리즘이 달라지는 함수가 존재한다.// 목표 : 이자 책정 책임을 별도로 분리해보자.class Account { get bankCharge() { let result = 4.5; if (this._daysOverdrawn > 0) result += this.overdraftCharge; return result; } get overdraftCharge() { if(this.type.isPremium) { const baseCharge = 10; if(this.daysOverdrawn <= 7) { return baseCharge; } else { return baseCharge + (this.daysOverdrawn - 7) * 0.85; } } else { return this.daysOverdrawn * 1.75; } }}// After : AccountType 클래스로 이동class Account { get bankCharge() { let result = 4.5; if (this._daysOverdrawn > 0) result += this.type.overdraftCharge(this.daysOverdrawn); return result; } // 위임 메서드로 만들거면 get overdraftCharge 메소드를 둔다.}class AccountType { overdraftCharge(daysOverdrawn) { // daysOverdrawn은 계좌별로 달라지는 메서드이기 때문에 그대로 둔다. // 사용하는 컨텍스트가 많으면 daysOverdrawn이 아니라 account를 통째로 사용한다. if(this.isPremium) { const baseCharge = 10; if(daysOverdrawn <= 7) { return baseCharge; } else { return baseCharge + (daysOverdrawn - 7) * 0.85; } } else { return daysOverdrawn * 1.75; } }}
1.2 필드 옮기기
프로그램을 짜다보면 주어진 문제에 적합하지 않은 데이터 구조를 활용하여 데이터를 다루기 위한 코드로 범벅이 되는 경험을 해본적 있을 것이다. 그래서 이해하기 어려운 코드가 만들어지고 어떤 일을 하는지 파악하는데 방해가 된다.
그래서 데이터 구조가 중요하고 막상 적합한 데이터 구조를 만들어 내고자 할 때 어렵다. 적합한 데이터 구조를 만들기 위해서는 경험과 도메인 주도 설계같은 기술이 필요하다. 그리고 그런 기술을 가졌다고 해도 숙련자들도 프로젝트를 진행할수록 문제 도메인과 데이터 구조에 대해 더 많이 배우고 잘못된 것으로 판명하고 수정하기도 한다고 한다.
필드 옮기는 Case
함수에 어떤 레코드를 넘길 때마다 다른 레코드의 필드도 함께 넒기고 있는 경우
어떤 레코드를 변경할 때 다른 레코드의 필드까지 변경해야하는 경우
필드 옮기기 리팩터링은 보통 더 큰 변경의 일환으로 수행된다. 하나의 필드가 옮겨지면 그 필드를 사용하는 많은 코드가 옮겨진 위치에서 사용하는게 더 수월할 수 있기 때문이다. 하지만 옮기려는 데이터가 쓰이는 패턴 때문에 당장 필드를 옮길 수 없는 경우가 있다. 그럴 땐 사용 패턴을 먼저 리팩터링하고 필드를 옮겨야한다.
아래가 그 적용 예시다.
// Before : Customer 클래스에 discountRate이 존재.// 목표 : 할인율을 뜻하는 discountRate 필드를 Customer -> CustomerContract로 옮기기class Customer { constructor(name, discountRate) { this._name = name; this._discountRate = discountRate; this._contract = new CustomerContract(dateToday()); } get discountRate() { return this._discountRate; } becomePreferred() { this._discountRate += 0.03; // + 다른 일들 } applyDiscount(amount) { return amount.subtract(amount.multiply(this._discountRate)); } dateToday() { return new Date(); }}class CustomerContract { // 계약 클래스 constructor(startDate) { this._startDate = startDate; }}// After : discountRate필드를 CustomerContract로 이동시킨다.class Customer { constructor(name, discountRate) { this._name = name; this._contract = new CustomerContract(this.dateToday()); this._setDiscountRate(discountRate); // contract 클래스 호출 뒤 실행 } get discountRate() { return this._contract.discountRate; } setDiscountRate(number) { return this._contract.discountRate = number; } becomePreferred() { this._contract.discountRate += 0.03; // 등등 } applyDiscount(amount) { return amount.subtract(amount.multiply(this._contract.discountRate)); } dateToday() { return new Date(); }}class CustomerContract { constructor(startDate, discountRate) { this._startDate = startDate; this._discountRate = discountRate; } get discountRate() { return this._discountRate; } _setDiscountRate(aNumber) { // 접근 제어 캡슐, 타입스크립트면 굳이 aNumber로?? this._discountRate = aNumber; }}
필드를 옮기는데 그 특성에 따라서 접근 제어자를 수정하는 등 레코드 특성에 맞게 캡슐화를 적절히 해야한다.
다음은 공유 객체로 이동시키는 예시이다. 코드보다는 리팩터링에서 신경써야할 점이 무엇인지 알아가는 것이 좋을 것 같다.
// Before : Account의 이자가 Account에 따라 다르다// 목표 : AccountType에 따라 이자가 다르게 변경했으며 DB까지 일치하는지 확인하기.class Account { constructor(number, type, interestRate) { this._number = number; this._type = type; this._interestRate = interestRate; } get interestRate() {return this._interestRate;}}class AccountType { constructor(name) { this._name = name; }}// After : AccountType에 따라 이자가 결정되며, 겉보기 동작도 일치하는지 확인하며 만들어야 함.class Account { constructor(number, type, interestRate) { this._number = number; this._type = type; } get interestRate() {return this._type.interestRate;}}class AccountType { constructor(name, interestRate) { this._name = name; this._interestRate = interestRate; } get interestRate() {return this._interestRate;}}
위 코드는 DB에 당연히 각 계좌가 계좌 종류에따라 같은 이자율을 공유한다고 가정하고 만든 코드이다. 하지만 이전에는 계좌 별로 이자율이 달라졌는데 새롭게 계좌 종류별로 이자율이 같게 설정한다면 수정 전과 후의 겉보기 동작이 같은지 확인해야한다. 그래서 아래와 같은 중간 코드를 짜는게 도움이 될 수 있다.
class Account { constructor(number, type, interestRate) { this._number = number; this._type = type; assert(interestRate === this._type.interestRate); // 운영, 테스트 중에 오류 확인하여 겉보기 동작 일치 여부 확인 this._interestRate = interestRate; } get interestRate() {return this._interestRate;}}
1.3 문장을 함수로 옮기기
중복 제거는 코드를 건강하게 관리하는 효과적인 방법이다. 특정 함수를 호출하는 코드가 나올 때마다 그 앞이나 뒤에서 똑같은 코드가 추가로 실행되는 경우 그 반복되는 부분을 피호출 함수로 합치는 것이 좋다. 그러면 나중에 수정할 일이 생겼을 때 한 곳만 수정하면 된다. 물론 반대로 코드의 동작을 여러 변형들로 나눠야 하는 순간이 오면 문장을 호출한 곳으로 옮기면 된다.
문장들을 함수로 옮기려면 그 문장들이 피호출 함수의 일부라는 확신이 있어야 한다.
문장을 함수로 옮기는 Case
피호출 함수와 한 몸은 아니지만 여전히 함께 호출돼야 하는 경우 해당 문장과 피호출 함수를 통째로 또하나의 함수로 추출
아래는 그 예시로 사진 관련 데이터를 HTML로 내보내는 코드다.
// Before : "제목"은 사진 정보의 일부이며 emitPhotoData와 항상 함께 호출된다. // 목표 : 제목을 출력하는 코드를 emitPhotoData로 옮겨 중복을 제거한다.function renderPerson(person) { const result = []; result.push(`<p>${person.name}</p>`); result.push(renderPhoto(person.photo)); result.push(`<p>제목: ${person.photo.title}</p>`); result.push(emitPhotoData(person.photo)); return result.join("\n");}function photoDiv(aPhoto) { return [ '<div>', `<p>제목: ${p.title}</p>`, emitPhotoData(aPhoto), '</div>'].join("\n");}function emitPhotoData(aPhoto) { const result = []; result.push(`<p>위치: ${aPhoto.location}</p>`); result.push(`<p>날짜: ${aPhoto.date.toDateString()}</p>`); return result.join("\n");}// After : emitPhotoData에서 제목을 함께 출력하게 변경function renderPerson(person) { const result = []; result.push(`<p>${person.name}</p>`); result.push(renderPhoto(person.photo)); result.push(emitPhotoData(person.photo)); return result.join("\n");}function photoDiv(aPhoto) { return [ '<div>', emitPhotoData(aPhoto), '</div>'].join("\n");}function emitPhotoData(aPhoto) { return [ `<p>제목: ${aPhoto.title}</p>` `<p>위치: ${aPhoto.location}</p>` `<p>날짜: ${aPhoto.date.toDateString()}</p>` ].join("\n");}
1.4 문장을 호출한 곳으로 옮기기
함수는 프로그래머가 쌓아 올리는 추상화의 기본 빌딩 블록이다. 그런데 추상화라는 것은 그 경계를 항상 올바르게 긋기는 힘들다. 그래서 코드 베이스의 기능 범위가 달라지면 추상화의 경계도 움직인다. 초기에는 응집도 높고 단일 책임을 가진 함수가 어느새 둘 이상의 책임을 수행하게 바뀔 수도 있다.
문장을 호출한 곳으로 옮기는 Case
여러 곳에서 사용 하던 기능이 일부 호출자들에게는 다르게 동작해야하는 경우
그런데 위 변경이 작은 변경이라면 그냥 문장을 호출한 곳으로 옮기면 끝이지만, 호출자와 호출 대상의 경계를 완전히 다시 그어야 하는 경우도 생길 수 있다. 그러면 함수를 다시 인라인으로 만들고 적합한 경계를 그어야 한다.
아래 그 예시로 위치 정보를 다르게 렌더링 하도록 만들어야하는 경우이다.
// Before : photo의 location이 항상 같게 렌더링 된다.// 목표 : listRecentPhotos()가 위치 정보(location)을 다르게 렌더링해야 한다.function renderPerson(outStream, person) { outStream.write(`<p>${person.name}</p>\n`); renderPhoto(outStream, person.photo); emitPhotoData(outStream, person.photo);}function listRecentPhotos(outStream, photos) { photos .filter((p) => p.date > recentDateCutoff()) .forEach((p) => { outStream.write('<div>\n'); emitPhotoData(outStream, p); outStream.write('</div>\n'); });}function emitPhotoData(outStream, photo) { outStream.write(`<p>제목: ${photo.title}</p>\n`); outStream.write(`<p>날짜: ${photo.date.toDateString()}</p>\n`); outStream.write(`<p>위치: ${photo.location}</p>\n`);}// After : function renderPerson(outStream, person) { outStream.write(`<p>${person.name}</p>\n`); renderPhoto(outStream, person.photo); emitPhotoData(outStream, person.photo); outStream.write(`<p>위치: ${person.photo.location}</p>\n`) // 위치가 별도로 빠짐}function listRecentPhotos(outStream, photos) { photos .filter((p) => p.date > recentDateCutoff()) .forEach((p) => { outStream.write('<div>\n'); emitPhotoData(outStream, p); outStream.write(`<p>위치: ${p.location}</p>\n`); // 위치가 별도로 빠짐 outStream.write('</div>\n'); });}function emitPhotoData(outStream, photo) { outStream.write(`<p>제목: ${photo.title}</p>\n`); outStream.write(`<p>날짜: ${photo.date.toDateString()}</p>\n`);}
위 예시는 피호출자가 간단하고 호출자가 2개 뿐인 상황이다. 하지만 만약 호출자가 되게 많고 피호출자가 복잡하면 단계 별로 "새로운 피호출자에 남길 코드 추출하기" - "기존 피호출 함수 코드 호출자에 인라인 하기"와 같이 단계적으로 리팩터링 하는 것이 도움이 될 것이라 생각한다.
1.5 인라인 코드를 함수 호출로 바꾸기
이 리팩터링 방식이 함수 추출하기와 다른점은 인라인 코드를 대체할 함수가 이미 있느냐 없느냐이다. 이 방식은 인라인 코드를 대체할 함수가 있을경우 적용하는 예시다. 만약 없다면 함수 추출하기를 통해 새로운 함수를 만들고 함수를 대체할 것이다. 예시를 보면 더 확실히 무슨말을 하는지 알 것 같다.
// 리팩터링 전let appliesToMass = false;for(const s of states) { if (s === "MA") appliesToMass = true;}// 리팩터링 후const appliesToMass = states.includes("MA");
황당할 수도 있지만 굳이 말하고 싶은 점은 사용중인 프로그래밍 언어와 라이브러리 API를 잘 파악하고 있다면 코드를 개선하는데 더 수월하다는 것을 알려주고 싶다.
1.6 문장 슬라이드하기 (조건문 공통 실행코드 내빼기)
관련된 코드들이 가까이 모여 있다면 이해하기가 더 쉽다. 예를 들면 하나의 데이터 구조를 사용하는 문장들은 다른 데이터를 사용하는 문장 사이에 흩어져 있기 보다는 한데 모여있는게 좋다. 문장 슬라이드하기 리팩터링이 이런 코드들을 한데 모아둔다.
문장 슬라이드하기를 간단히 말하면 코드 위치를 정리하여 리팩터링 하게 편하고 이해하기 더 쉽게 만드는 것이다.
물론 모든 변수 선언을 함수 첫머리에 모아두는 사람도 있지만, 리팩터링 저자는 변수를 처음 사용할 때 선언하는 스타일을 선호한다.
문장 스라이드 주의점
코드 조각에서 참조하는 요소를 선언하는 문장 앞으로는 이동이 불가하다.
코드 조각을 참조하는 요소의 뒤로는 이동할 수 없다.
코드 조각에서 참조하는 요소를 수정하는 문장을 건너뛰어 이동할 수 없다.
코드 조각이 수정하는 요소를 참조하는 요소를 건너띄어 이동할 수 없다.
그리고 코드 조각을 슬라이드할 때는 두 가지를 확인해야한다. "무엇을 슬라이드"할 것인가와 "슬라이드 가능"한지 여부이다. "무엇을"은 맥락과 관련이 깊으며, 슬라이드 가능 여부도 코드 조각 순서에 따라 동작이 달라지는 지를 확인해야 한다. 다음은 예시이다.
// Before : 코드가 관심사에 상관없이 배치된 경우들이 있음. 중간에 어떤 과정이 추가되면 추적하기도 힘들것이다.// 목표 : 코드를 비슷한 관심사끼리 묶어보자const pricingPlan = retrievePricingPlan();const order = retreiveOrder();const baseCharge = pricingPlan.base;let charge;const chargePerUnit = pricingPlan.unit;const units = order.units;let discount;charge = baseCharge + units * chargePerUnit;let discountableUnits = Math.max(units - pricingPlan.discountThreshold, 0);discount = discountableUnits * pricingPlan.discountFactor;if(order.isRepeat) discount += 20;charge = charge - discount;chargeOrder(charge);// 9번 줄까지는 선언문으로 보인다. 그다음 줄부터는 요금 계산이 시작된다. 12번 줄에 는 discount를 계산한다.// 위와 같은 방식으로 한번 슬라이싱 해보자. 단 위치 변경이 가능한지 반드시 확인하면서 해야한다.// After : 관심사 끼리 묶어보자const pricingPlan = retrievePricingPlan();const order = retreiveOrder(); // 주문const units = order.units; // 주문 단위 수let discountableUnits = Math.max(units - pricingPlan.discountThreshold, 0); // 할인 가능 개수let discount; // 할인가discount = discountableUnits * pricingPlan.discountFactor; // 할인가 계산if(order.isRepeat) discount += 20; // 할인가 계산2const baseCharge = pricingPlan.base; // 요금 기본const chargePerUnit = pricingPlan.unit; // 요금 단위let charge; // 요금charge = baseCharge + units * chargePerUnit; // 요금 계산1charge = charge - discount;// 요금 계산2chargeOrder(charge);// 주문 요금 부과
위에서 주문, 할인, 요금 이렇게 3가지로 나눠서 슬라이싱했다. 저렇게 보니 그 다음 무엇을 어떻게 리팩터링해야할 지 더 명확해진다.(확실히 잘 보인다) 이제 합칠거 합치고 추출할 거 추출하면 훨씬 깔끔한 코드가 나올 것이다. 그리고 만약 슬라이싱이 가능한지 더 빠르게 판단하기 위해서는 "명령-질의 분리 원칙(Command-Query Separation)"을 지켜서 코드를 짜면 좋다.
다음은 조건문이 있을 경우 슬라이싱 예시이다. 조건문의 안밖으로 슬라이드해야할 경우가 있는데, 밖으로 슬라이드하면 중복 로직이 제거될 것이고, 조건문 안으로 슬라이드할 때는 반대로 중복 로직이 추가될 것이다.
// Before : allocatedResources.push가 조건문 마다 중복된다.// 목표 : 밖으로 슬라이싱 하여 중복을 제거해보자.let result;if (availableResources.length ===0) { result = createResource(); allocatedResources.push(result);} else { result = availableResources.pop(); allocatedResources.push(result);}return result;// After : 조건문마다 allocatedResouces.push가 여전히 실행되지만, 중복이 제거되었다.let result;if (availableResources.length ===0) { result = createResource();} else { result = availableResources.pop();}allocatedResources.push(result);return result;
1.7 반복문 쪼개기
종종 반복문 하나에서 두 가지 일을 수행하는 경우가 있다. 하지만 이렇게 하면 반복문을 수정해야 할 때마다 두 가지 일 모두 잘 이해하고 진행해야 한다. 반대로 각각의 반복문으로 분리해두면 수정할 동작 하나만 이해하면 된다. 이건 우선 예시를 보여주고 그 다음 의문에 대해서 말해보고자 한다.
// Before : 하나의 반복문에 2가지 일을 한다.// 목표 : 반복문을 쪼 개서 한 개의 반복문이 하나의 책임을 수행하게 해보자.let averageAge = 0;let totalSalary = 0;for (const p of people) { averageAge += p.age; totalSalary += p.salary;}averageAge = averageAge / people.length;// After : 하나의 반복문에 1가지 일을 처리한다.let totalSalary = 0;for (const p of people) { totalSalary += p.salary;}let averageAge = 0;for (const p of people) { averageAge += p.age;}averageAge = averageAge / people.length;
위 코드를 보면 관심사 별로 코드 조각이 존재하는 것을 볼 수 있다. 그러면 이 리팩터링을 매우 불편해하는 사람이 많을 것 같다. 왜냐하면 반복문을 2번 실행하여 병목 현상을 유발할 수 있기 때문이다(이 글을 쓰는 저도 이건 쫌...). 저자는 리팩터링과 최적화를 구분하라고 한다. 왜냐하면 최적화는 코드를 깔끔히 정리한 후에 적용하는 것이라 하기 때문이다. 그리고 깔끔하게 정리된 후에는 최적화로 두 for문을 합치는 것은 식은 죽 먹기라고 한다. 그리고 병목 현상으로 이어지는 경우가 드물기 때문에 오히려 반복문 쪼개기가 다른 더 강력한 최적화를 적용할 수 있는 길을 열어주기도 한다고 한다.
물론 나도 저자 의견에 동의하지만, 간단한 for문 코드라면 굳이 적용할 필요가 있을까 라는 생각이 든다. 그리고 저런 코드가 1개일 때는 병목이 없겠지만 "이곳 저곳에서 for문이 모두 분리되어 존재한다면 그 누적치가 병목으로 작용하지 않을까?"라는 생각도 든다.
물론 아래 예시를 보면 가독성이 상당히 좋다는 장점을 볼 수 있기는 하다.
// Before : 반복문 쪼개기 적용 전let youngest = people[0] ? people[0].age : Infinity;let totalSalary = 0;for (const p of people) { totalSalary += p.salary;}for (const p of people) { if (p.age < youngest) youngest = p.age;}return `youngestAge: ${youngest}, totalSalary: ${totalSalary}`;// After : 반복문 쪼개기에 youngestAge와 totalSalary 함수로 추출하기 까지 적용.return `youngestAge: ${youngestAge()}, totalSalary: ${totalSalary()}`;function totalSalary() { return people.reduce((total, p) => total + p.salary, 0);}function youngestAge() { return Math.min(...people.map(p => p.age));}
1.8 반복문을 파이프라인으로 바꾸기
객체 컬렉션을 순회할 때 반복문을 많이 사용할 것이다. 그런데 언어가 발전하면서 이러한 처리 과정을 함수형 프로그래밍으로 일련의 연산 과정으로 표현할 수 있다. 대초적인 컬렉션 함수가 map과 filter가 있다. 논리를 파이프라인으로 표현한다고 이해하면 훨씬 쉬워진다.
예시를 보면 대부분 무슨 말인지 아실 것 같다.
// Before : 단순 for문 사용// 목표 : 컬렉션 파이프라인을 이용하여 논리의 흐름 파악을 쉽게 만들어 보자.function acquireData(input) { const lines = input.split('\n'); let firstLine = true; const result = []; for (const line of lines) { // line 별 순회 if (firstLine) { firstLine = false; // 1번째 line이면 버리기 continue; } if (line.trim() === '') continue; // trim한 경우 빈 라인이면 버리기 const record = line.split(','); // ,을 구분자로 레코드 분리 if (record[1].trim() === 'India') { // India인 레코드면 결과에 저장 result.push({ city: record[0].trim(), phone: record[2].trim() }); // 저장된 레코드의 도시와 전화번호만 저장 } } return result;}// After : 내장 Array 함수 사용function acquireData(input) { const lines = input.split('\n'); return lines .slice (1) // 1번째 라인 버리기 .filter (line => line.trim() != '') // line을 트림한 경우 빈라인이면 버리기 .map (line => line.split(',')) // ,을 구분자로 레코드 분리 .filter (fields => fields[1].trim() === 'India') // 레코드가 India꺼면 저장 .map (fields => ({city: fields[0].trim(), phone: fields[2].trim()})); // 저장된 레코드의 도시와 전화번호만 저장}
위와 같이 파이프라이닝하여 무슨 논리로 컬렉션을 가공하는지 한눈에 볼 수 있다.
마무리
이번 장에서는 코드나 기능의 위치 변경에 대해서 이야기 했다. 다 예시들을 보면 시시할거라 생각한다. 그리고 무의식 중에 하고있는 리팩터링 방법도 많을 거라고 생각한다. 하지만 리팩터링 책에서는 리팩터링 방법의 절차에 따라 하나씩 설명해준다. 그리고 그 설명에서 각 단계에서 무엇을 고려해야할지 설명이 함께 있다. 그래서 그 고려해야할 점들을 한번 쯤 보고 느꼈으면 나중에 리팩터링할 때 더 도움이 될 것이라는 막연한 생각을 한다.