logo
새로운 블로그로 이전하였습니다.

(리팩터링 2판) ch11.3 플래그 인수 제거하기

  • 리팩터링
  • 플래그 인수
  • flag argument

💭 배경

플래그 인수 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);
  • 첫 번째 예시와 같은 방식으로 호출문을 대체하면 된다.

Source

리팩터링 2판, 마틴파울러