리팩터링 2판의 Chatper 11를 보고 정리한 글입니다.
모듈과 함수는 소프트웨어를 구성하는 빌딩 블록이며, API는 이 블록들을 끼워 맞추는 연결부이다. 따라서 API를 이해하기 쉽고 사용하기 쉽게 만드는 일은 중요한 일이다. 이러한 API관련 리팩터링 기법들을 알아보자.
값을 반환하면서도 부수효과가 있는 함수를 발견하면 상태를 변경하는 부분과 질의하는 부분을 분리해보자.
리팩터링 전
이름 목록을 훑어 악당을 찾아 악당을 찾으면 그 사람의 이름을 반환하고 경고를 울리는 함수
function alertForMiscreant(people) {
for (const p of people) {
if (p === '조커') {
setOffAlarms();
return '조커';
}
if (p === '사루만') {
setOffAlarms();
return '사루만';
}
}
return '';
}
// 호출하는 쪽 코드
const found = alertForMiscreant(people);
리팩터링 후
함수를 복제하고 질의 목적에 맞는 이름을 지은후, 부수효과를 낳는 부분을 제거한다. 원래 함수를 호출하는 곳을 모두 찾아서 새로운 질의 함수로 바꾸고 변경 함수 호출 코드를 바로 아래에 삽입하자.
function findMiscreant(people) {
for (const p of people) {
if (p === '조커') {
return '조커';
}
if (p === '사루만') {
return '사루만';
}
}
return '';
}
function alertForMiscreant(people) {
for (const p of people) {
if (p === '조커') {
setOffAlarms();
return;
}
if (p === '사루만') {
setOffAlarms();
return;
}
}
}
// 호출하는 쪽 코드
const found = findMiscreant(people);
alertForMiscreant(people);
// 여기서 alertForMiscreant 함수를 더 가다듬으면 다음과 같이 만들 수 있다.
function alertForMiscreant(people) {
if (findMiscreant(people) !== '') setOffAlarams();
}
두 함수의 로직이 비슷하고 단지 리터럴 값만 다르다면 그 다른 값만 매개변수로 받아 처리하는 함수 하나로 합쳐서 중복을 없앨 수 있다.
직관적인 경우
리팩터링 전
function tenPercentRaise(aPerson) {
aPerson.salary = aPerson.salary.multiply(1.1);
}
function fivePercentRaise(aPerson) {
aPerson.salary = aPerson.salary.multiply(1.05);
}
리팩터링 후
function raise(aPerson, factor) {
aPerson.salary = aPerson.salary.multiply(1 + factor);
}
덜 직관적인 경우
리팩터링 전
function baseCharge(usage) {
if (usage < 0) return usd(0);
const amount = bottomBand(usage) * 0.03 + middleBand(usage) * 0.05 + topBand(usage) * 0.07;
return usd(amount);
}
function bottomBand(usage) {
return Math.min(usage, 100);
}
function middleBand(usage) {
return usage > 100 ? Math.min(usage, 200) - 100 : 0;
}
function topBand(usage) {
return usage > 200 ? usage - 200 : 0;
}
리팩터링 후
// withinBand라는 3가지의 매개변수를 받는 함수를 만들어 사용!
function withinBand(usage, bottom, top) {
return usage > bottom ? Math.min(usage, top) - bottom : 0;
}
// 하나의 함수로 리팩터링 완료
function baseCharge(usage) {
if (usage < 0) return usd(0);
const amount = withinBand(usage, 0, 100) * 0.03
+ withinBand(usage, 100, 200) * 0.05
+ withinBand(usage, 200, Infinity) * 0.07;
return usd(amount);
}
플래그 인수란 호출되는 함수가 실행한 로직을 호출하는 쪽에서 선택하기 위해 전달하는 인수다. 그러나 이러한 인수가 있으면 함수들의 기능 차이가 함수 목록에서 드러나지 않는다.
특정한 기능 하나만 수행하는 명시적인 함수를 제공하는 편이 훨씬 깔끔하다.
리팩터링 전
배송일자를 계산하는 코드
// 호출하는 쪽
// boolean 값이 뭘 의미하는지라는 의문이 떠오름
aShipment.deliveryDate = deliveryDate(anOrder, true);
aShipment.deliveryDate = deliveryDate(anOrder, false);
function deliveryDate(anOrder, isRush) {
if (isRush) {
let deliveryTime;
if (['MA', 'CT'].includes(anOrder.deliveryState)) deliveryTime = 1;
else if (['NY', 'NH'].includes(anOrder.deliveryState)) deliveryTime = 2;
else deliveryTime = 3;
return anOrder.placedOn.plusDays(1 + deliveryTime);
}
let deliveryTime;
if (['MA', 'CT', 'NY'].includes(anOrder.deliveryState)) deliveryTime = 2;
else if (['ME', 'NH'].includes(anOrder.deliveryState)) deliveryTime = 3;
else deliveryTime = 4;
return anOrder.placeOn.plusDays(2 + deliveryTime);
}
전형적인 플래그 인수를 사용하고 있는 코드
리팩터링 후
// 명시적인 함수가 호출자의 의도를 더 잘 드러낸다.
aShipment.deliveryDate = rushDeliveryDate(anOrder);
aShipment.deliveryDate = regularDeliveryDate(anOrder);
function rushDeliveryDate(anOrder) {
let deliveryTime;
if (['MA', 'CT'].includes(anOrder.deliveryState)) deliveryTime = 1;
else if (['NY', 'NH'].includes(anOrder.deliveryState)) deliveryTime = 2;
else deliveryTime = 3;
return anOrder.placedOn.plusDays(1 + deliveryTime);
}
function regularDeliveryDate(anOrder) {
let deliveryTime;
if (['MA', 'CT', 'NY'].includes(anOrder.deliveryState)) deliveryTime = 2;
else if (['ME', 'NH'].includes(anOrder.deliveryState)) deliveryTime = 3;
else deliveryTime = 4;
return anOrder.placeOn.plusDays(2 + deliveryTime);
}
앞 예시와 달리 매개변수가 훨씬 까다로운 방식(if문안의 중첩 조건으로 사용)일 땐, 그 함수를 래핑하는 식으로 하여 앞에서 호출하는 코드들을 작성하고 뒤에 이어서 매개변수를 사용하는 부분만 함수를 만들어 리팩터링을 진행할 수 있다.
하나의 레코드에서 값 두어개를 인수로 넘기는 경우, 그 값들 대신 레코드를 통째로 넘기고 함수 본문에서 필요한 값들을 꺼내 쓰도록 수정할 수 있다.
실내온도 모니터링 시스템(일일 최저,최고 기온이 난방 계획에서 정한 범위를 벗어나는지 확인)
리팩터링 전
const { low } = aRoom.daysTempRange;
const { high } = aRoom.daysTempRange;
if (!aPlan.withinRange(low, high)) alerts.push('room temperature went outside range');
class HeatingPlan {
withinRange(bottom, top) {
return bottom >= this._temperatureRange.low && top <= this._temperatureRange.high;
}
}
리팩터링 후
class HeatingPlan {
withinRange(aNumberRange) {
return aNumberRange.low >= this._temperatureRange.low && aNumberRange.high <= this._temperatureRange.high;
}
}
if (!aPlan.withinRange(aRoom.daysTempRange)) alerts.push('room temperature went outside range');
반대 리팩터링: 질의 함수를 매개변수로 바꾸기
매개변수 목록은 함수의 변동 요인을 모아놓은 곳이기 때문에 중복은 피하는 것이 좋으며 짧을수록 이해하기 쉽다. 따라서 매개변수를 줄이고, 책임 소재를 피호출 함수로 옮겨 호출하는 쪽에서의 책임 주체를 간소하게 만들자.
→ 새로운 의존성이 생기거나 제거하고 싶은 기존 의존성을 강화하는 경우 매개변수를 질의 함수로 바꾸지 말아야 함.
리팩터링 전
class Order {
get finalPrice() {
const basePrice = this.quantity * this.itemPrice;
let discountLevel;
if (this.quantity > 100) discountLevel = 2;
else discountLevel = 1;
return this.discountedPrice(basePrice, discountLevel);
}
discountedPrice(basePrice, discountLevel) {
switch (discountLevel) {
case 1:
return basePrice * 0.95;
case 2:
return basePrice * 0.9;
}
}
}
리팩터링 후
class Order {
// 기존 로직 변경
get finalPrice() {
const basePrice = this.quantity * this.itemPrice;
return this.discountedPrice(basePrice);
}
// 기존 매개변수 삭제
discountedPrice(basePrice) {
switch (this.discountLevel) {
case 1:
return basePrice * 0.95;
case 2:
return basePrice * 0.9;
}
}
// 질의 함수 생성
get discountLevel() {
return this.quantity > 100 ? 2 : 1;
}
}
필요할 때 직접 호출함으로써, 매개변수를 줄일 수 있다.
반대 리팩터링: 매개변수를 질의 함수로 바꾸기
코드를 보면 전역 변수를 참조하거나 제거하길 원하는 원소를 참조하는 경우와 같이 함수 안에 두기엔 거북한 참조를 발견할 때가 있다. 이 경우 매개변수로 바꾸어 참조를 풀어내는 책임을 호출자로 옮겨보자.
→ 질의 함수를 매개변수로 바꾸면 어떤 값을 제공할지를 호출자가 알아내야하므로 호출자가 복잡해지는데 결국 이 문제는 책임 소재를 프로그램의 어디에 배정하느냐의 문제로 귀결된다.(답이 없는 문제)
실내 온도 제어시스템으로 사용자는 온도조절기(thermostat)로 온도를 설정하지만 목표 온도는 난방 계획에서 정한 범위에서만 선택!
리팩터링 전
class HeatingPlan {
get targetTemperature() {
if (thermostat.selectedTemperature > this._max) return this._max;
if (thermostat.selectedTemperature < this._min) return this._min;
return thermostat.selectedTemperature;
}
}
if (thePlan.targetTemperature > thermostat.currentTemperature) {
setToHeat();
} else if (thePlan.targetTemperature < thermostat.currentTemperature) {
setToCool();
} else {
setOff();
}
현재 thermostat이라는 전역 객체에 의존하고 있음. 이를 리팩터링 해보자.
리팩터링 후
class HeatingPlan {
targetTemperature(selectedTemperature) {
if (selectedTemperature > this._max) return this._max;
if (selectedTemperature < this._min) return this._min;
return selectedTemperature;
}
}
if (thePlan.targetTemperature(thermostat.selectedTemperature) > thermostat.currentTemperature) {
setToHeat();
} else if (thePlan.targetTemperature(thermostat.selectedTemperature) < thermostat.currentTemperature) {
setToCool();
} else {
setOff();
}
매개 변수를 받아 다음과 같이 리팩터링을 할 수 있게 되었다.
또한 클래스는 불변이 되어 모든 필드가 생성자에서 설정되며 필드를 변경할 수 있는 메서드는 존재하지 않게되었다.
→ 테스트하고 다루기 쉬워진다는 장점을 가짐
세터 제거하기 리팩터링이 필요한 상황 2가지
간단한 사람 클래스
리팩터링 전
class Person {
get name() {
return this._name;
}
set name(value) {
this._name = value;
}
get id() {
return this._id;
}
set id(value) {
this._id = value;
}
}
const martin = new Person();
martin.name = 'Martin';
martin.id = 1;
id는 변경되며 안된다는 의도를 명확히 알리기 위해 ID 세터를 없애보자.
리팩터링 후
최초 한번은 ID를 설정할 수 있도록 생성자에서 ID를 받도록 해보자.
이후 생성 스크립트가 이 생성자를 통해 ID를 설정하게끔 수정하고, 모두 수정했다면 세터 메서드를 인라인 한다.
class Person {
constructor(id) {
this._id = id;
}
get name() {
return this._name;
}
set name(value) {
this._name = value;
}
get id() {
return this._id;
}
}
const martin = new Person('1');
martin.name = 'Martin';
생성자는 객체를 초기화하는 특별한 용도의 함수이다. 그러나 생성자에는 일반 함수에는 없는 이상한 제약이 따라붙기도하여 이를 사용하기보다 팩터리 함수를 사용하여 제약을 없애보자.
직원 유형을 다루는, 간단하지만 이상한 예
리팩터링 전
class Employee {
constructor(name, typeCode) {
this._name = name;
this._typeCode = typeCode;
}
get name() {
return this._name;
}
get type() {
return Employee.legalTypeCodes[this._typeCode];
}
static get legalTypeCodes() {
return { E: 'Engineer', M: 'Manager', S: 'Salesperson' };
}
}
// 호출자
candidate = new Employee(document.name, document.empType);
const leadEngineer = new Employee(document.leadEngineer, 'E');
팩터리 함수를 만들고, 생성자를 호출하는 곳을 찾아 수정하자.
리팩터링 후
function createEmployee(name, typeCode) {
return new Employee(name, typeCode);
}
candidate = createEmployee(document.name, document.empType);
// 호출하는 부분의 리드 엔지니어 같은 경우는 문자열 리터럴을 건네는 방식이므로 좋지않고
// 차라리 createEnginner라는 형식으로 만든다.
function createEngineer(name) {
return new Employee(name, 'E');
}
const leadEngineer = createEngineer(document.leadEngineer);
반대 리팩터링: 명령을 함수로 바꾸기
함수는 프로그래밍의 기본적인 빌딩 블록 중 하나다. 그런데 함수를 그 함수만을 위한 객체 안으로 캡슐화하면 더 유용해지는 상황이 있다. 이런 객체를 가리켜 '명령 객체' 혹은 단순히 명령이라 한다.
건강보험 애플리케이션에서 사용하는 점수 계산 함수다.
리팩터링 전
function score(candidate, medicalExam, scoringGuide) {
let result = 0;
let healthLevel = 0;
let highMedicalRiskFlag = false;
if (medicalExam.isSmoker) {
healthLevel += 10;
highMedicalRiskFlag = true;
}
let certificationGrade = 'regular';
if (scoringGuide.stateWithLowCertification(candidate.originState)) {
certificationGrade = 'low';
result -= 5;
}
// lots more code like this
result -= Math.max(healthLevel - 5, 0);
return result;
}
리팩터링 후
빈 클래스를 만들고, 함수를 클래스로 옮겨보자.
function score(candidate, medicalExam, scoringGuide) {
return new Scorer().execute(candidate, medicalExam, scoringGuide);
}
class Scorer {
execute(candidate, medicalExam, scoringGuide) {
let result = 0;
let healthLevel = 0;
let highMedicalRiskFlag = false;
if (medicalExam.isSmoker) {
healthLevel += 10;
highMedicalRiskFlag = true;
}
let certificationGrade = 'regular';
if (scoringGuide.stateWithLowCertification(candidate.originState)) {
certificationGrade = 'low';
result -= 5;
}
// lots more code like this
result -= Math.max(healthLevel - 5, 0);
return result;
}
}
이후 execute() 메서드가 매개변수를 받지 않게 생성자쪽으로 옮겨보자.
function score(candidate, medicalExam, scoringGuide) {
return new Scorer(candidate, medicalExam, scoringGuide).execute();
}
class Scorer {
constructor(candidate, medicalExam, scoringGuide) {
this._candidate = candidate;
this._medicalExam = medicalExam;
this._scoringGuide = scoringGuide;
}
execute() {
let result = 0;
let healthLevel = 0;
let highMedicalRiskFlag = false;
if (this._medicalExam.isSmoker) {
healthLevel += 10;
highMedicalRiskFlag = true;
}
let certificationGrade = 'regular';
if (this._scoringGuide.stateWithLowCertification(this._candidate.originState)) {
certificationGrade = 'low';
result -= 5;
}
// lots more code like this
result -= Math.max(healthLevel - 5, 0);
return result;
}
}
이후 더 가다듬기에서 지역 변수를 필드로 바꾸고, 중첩 함수로 분리하여 리팩터링이 더 진행된다.
반대 리팩터링: 함수를 명령으로 바꾸기
로직이 크게 복잡하지 않다면 명령 객체는 장점보다 단점이 크니 평범한 함수로 바꿔주는게 낫다.
리팩터링 전
class ChargeCalculator {
constructor(customer, usage, provider) {
this._customer = customer;
this._usage = usage;
this._provider = provider;
}
get baseCharge() {
return this._customer.baseRate * this._usage;
}
get charge() {
return this.baseCharge + this._provider.connectionCharge;
}
}
// 호출자
monthCharge = new ChargeCalculator(customer, usage, provider).charge;
이 명령 클래스는 간단하므로 함수로 대체해보자.
리팩터링 후
이 클래스를 생성하고 호출하는 코드를 함께 함수로 추출한다. 또한 값을 반환하는 메서드라면 먼저 반환할 값을 변수로 추출한다.
class ChargeCalculator {
constructor(customer, usage, provider) {
this._customer = customer;
this._usage = usage;
this._provider = provider;
}
get charge() {
const baseCharge = this._customer.baseRate * this._usage;
return baseCharge + this._provider.connectionCharge;
}
}
function charge(customer, usage, provider) {
return new ChargeCalculator(customer, usage, provider).charge;
}
monthCharge = charge(customer, usage, provider);
최종 코드는 다음과 같다.
function charge(customer, usage, provider) {
const baseCharge = customer.baseRate * usage;
return baseCharge + provider.connectionCharge;
}
// 호출자
monthCharge = charge(customer, usage, provider);
데이터가 어떻게 수정되는지를 추적하는 일은 코드에서 이해하기 가장 어려운 부분 중 하나다. 따라서 데이터가 수정된다면 그 사실을 명확히 알려주어서, 어느 함수가 무슨 일을 하는지 쉽게 알 수 있게 하는 일이 중요하다.
→ 좋은 방법으로, 변수를 갱신하는 함수라면 수정된 값을 반환하여 호출자가 그 값을 변수에 담도록 하는 것이다.
GPS 위치 목록으로 다양한 계산을 수행하는 코드
리팩터링 전
let totalAscent = 0;
let totalTime = 0;
let totalDistance = 0;
calculateAcent();
calculateTime();
calculateDistance();
const pace = totalTime / 60 / totalDistance;
function calculateAscent() {
for (let i = 1; i < points.length; i++) {
const verticalChange = points[i].elevation - points[i - 1].elevation;
totalAscent += verticalChange > 0 ? verticalChange : 0;
}
}
calculateAscent() 안에서 totalAscent가 갱신된다는 사실이 드러나지 않음
→ 밖으로 알려보자.
리팩터링 후
totalAscent 값을 반환하고, 호출한 곳에서 변수에 대입하게 고친다.
const totalAscent = calculateAscent();
const totalTime = calculateTime();
const totalDistance = calculateDistance();
const pace = totalTime / 60 / totalDistance;
function calculateAscent() {
let result = 0;
for (let i = 1; i < points.length; i++) {
const verticalChange = points[i].elevation - points[i - 1].elevation;
result += verticalChange > 0 ? verticalChange : 0;
}
return result;
}
function calculateTime() {...}
function calculateDistance() {...}
위 처럼 코드가 바뀌게 된다.
예외는 프로그래밍 언어에서 제공하는 독립적인 오류 처리 메커니즘이다. 오류가 발견되면 예외를 던진다. 이러한 예외를 사용하면 오류 코드를 일일이 검사하거나 오류를 식별해 콜스택 위로 던지는 일을 신경쓰지 않아도 된다.
프로그램의 정상 동작 범주에 들지 않는 오류를 나타낼 때만 이러한 예외를 사용하자.
전역 테이블에서 배송지의 배송 규칙을 알아내는 코드
리팩터링 전
function localShippingRules(country) {
const data = countryData.shippingRules[country];
if (data) return new ShippingRules(data);
else return -23;
}
function calculateShippingCosts(anOrder) {
const shippingRules = localShippingRules(anOrder.country);
if (shippingRules < 0) return shippingRules; // 오류 전파
}
// 최상위
const status = calculateShippingCosts(orderData);
if (status < 0) errorList.push({order: orderData, errorCode: status});
이 경우 앞서 country(국가 정보)가 유효한지를 함수 호출전에 다 검증했다고 가정하였을 때, 위 코드들이 예상할 수 있는 정상 동작 범주안에 든다면 오류 코드를 예외로 바꾸는 리팩터링을 적용할 준비가 된것이다.
리팩터링 후
function localShippingRules(country) {
const data = countryData.shippingRules[country];
if (data) return new ShippingRules(data);
throw new OrderProcessingError(-23);
}
function calculateShippingCosts(anOrder) {
const shippingRules = localShippingRules(anOrder.country);
if (shippingRules < 0) throw new Error('오류 코드가 다 사라지지 않음');
}
try {
calculateShippingCosts(orderData);
} catch (e) {
if (e instanceof OrderProcessingError) {
errorList.push({ order: orderData, errorCode: e.code });
} else {
throw e;
}
}
저자는 다른 예외와 구별을 위해 별도의 클래스(OrderProcessingError)를 만들어 처리하는것을 좋아하여 위와같이 코드를 작성한다.
예외는 '뜻밖의 오류'라는, 말 그대로 예외적으로 동작할 때만 쓰여야한다. 함수 수행 시 문제가 될 수 있는 조건을 함수 호출 전에 검사할 수 있다면, 예외를 던지는 대신 호출하는 곳에서 조건을 검사하도록 해야 한다.
책의 예시론 자바코드로 자원들을 관리하는 과정에서 자원이 바닥나는 경우, 예외 처리로 대응하기 보다 그 상태를 확인하여 조건처리하는 식으로 문제를 해결한다.