Chapter 10. 조건부 로직 간소화
✏️ 조건문 분해하기
배경
- 복잡한 조건부 로직은 프로그램을 복잡하게 만든다.
- 다양한 조건, 그에 따라 동작도 다양한 코드를 작성하면 순식간에 꽤 긴 함수가 탄생한다.
- 거대한 코드 블록이 주어지면 코드를 부위별로 분해한 다음 각각 의도를 살린 이름의 함수 호출로 바꿔주자.
- 이렇게 하면 해당 조건이 무엇인지 강조하고, 그래서 무엇을 분기했는지 명확해진다.
절차
- 조건식과 그 조건식에 딸린 조건절 각각을 함수로 추출한다.
예시
Before
// 함수 내용 : 여름 기간 여부에 따라 할인율이 달라지는 함수
// 목표: 조건식을 별도 함수로 추출해 조건문 정리하기
if (!aDate.isBefore(plan.summerStart) && !aDate.isAfter(plan.summerEnd)) {
charge = quantity * plan.summerRate;
} else {
charge = quantity * plan.regularRate + plan.regularServiceCharge;
}
After
function summer() { // 여름철 기간 조건
return !date.isBefore(plan.summerStart) && !date.isAfter(plan.summerEnd)
}
function summerCharge() { // 여름철 비용
return quantity * plan.summerRate;
}
function regularCharge() { // 일반 비용
return quantity * plan.regularRate + plan.regularServiceCharge;
}
// 결과 1 ⭐️
if (summer()) {
charge = summerCharge();
} else {
charge = regularCharge();
}
// 결과 2 ⭐️ - 삼항 연산자로 변경
charge = summer() ? summerCharge() : regularCharge();
Deep dive
- 이번 예시에서는 함수로 변경하는 방안에 대해 소개했는데, 이런 조건과 값의 조합에는 변수화해서 적용하는게 더 낫지 않을까?
- 저도 조건식안
✏️ 조건식 통합하기
배경
- 비교하는 조건은 다르지만 그 결과로 수행하는 동작이 똑같은 코드들이 있다.
- 이럴 때 and 연산자와 or 연산자를 사용해 여러 개의 비교 로직을 하나로 합칠 수 있다.
- 조건부 코드를 통합하는 게 중요한 이유는 두 가지다.
- 여러 조건들을 하나로 통합해 하려는 일을 명확하게 만든다.
- 이 작업이 함수 추출하기까지 이어질 수 있다. 함수로 추출해 의도를 명확하게 드러내게 할 수 있기 때문이다.
절차
- 해당 조건식들에 부수효과가 없는지 확인하고, 있다면 질의 함수와 변경 함수 분리하기를 먼저 적용한다.
- 조건문 두 개를 선택해 논리 연산자로 결합한다.
- 조건이 하나만 남을 때까지 반복한다.
- 하나로 합쳐진 조건식을 함수로 추출할지 고민해본다.
예시
Before
// 함수 내용 : 직원의 연차, 장애를 갖게 된 기간, 근무 형태를 확인해 장애 수당 계산
// 목표 : 순차적으로 진행되는 조건
function disabilityAmount(employee) {
if (employee.seniority < 2) return 0;
if (employee.monthsDisabled > 12) return 0;
if (employee.isPartTime) return 0;
return 1;
}
After
function disabilityAmount(employee) {
if (isNotEligibleForDisability(employee)) {
return 0;
}
return 1;
}
// 장애 수당 자격 조건 함수
function isNotEligibleForDisability(employee) {
return employee.seniority < 2 || employee.monthsDisabled > 12 || employee.isPartTime
}
✏️ 중첩 조건문을 보호 구문으로 바꾸기
배경
- 조건문은 주로 두 가지 형태로 쓰인다.
- 참인 경로, 거짓인 경로 모두 정상 동작으로 이어지는 형태
- 정상인 형태
- 두 형태는 의도하는 바가 서로 다르므로 그 의도가 코드에 드러나야 한다.
- 한 쪽만 정상이라면 비정상 조건을 if에서 검사한다.
- 조건이 참(또는 비정상)이면 함수에서 빠져나온다.
- 이 검사 형태를 흔히 보호 구문(guard clause)이라고 한다.
- “이건 이 함수의 핵심이 아니다. 이 일이 일어나면 뭔가 조치를 취한 후 함수에서 빠져나온다”
- 코드에서는 명확함이 핵심이다. 반환점이 하나일 때 명확해진다면 그렇게 하고, 아니라면 하지 않는다.
절차
- 교체해야 할 조건 중 가장 바깥 것을 선택해 보호 구문으로 바꾼다.
- 이를 반복한다.
- 모든 보호 구문이 같은 결과를 반환한다면 보호 구문들의 조건식을 통합한다.
예시
Before
// 함수 내용 : 직원 급여를 계산하는 코드. 각 조건에 따라 result에 값을 넣어 return한다.
// 목표 : 중첩 조건문을 보호 구문으로 바꾸기
function payAmount(employee) {
let result;
// 퇴사한 직원인가?
if (employee.isSeparated) {
result = { amount: 0, reasonCode: 'SEP' };
} else {
// 은퇴한 직원인가?
if (employee.isRetired) {
result = { amount: 0, reasonCode: 'RET' };
} else {
// 급여 계산 로직
lorem.ipsum(dolor.sitAmet);
consectetur(adipiscing).elit();
sed.do.eiusmod = tempor.incididunt.ut(labore) && dolore(magna.aliqua);
ut.enim.ad(minim.veniam);
result = someFinalComputation();
}
}
return result;
}
After
- 최상위 조건부터 하나씩 보호 구문으로 변경한다.
- 다음 조건도 동일하게 작성한다.
- result 가변 변수를 제거하고 보호 구문을 통과한 값을 return 시켜준다.
export function payAmount(employee) {
// early return ⭐️
if (employee.isSeparated) return { amount: 0, reasonCode: 'SEP' };
if (employee.isRetired) return { amount: 0, reasonCode: 'RET' };
// 급여 계산 로직
lorem.ipsum(dolor.sitAmet);
consectetur(adipiscing).elit();
sed.do.eiusmod = tempor.incididunt.ut(labore) && dolore(magna.aliqua);
ut.enim.ad(minim.veniam);
return someFinalComputation();
}
✏️ 조건부 로직을 다형성으로 바꾸기
배경
- 복잡한 조건부 로직은 해석하기 가장 난해한 대상에 속한다.
- 타입을 기준으로 분기하는 switch문이 포함 된 함수가 여러개 있는 경우
- case별로 클래스를 만들어 공통 switch 로직 중복 제거
- 기본 동작을 위한 case문과 그 변형 동작들로 구성된 로직 생성
- 타입을 기준으로 분기하는 switch문이 포함 된 함수가 여러개 있는 경우
절차
- 다형적 동작을 표현하는 클래스를 만들어준다. 또한 적합한 인스턴스를 알아서 만들어 반환하는 팩토리 함수도 함께 만든다.
- 호출하는 코드에서 팩토리 함수를 사용하게 한다.
- 조건부 로직 함수를 슈퍼클래스로 옮긴다.
- 서브클래스 중 하나를 선택해 슈퍼클래스의 조건부 로직 메서드를 오버라이드한다.
- 같은 방식으로 각 조건절을 해당 서브클래스에서 메서드로 구현한다.
- 슈퍼클래스 메서드에는 기본 동작만 남긴다.
예시
Before
Plumage: 깃털
Voltage: 전압
Nail: 못으로 고정하다, 못으로 박다
Velocity: 속도
// 새 정보로 새 이름, 깃털 상태 정보를 가진 맵 객체 생성
function plumages(birds) {
return new Map(birds.map(b => [b.name, plumage(b)]));
}
// 새 정보로 새 이름, 비행 속도 정보를 가진 맵 객체 생성
function speeds(birds) {
return new Map(birds.map(b => [b.name, airSpeedVelocity(b)]));
}
// 새 타입에 따라 깃털 상태 출력
function plumage(bird) { // 깃털 상태
switch (bird.type) {
case '유럽 제비':
return "보통이다";
case '아프리카 제비':
return (bird.numberOfCoconuts > 2) ? "지쳤다" : "보통이다";
case '노르웨이 파랑 앵무':
return (bird.voltage > 100) ? "그을렸다" : "예쁘다";
default:
return "알 수 없다";
}
}
// 새 타입에 따라 비행 속도 출력
function airSpeedVelocity(bird) { // 비행속도
switch (bird.type) {
case '유럽 제비':
return 35;
case '아프리카 제비':
return 40 - 2 * bird.numberOfCoconuts;
case '노르웨이 파랑 앵무':
return (bird.isNailed) ? 0 : 10 + bird.voltage / 10;
default:
return null;
}
}
After
- airSpeedVelocity(), plumage()를 Bird 클래스로 묶는다.
class Bird {
constructor(birdObject) {
Object.assign(this, birdObject)
}
get plumage() {
switch (this.type) {
case '유럽 제비':
return "보통이다";
case '아프리카 제비':
return (this.numberOfCoconuts > 2) ? "지쳤다" : "보통이다";
case '노르웨이 파랑 앵무':
return (this.voltage > 100) ? "그을렸다" : "예쁘다";
default:
return "알 수 없다";
}
}
get airSpeedVelocity() {
switch (this.type) {
case '유럽 제비':
return 35;
case '아프리카 제비':
return 40 - 2 * this.numberOfCoconuts;
case '노르웨이 파랑 앵무':
return (this.isNailed) ? 0 : 10 + this.voltage / 10;
default:
return null;
}
}
}
2. 종별 서브클래스와 서브클래스의 인스턴스를 만들어줄 팩토리 함수를 만든다.
function createBird(bird) {
switch (bird.type) {
case '유럽 제비':
return new EuropeanSwallow(bird);
case '아프리카 제비':
return new AfricanSwallow(bird);
case '노르웨이 파랑 앵무':
return new NorwegianBlueParrot(bird);
default:
return new Bird(bird);
}
}
// 팩토리 함수
function plumage(bird) {
return createBird(bird).plumage;
}
function airSpeedVelocity(bird) {
return createBird(bird).airSpeedVelocity;
}
// 서브클래스
class EuropeanSwallow extends Bird { }
class AfricanSwallow extends Bird { }
class NorwegianBlueParrot extends Bird { }
3. switch문을 하나씩 해당 서브클래스로 오버라이드한다.
function createBird(bird) {
get plumage() {
return "알 수 없다";
}
get airSpeedVelocity() {
return null;
}
}
class EuropeanSwallow extends Bird {
get plumage() {
return "보통이다";
}
get airSpeedVelocity() {
return 35;
}
}
class AfricanSwallow extends Bird {
get plumage() {
return (this.numberOfCoconuts > 2) ? "지쳤다" : "보통이다";
}
get airSpeedVelocity() {
return 40 - 2 * this.numberOfCoconuts;
}
}
class NorwegianBlueParrot extends Bird {
get plumage() {
return (this.voltage > 100) ? "그을렸다" : "예쁘다";
}
get airSpeedVelocity() {
return (this.isNailed) ? 0 : 10 + this.voltage / 10;
}
}
✏️ 특이 케이스 추가하기
배경
- 데이터 구조의 특정 값을 확인한 후 똑같은 동작을 수행하는 코드가 곳곳에 등장하는 경우가 있다.
- 이런 코드가 여러 곳에 있다면 그 반응들을 한 데로 모으는 게 효율적이다.
- 특수한 경우의 공동 동작을 요소 하나에 모아서 사용하는 패턴을 특이 케이스 패턴(Special Case Pattern)이라 한다.
- 이 패턴을 활용해 특이 케이스를 확인하는 코드 대부분을 단순 함수 호출로 바꿀 수 있다.
- 특이 케이스는 여러 형태로 표현할 수 있다.
- 특이 케이스 객체에서 단순히 데이터를 읽기만 한다면 반환값을 담은 리터럴 객체 형태로 준비하면 된다.
- 그 이상의 동작을 한다면 필요한 메서드를 담은 객체를 생성한다.
- 특히 null을 특이 케이스로 처리해야 할 때가 많아 이 패턴을 널 객체 패턴(Null Object Pattern)이라고도 한다.
절차
컨테이너 : 리팩터링 대상이 될 속성을 담은 데이터 구조(혹은 클래스)
- 컨테이너에 특이 케이스를 검사하는 속성을 추가하고, false를 반환하게 한다.
- 그 속성들을 모은 객체를 만들어 true를 반환하게 한다.
- 클라이언트에서 특이 케이스 검사 코드를 함수로 추출한다. 그리고 이 함수를 사용하도록 고친다.
- 코드에 새로운 특이 케이스 대상을 추가한다.
- 특이 케이스를 검사하는 함수 본문을 수정해 특이 케이스 객체의 속성을 사용하도록 한다.
- 여러 함수를 클래스로 묶거나 변환 함수로 묶어 특이 케이스를 처리하는 공통 동작을 새로운 요소로 옮긴다.
예시
Before
Delinquent: 채무를 이행하지 않은, 연체된
Acquire: 획득하다, 취득하다
Enrich: 강화하다
📍 현장 정보와 고객 정보가 담긴 기본 데이터 형태
{
name: "애크미 보스턴",
location: "Malden MA",
// 더 많은 현장(site) 정보
customer: {
name: "애크미 산업",
billingPlan: "plan-451",
paymentHistory: {
weekDelinquentInLastYear: 7
}
}
};
📍 미확인 고객인 경우
const UnknownCustomer = {
name: "물류창고 15",
location: "Malden MA",
// 더 많은 현장(site) 정보
customer: "미확인 고객", ⭐️
};
// 클라이언트1
// 고객 정보가 미확인이면 '거주자'로, 확인됐다면 고객명 표시
const site = acquireSiteData();
const aCustomer = site.customer;
...
let customerName;
if (aCustomer === "미확인 고객") customerName = "거주자";
else customerName = aCustomer.name;
// 클라이언트2
// 미확인 고객이면 기본 요금제를, 확인됐다면 해당 고객의 요금제를 반환
const plan = (aCustomer === "미확인 고객") ?
registry.billingPlans.basic
: aCustomer.billingPlan;
// 클라이언트3
// 고객의 연체 기간을 주로 환산하는데, 미확인 고객이면 0주,
// 확인된 고객이면 체납 기록에서 작년 주당 연체 기록 반환
const weeksDelinquent = (aCustomer === "미확인 고객")
? 0
: aCustomer.paymentHistory.weekDelinquentInLastYear;
After
- 현장 데이터 구조를 변환하는 enrichSite 함수를 생성한다.
- 우선 깊은 복사만 수행하도록 한다.
- const result = _.cloneDeep(inputSite);
- 알려지지 않은 고객인지 검사하는 로직을 함수로 추출한다.
- enrichSite 함수에 고객 레코드에 isUnknown 속성을 추가하도록 보강한다.
- const UnknownCustomer = { isUnknown: true, }…
- isUnknown 함수에서 기존 데이터, 변환된 데이터 모두 사용할 수 있도록 조건을 추가한다.
- 특이 케이스들을 unknownCustomer로 몰아넣는다.
function isUnknown(aCustomer) { 📍 3번
if (aCustomer === "미확인 고객") return true;
else return aCustomer.isUnknown; 📍 5번
}
// 클라이언트1
const rawSite = acquireSiteData();
const site = enrichSite(rawSite);
const aCustomer = site.customer;
const customerName = aCustomer.name;
function enrichSite(inputSite) {
const result = _.cloneDeep(inputSite); 📍 2번
const unknownCustomer = {
isUnknown: true,
name: "거주자",
billingPlan: registry.billingplans.basic,
paymentHistory: {
weeksDelinquentInLastYear: 0,
}
};
// 미확인 고객이면 미확인 고객 데이터로 교체
// 확인된 고객이면 isUnknown: false 속성 추가
if (isUnknown(result.customer)) result.customer = unknownCustomer;
else result.customer.isUnknown = false;
return result;
}
// 클라이언트2
// 미확인 고객이면 기본 요금, 아니면 고객의 요금 정보
const plan = aCustomer.billingPlan;
// 클라이언트3
const weeksDelinquent = aCustomer.paymentHistory.weekDelinquentInLastYear;
✏️ 어서션 추가하기
배경
- 어서션은 항상 참이라고 가정하는 조건부 문장을 뜻한다.
- 어서션은 프로그램이 어떤 상태임을 가정한 채 실행되는지를 다른 개발자에게 알려주는 훌륭한 소통 도구이다.
절차
- 참이라고 가정하는 조건이 보이면 그 조건을 명시하는 어서션을 추가한다.
예시
Before
// 클래스 내용 : 할인율이 존재할 때 구매 개수만큼 할인율을 적용해 할인가를 도출
// 목표 : 할인율이 항상 양수라는 가정을 추가하기
class Customer {
...
applyDiscount(number) {
return this.discountRate ? number - (this.discountRate * number) : number;
}
}
After
class Customer {
...
applyDiscount(number) {
if (!this.discountRate) return number;
else {
assert(this.discountRate >= 0); // 할인율이 양수라고 가정해보자 ⭐️
return number - (this.discountRate * number);
}
}
}
Deep dive
- 어서션을 남발하는 것은 위험하다.
- ‘반드시 참이어야만 하는 것’만 검사한다.
✏️ 제어 플래그를 탈출문으로 바꾸기
배경
- 제어 플래그란 코드의 동작을 변경하는 데 사용되는 변수를 말한다.
- 어딘가에서 값을 계산해 제어 플래그에 설정한 후
- 다른 어딘가의 조건문에서 검사하는 형태로 쓰인다.
- 제어 플래그의 주 서식지는 반복문 안이다.
- return 문이 여러 개 생기더라도 명확한 역할을 하는 것이 좋을 수 있다.
절차
- 제어 플래그를 사용하는 코드를 함수로 추출할지 고민한다.
- 제어 플래그를 갱신하는 코드 각각을 적절한 제어문으로 바꾼다.
- 모두 수정했다면 제어 플래그를 제거한다.
예시
Before
// 함수 내용 : 위험한 사람을 발견하면 경고하고 찾았다고 알리는 코드
let found = false;
for (const p of people) {
if (!found) {
if (p === '조커') {
sendAlert();
found = true;
}
if (p === '사루만') {
sendAlert();
found = true;
}
}
}
After
function checkForVillain(people) {
for (const p of people) {
if (p === '조커') {
sendAlert();
return;
}
if (p === '사루만') {
sendAlert();
return;
}
}
}
// 더 가다듬기
const villainList = ['조커', '사루만'];
function checkForVillain(people) {
if (people.some(p => villainList.includes(p))) sendAlert();
}
반응형
'Programming > 13. Book' 카테고리의 다른 글
리팩터링 - 12장 (0) | 2024.03.02 |
---|---|
리팩터링 - 11장 (0) | 2024.03.02 |
리팩터링 - 8, 9장 (0) | 2024.03.02 |
리팩터링 - 5, 6장 (0) | 2024.03.02 |
리팩터링 - 2장 (0) | 2024.03.02 |
댓글