더 좋은 성능과 가독성을 위한 if-else문을 작성할 수 있는 방법 4가지 중에 남은 2가지를 마저 작성해보자.
두가지의 방법을 먼저 작성했던 첫번째 포스팅 다음으로 남은 두가지의 방법을 알아보고 포스팅을 해보려한다.
1, 2번의 방법은 위에 올려놓은 이전의 포스팅에 올려놓았기 때문에 이번 글은 3번부터 시작하겠습니다잉
먼저, 오늘의 두가지 방법을 알기 위해서는 자바스크립트의 일급 객체에 대해서 간단하게라도 알면 좋기 때문에 간단하게 일급 객체가 무엇인지 알아보자.
일급 객체
일급 객체는 다른 객체들에게 일반적으로 적용 가능한 연산을 모두 지원하는 객체를 가리킨다. 함수에 매개변수로 넘기기, 수정하기, 변수에 대입하기 등과 같은 연산을 지원하는 객체를 일급 객체라고 부른다. 즉, 사용할 때 다른 요소들과 아무런 차별이 없는 객체를 일급 객체라고 부른다. 아래 3가지의 조건을 충족해야 일급 객체라고 할 수 있다.
- 모든 일급 객체는 변수나 데이터에 담을 수 있어야 한다.
- 모든 일급 객체는 함수의 파라미터로 전달할 수 있어야 한다.
- 모든 일급 객체는 다른 함수의 리턴값으로 사용할 수 있어야 한다.
그럼 자바스크립트의 함수는 위의 3가지 조건을 만족할까?? 정답은 만족한다.
1. 모든 일급 객체는 변수나 데이터에 담을 수 있어야 한다.
이는 자바스크립트에서 함수를 선언할 때 함수 표현식으로 변수나 데이터에 담을 수 있기 때문에 성립한다.
const greeting = () => {
console.log("my name is plla2");
};
greeting(); // my name is plla2
2. 모든 일급 객체는 함수의 파라미터로 전달할 수 있어야 한다.
자바스크립트에서 함수는 콜백 함수를 통해 다른 함수의 파라미터로 전달이 가능하기 때문에 성립한다.
const mul = (num) => {
return num * num;
};
const mulNum = (func, number) => {
return func(number);
};
let result = mulNum(mul, 3);
console.log(result); // 9
3. 모든 일급 객체는 다른 함수의 리턴값으로 사용할 수 있어야 한다.
자바스크립트에서 함수는 커링을 통해 함수의 리턴값을 호출이 가능하기 때문에 성립한다.
const add = (num1) => {
return (num2) => {
return num1 + num2;
};
};
let result = add(1)(2);
console.log(result); // 3
자 그럼, 자바스크립트의 함수가 일급 객체인걸 알았으니까 본론으로 넘어가보자.
3. Lookup Table 기법
나는 lookup table을 처음들어보았기 때문에 검색을 해보니까, switch문이나 if else문이 너무 길게 늘어질 경우를 대비하여 명시적이며 효율적인 코드 작성 방식을 지칭하는 것이라고 하고, 실제 있는 문법이 아니라 자바스크립트의 일급 객체의 특성을 이용하여 else if 로직을 key-value 쌍의 형태로 각각 논리를 캡슐화 한 것 이라고 한다. 상수를 잘 활용하면 불필요한 분기문을 줄일 수 있고, lookup table과 객체를 잘 엮어서 좀 더 유연하게 작성도 가능하다고 하니 잘 알아보자.
그럼, 예시코드를 통해 어떻게 사용하는지 활용해보자. 각 유저들의 역할에 따른 기능을 확인하는 4개의 함수들이 있다고 했을 때, checkRole() 함수에서 한개의 매개변수인 role에 대해 조건별로 분기가 나누어져 있다.
const adminRole = () => { return "관리자"; };
const teacherRole = () => { return "강사"; };
const studentRole = () => { return "학생"; };
const nonMemberRole = () => { return "비회원"; };
const checkRole = (role, id) => {
let checking = "";
if (role === "admin") {
checking = adminRole();
} else if (role === "teacher") {
checking = teacherRole();
} else if (role === "student") {
checking = studentRole();
} else if (role === "nonMember") {
checking = nonMemberRole();
}
return `${checking}인 ${id}`;
};
console.log(checkRole("student", "plla2")); // 학생인 plla2
console.log(checkRole("admin", "승현")); // 관리자인 승현
그렇게 보기 안좋아보이진 않는다. 하지만 Lookup Table 기법을 활용하여 좀더 효율적으로 바꿀 수 있다.
그럼 어떻게 바꿀 수 있는지 바꿔보자!
1. 조건문만 따로 분리해낸다.
말그대로 checkRole() 함수의 조건문 부분만 따로 빼내어 새로운 함수로 분리한다. 그리고 조건문이 들어있는 새로운 함수conditionalRole()를 호출하여 결과값을 받아서 checkRole() 함수에서 사용할 수 있게 해준다.
const conditionalRole = (role) => {
if (role === "admin") {
checking = adminRole();
} else if (role === "teacher") {
checking = teacherRole();
} else if (role === "student") {
checking = studentRole();
} else if (role === "nonMember") {
checking = nonMemberRole();
}
};
const checkRole = (role, id) => {
let checking = conditionalRole(role);
return `${checking}인 ${id}`;
};
2. 원래의 if-else 문을 switch-case 문으로 바꿔준다.
중첩된 if-else문이 아니기 때문에 switch-case 문으로 변환이 가능하다.
const conditionalRole = (role) => {
switch (role) {
case "admin":
return adminRole();
case "teacher":
return teacherRole();
case "student":
return studentRole();
case "nonMember":
return nonMemberRole();
}
};
const checkRole = (role, id) => {
let checking = conditionalRole(role);
return `${checking}인 ${id}`;
};
자바스크립트에서 switch-case문을 사용할 때는 원하는 case로 바로 이동하는 것이 아닌 case의 나열된 순서대로 평가를 하기 때문에 switch문을 지양하라는 말도 있답니다.
3. swtich-case 문을 객체로 변환해준다.
아래의 코드처럼 case부분을 객체의 key값으로, 리턴문을 가진 각 함수를 객체의 value값으로 넣어주어 자바스크립트의 객체처럼 구성을 해준다.
const conditionalRoleMap = {
admin: adminRole,
teacher: teacherRole,
student: studentRole,
nonMember: nonMemberRole,
};
const checkRole = (role, id) => {
let checking = conditionalRoleMap[role]();
return `${checking}인 ${id}`;
};
위처럼 코드를 리팩토링해주어 checkRole() 함수의 매개변수로 받은 role을 통해 객체의 key값을 받아오고, conditionalMap[role] 을 통해서 받아온 key에 따른 value값인 함수를 실행시켜 결과값을 얻을 수 있게 바꿔주는 것이다.
최종 리팩토링 코드
const adminRole = () => {
return "관리자";
};
const teacherRole = () => {
return "강사";
};
const studentRole = () => {
return "학생";
};
const nonMemberRole = () => {
return "비회원";
};
const conditionalRoleMap = {
admin: adminRole,
teacher: teacherRole,
student: studentRole,
nonMember: nonMemberRole,
};
const checkRole = (role, id) => {
let checking = conditionalRoleMap[role]();
return `${checking}인 ${id}`;
};
console.log(checkRole("student", "plla2")); // 학생인 plla2
console.log(checkRole("admin", "승현")); // 관리자인 승현
이러한 Lookup Table 기법을 사용하여 분기별로 순회해야할 로직을 순회할 필요없이 매개변수의 값에 따라 객체의 key-value를 통해 함수를 호출할 수 있기 때문에 병렬적으로 분기에 접근하게 되고, 이는 성능 향상으로 이어지게 된다.
4. 책임 연쇄 패턴 (Chain Responsility Pattern)
이 패턴은 key매칭을 통해 하나의 인자값에 대해서만 비교를 할 수 있기 때문에 여러개의 인자를 비교할때는 좋지 않은 Lookup Table기법에 대한 대응 기법이라고 한다. 하지만 이 방법은... 써먹기 힘들 것 같다는 나의 생각. ㅋㅋㅋㅋㅋㅋㅋㅋ
그래도 뭔지는 알아봐보자. 책임 연쇄 패턴은 분기문의 블럭들을 객체화하여, 다수의 처리 객체들을 체인형태로 묶는 패턴이다. 벌써부터 머리가 지끈거린다. 무슨소리야... 다른말로는 어떤 요청을 처리할 때 객체 핸들러를 순회하는 식으로 하여 분기문을 객체 지향적으로 표현한 기법이라고 한다. 이론으로 잘 모르겠을 땐 코드를 보는게 좋을수도? 코드를 봐보자. 1편의 포스팅에서 사용했던 Early Return 코드를 다시 가져와보자.
const storeInventory = () => { /* 재고 채우는 로직 */ };
const completeOrder = () => { /* 주문 완료시키는 로직 */ };
const orderProcess = (orderStatus, paymentStatus, inventoryStatus) => {
let result = "";
let count = 0;
if (orderStatus === true) {
result = "이미 주문이 완료되었습니다.";
result += ` (주문요청 횟수 ${++count}번)`;
return result;
}
if (paymentStatus === false) {
throw new Error("결제가 완료되지 않았습니다!");
}
if (inventoryStatus === 0) {
storeInventory();
result = "재고를 채웠습니다.";
result += ` (주문요청 횟수 ${++count}번)`;
return result;
}
completeOrder();
result = "주문이 완료되었습니다.";
result += ` (주문요청 횟수 ${++count}번)`;
return result;
};
console.log(orderProcess(true, false, 10)); // 이미 주문이 완료되었습니다. (주문요청 횟수 1번)
console.log(orderProcess(false, true, 0)); // 재고를 채웠습니다. (주문요청 횟수 1번)
console.log(orderProcess(false, true, 10)); // 주문이 완료되었습니다. (주문요청 횟수 1번)
console.log(orderProcess(false, false, 10)); // Error: 결제가 완료되지 않았습니다 !
위의 예제 코드에서 어떤 분기문이 새로 추가되면 분기문에 따라 orderProcess 함수 자체를 고쳐야할 수도 있다. orderProcess라는 함수에 3가지의 매개변수에 대한 조건 판단을 맡기고 있기 때문이다. 그렇기 때문에 책임 연쇄 패턴은 각 핸들러를 만들어서 각자 책임을 지니게 해주는 것이다. 그럼 한번 해보자!
1. 조건식과 실행코드가 있는 객체를 담을 배열을 만들어준다.
const roles=[
{
match: (a,b,c)=>{ /* if문 조건식을 넣어준다 */ },
action:(a,b,c)=>{ /* if문 블럭의 실행코드를 넣어준다. */ },
},
{
match: (a,b,c)=>{ /* if문 조건식을 넣어준다 */ },
action:(a,b,c)=>{ /* if문 블럭의 실행코드를 넣어준다. */ },
},
{
match: (a,b,c)=>{ /* if문 조건식을 넣어준다 */ },
action:(a,b,c)=>{ /* if문 블럭의 실행코드를 넣어준다. */ },
},
{
...
},
...
]
이런식으로 원소 객체마다 match와 action 함수를 만들어주고, match가 true일 때 해당 객체의 action함수가 실행되고, match가 false일 때는 다음 객체의 match로 넘어가는식으로 동작되게 하기 위한 첫 단계이다.
2. 1번의 match, action 함수에 필요한 조건식과 실행코드를 넣어준다.
const Handler = {
result: "",
count: 0,
roles: [
{
match: (orderStatus) => {
return orderStatus ? true : false;
},
action: () => {
Handler.result = "이미 주문이 완료되었습니다.";
Handler.result += ` (주문요청 횟수 ${++count}번)`;
return Handler.result;
},
},
{
match: (paymentStatus) => {
return !paymentStatus ? true : false;
},
action: () => {
throw new Error("결제가 완료되지 않았습니다!");
},
},
{
match: (inventoryStatus) => {
return inventoryStatus === 0 ? true : false;
},
action: () => {
storeInventory();
Handler.result = "재고를 채웠습니다.";
Handler.result += ` (주문요청 횟수 ${++count}번)`;
return Handler.result;
},
},
{
match: () => {
return true; // 마지막 핸들러부분은 조건판별을 하지않고 바로 action()이 실행
},
action: () => {
completeOrder();
Handler.result = "주문이 완료되었습니다.";
Handler.result += ` (주문요청 횟수 ${++count}번)`;
return Handler.result;
},
},
],
};
3. 배열 roles를 순회하면서 조건식이 true일 때 해당 role의 action을 실행시켜준다.
const orderProcess = (orderStatus, paymentStatus, inventoryStatus) => {
let result = "";
for (const role of Handler.roles) {
if (role.match(orderStatus, paymentStatus, inventoryStatus)) {
result = role.action();
return result;
}
}
};
{match, action} 핸들러 객체들이 들어있는 의사 결정 규칙 배열인 roles를 순회하면서 어떤 핸들러 객체의 match 반환값이 true일 때 같은 핸들러 객체의 action을 실행시키고, false일 때는 roles를 순회하기 때문에 자연스럽게 다음 핸들러 객체로 이동하는 방식으로 동작한다.
최종 리팩토링 코드
const storeInventory = () => {};
const completeOrder = () => {};
const Handler = {
result: "",
count: 0,
roles: [
{
match: (orderStatus, paymentStatus, inventoryStatus) => {
return orderStatus ? true : false;
},
action: () => {
Handler.result = "이미 주문이 완료되었습니다.";
Handler.result += ` (주문요청 횟수 ${++Handler.count}번)`;
return Handler.result;
},
},
{
match: (orderStatus, paymentStatus, inventoryStatus) => {
return !paymentStatus ? true : false;
},
action: () => {
throw new Error("결제가 완료되지 않았습니다!");
},
},
{
match: (orderStatus, paymentStatus, inventoryStatus) => {
return inventoryStatus === 0 ? true : false;
},
action: () => {
storeInventory();
Handler.result = "재고를 채웠습니다.";
Handler.result += ` (주문요청 횟수 ${++Handler.count}번)`;
return Handler.result;
},
},
{
match: () => {
return true;
},
action: () => {
completeOrder();
Handler.result = "주문이 완료되었습니다.";
Handler.result += ` (주문요청 횟수 ${++Handler.count}번)`;
return Handler.result;
},
},
],
};
const orderProcess = (orderStatus, paymentStatus, inventoryStatus) => {
let result = "";
for (const role of Handler.roles) {
if (role.match(orderStatus, paymentStatus, inventoryStatus)) {
result = role.action();
return result;
}
}
};
console.log(orderProcess(true, false, 10)); // 이미 주문이 완료되었습니다. (주문요청 횟수 1번)
console.log(orderProcess(false, true, 0)); // 재고를 채웠습니다. (주문요청 횟수 2번)
console.log(orderProcess(false, true, 10)); // 주문이 완료되었습니다. (주문요청 횟수 3번)
console.log(orderProcess(false, false, 10)); // Error: 결제가 완료되지 않았습니다 !
상당히 복잡스러우면서도 내가 느꼈을 땐 신기한 방식이라고 생각했다. 이러한 생각을 할 수 있다는게 대단하다고 생각하고, 이러한 방법으로 설계를 할 수 있다~ 라고 정말 하나의 동작을 만드는데 여러가지 방법이 있다는 것을 다시한번 배울 수 있었다. 실제로 써먹...을 수 있을까?? 그건 모르겠다.
이렇게 저번 포스팅을 포함하여 4가지의 리팩토링 방법을 알아보고 직접 쳐보며 공부해보았다. 4가지 중 early return 기법은 충분히 계속 생각하여 실제로 많이 써먹을 수 있을 기법으로 느껴졌다.