💭 배경
플래그 인수 flag argument
플래그 인수란 호출되는 함수가 실행할 로직을 호출하는 쪽에서 선택하기 위해 전달하는 인수를 말한다.
예시 코드
function bookConcert(aCustomer, isPremium) {
if (isPremium) {
// 프리미엄 예약용 로직
} else {
// 일반 예약용 로직
}
}
예를 들어 bookConcert
라는 함수를 이렇게 만들고, 호출한다고 해보자
프리미엄 콘서트 예약
bookConcert(aCustomer, true);
만약 콘서트를 프리미엄으로 예약한다면 이렇게 호출할 것이다.
플래그 인수가 열거형일 때
bookConcert(aCustomer, CustomerType.PREMIUM);
플래그 인수가 열거형일 때는 이렇게 호출할 것이다.
플래그 인수가 문자열일 때
bookConcert(aCustomer,"premium");
플래그 인수가 문자열(혹은 해당 프로그래밍 언언가 제공하는 또 다른 타입)일 때는 이렇게 호출할 것이다.
플래그 인수의 문제점
bookConcert(aCustomer, true);
실제로 개발 중인 프로젝트에서 위와 같은 코드를 본다고 생각하면, 대다수의 사람이 같은 의문을 가질 것이라 생각한다.
bookConcert의 두 번째 인자로 전달하는 true가 뭐지?
이렇게 플래그 인자를 쓰게되면, 코드를 읽는 사람은 코드를 이해하기 어려워진다.
이 코드는 다음처럼, 특정한 기능 하나만 수행하는 명시적인 함수로 바꾸는 것이 깔끔하다.
premiumBookConcert(aCustomer);
플래그 인수를 사용했을 때의 문제점들을 정리하면 다음과 같다.
1. `호출할 수 있는 함수가 무엇인지`, `그 함수를 어떻게 호출해야하는지`를 이해하기 어렵게 만든다.
2. 함수들의 기능 차이가 잘 드러나지 않게 만든다.
3. 코드를 읽는 이에게 뜻을 온전히 전달하지 못하게 한다.
이에 따라 코드의 가독성이 저하되고, 유지보수성이 떨어질 수 있다.
플래그 인수의 조건
그렇다고 위의 예시에서 들었던 인수같이 생겼다고 해서 모두 플래그 인수가 아니다.
플래그 인수가 되기 위해선 다음과 같은 조건을 만족해야 한다.
1. 호출하는 쪽에서 `boolean 값`으로 (프로그램에서 사용되는 데이터가 아닌) `리터럴 값`을 건네야 한다.
2. 호출되는 함수는 그 인수를 (다른 함수에 전달되는 데이터가 아닌) `제어 흐름을 결정하는 데 사용`해야 한다.
플래그 인수를 제거함으로써 얻는 효과
플래그 인수를 제거하면 코드가 깔끔해지고, 코드 분석 도구가 프리미엄 로직 호출과 일반 로직 호출의 차이를 더 쉽게 파악할 수 있게 된다.
플래그 인수를 사용해도 되는 경우
함수 하나에서 플래그 인수를 두 개 이상
사용할 때는 플래그 인수를 사용해도 된다.
이 경우에는 플래그 인수 없이 구현하는 게 플래그 인수들의 가능한 조합 수만큼의 함수를 만들지 않는 이상 힘들기 때문이다.
하지만, 플래그 인수가 두 개 이상이면 함수 하나가 너무 많은 일을 담당하고 있다는 신호이기도 하다.
그러므로 같은 로직을 조합해내는 더욱 간단한 함수를 만들 방법을 고민해볼 필요가 있다.
📏 절차
1. 매개변수로 주어질 수 있는 값 각각에 대응하는 명시적 함수들을 생성한다.
-> 주가 되는 함수에 깔끔한 분배 조건문이 포함되어 있다면,
조건문 분해하기(10.1절)로 명시적 함수들을 생성하자.
그렇지 않다면 래핑 함수 wrapping function 형태로 만든다.
2. 원래 함수를 호출하는 코드를 모두 찾아서, 각 리터럴 값에 대응되는 명시적 함수를 호출하도록 수정한다.
🧑🏻🏭 리팩토링 실습
첫 번째 예시 코드
1. 배송일자를 계산하는 호출
aShipment.deliveryDate = deliveryDate(anOrder, true);
aShipment.deliveryDate = deliveryDate(anOrder, false);
- 배송일자를 계산하는 호출문들이다.
- 이 코드에서는
deliveryDate
에 전달되는boolean
값들이 무엇을 의미하는지 직관적으로 파악하기 어렵다.
2. deliveryDate()
함수
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 + dliveryTime);
}
else {
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.placedOn.plusDays(2 + dliveryTime);
}
}
- 호출하는 쪽에서
boolean 리터럴 값
을 통해 어느 쪽 코드를 실행할 것인지 정하고 있다. - 그러므로
deliveryDate
에 전달되는boolean 리터럴 값
은 전형적인플래그 인수
라고 할 수 있다. - 호출자의 의도를 명시적인 함수로 나타낼 필요가 있다.
1️⃣ 조건문 분해
function deliveryDate(anOrder, isRush) {
if (isRush) return rushDeliveryDate(anOrder);
else return regularDeliveryDate(anOrder);
}
function rushDeliveryDate(anOrder) {
let deliveryDate;
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)l
}
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.placedOn.plusDays(2 + dliveryTime);
}
2️⃣ 새로 만든 함수로 호출문 대체
aShipment.deliveryDate = rushDeliveryDate(anOrder);
aShipment.deliveryDate = regularDeliveryDate(anOrder);
deliveryDate()
를 호출했던 부분을 전부 새로 만든 함수로 대체하여,플래그 인수
를 사용하지 않도록 한다.deliveryDate()
를 호출하는 곳을 전부 교체했다면,deliveryDate()
를 제거한다.
두 번째 예시코드
앞에 예시에서는 조건문을 쪼개서 리팩토링을 수월하게 진행했다.
하지만 매개변수에 따른 분배 로직이 함수 핵심 로직의 바깥에 해당할 때 사용이 편리하다.
매개변수가 훨씬 까다로운 방식으로 사용될 때의 예시도 살펴보자.
1. 까다로운 deliveryDate()
function deliveryDate(anOrder, isRush) {
let result;
let deliveryTime;
if (anOrder.deliveryState == "MA" || anOrder.deliveryState === "CT")
deliveryTIme = isRush? 1:2;
else if (anOrder.deliveryState === "NY" || anOrder.deliveryState === "NH") {
deliveryTime = 2;
if (anOrder.deliveryState === "NH" && !isRush)
deliveryTime = 3;
}
else if (isRush)
deliveryTime = 3;
else if (anOrder.deliveryState === "ME")
deliveryTIme = 3;
else
deliveryTime = 4;
result = anOrder.placedOn.plusDays(2 + deliveryTime);
if (isRush) result = result.minusDays(1);
return result;
}
1️⃣ 래핑 함수로 감싸기
function rushDeliveryDate (anOrder) {return dliveryDate(anOrder, true);}
function regularDeliveryDate (anOrder) {return dliveryDate(anOrder, false);}
isRush
를 최상위 조건 분배문으로 뽑아내려면 일이 커질 수 있다.- 이럴 때는
deliveryDate
를 수정하지 않고, 이 함수 자체를 다른 함수로 감싸서 호출하도록 하면 된다. - 래핑 함수들을 독립적으로 정의했지만, 새로운 기능을 추가하지 않고 각각이
deliveryDate()
의 기능 일부만을 제공하도록 처리했다.
2️⃣ 호출문 대체하기
aShipment.deliveryDate = rushDeliveryDate(anOrder);
aShipment.deliveryDate = regularDeliveryDate(anOrder);
- 첫 번째 예시와 같은 방식으로 호출문을 대체하면 된다.