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판, 마틴파울러