🎯 주제
벌써 마지막 미션인 크리스마스 프로모션이다. 이번 미션은 이전과는 달리 많은 요구사항이 있어서 미션을 이해하는 데에 시간이 많이 소요되었다. 주문한 메뉴의 개수와 방문 날짜에 따라 할인과 혜택이 달라지기 때문에 이러한 기능들을 효과적으로 설계할 수 있는 방법에 대해 고민해보았다.
또한, 이번 미션에서는 제출 방식이 다소 변경되었다. 이제는 비공개 저장소를 통해 미션을 진행하며, 기간 내에는 다른 사람들의 코드를 확인할 수 없도록 변경되었다.
이번 미션의 목표는 이전의 로또 미션과 마찬가지로 클래스(객체)를 분리하는 연습이었다. 도메인 로직을 적절하게 분리하여 책임과 역할에 맞게 구현하는 것이 이번 미션의 핵심이라고 생각한다.
📄 기능 목록 작성하기
# 📄 기능 목록
- 입력 기능
- [ ] 예상 방문 날짜 입력받는 기능
- [ ] 주문메뉴와 개수를 입력받는 기능
- 출력 기능
- [ ] 주문 메뉴와 개수를 출력하는 기능
- [ ] 할인 전 총 주문 금액을 출력하는 기능
- [ ] 증정 메뉴를 출력하는 기능
- [ ] 혜택 내역을 출력하는 기능
- [ ] 총 혜택 금액을 출력하는 기능
- [ ] 할인 후 예상 결제 금액을 출력하는 기능
- [ ] 12월 이벤트 배지를 출럭하는 기능
- 핵심 기능
- [ ] 주문 메뉴에 따른 총 주문 금액을 계산하는 기능
- [ ] 총 주문 금액에 따른 증정 메뉴를 추가하는 기능
- [ ] 크리스마스 디데이 할인 금액을 계산하는 기능
- [ ] 평일 할인 금액을 계산하는 기능
- [ ] 특별 할인 금액을 계산하는 기능
- [ ] 증정 이벤트 할인 금액을 계산하는 기능
- [ ] 총 혜택 금액을 계산하는 기능
- [ ] 총 혜택 금액에 따른 배지를 부여하는 기능
- [ ] 할인 후 예상 결제 금액을 계산하는 기능
# 🎯 예외 상황
- 예상 방문 날짜 입력
- [ ] 1~31인 숫자가 아닌 경우
- 주문 메뉴와 개수를 입력
- [ ] 메뉴판에 없는 메뉴인 경우
- [ ] 개수가 1이상이 아닌 경우
- [ ] 형식이 예시와 다른 경우
- [ ] 중복 메뉴를 입력한 경우
이전 미션들과 마찬가지로 기능 목록은 입/출력과 핵심 기능으로 나누어 작성하였고, 이로 인해 발생하는 예외 상황들도 함께 작성하였다. 특히 이번 미션에서는 요구사항이 많아서 필요한 핵심 기능들이 많았는데, 추후에 구현하면서 해당 기능에 대한 세부적인 기능들은 추가하면 되기 때문에 큰 기능들을 먼저 작성하였다.
구현 전 기능 목록과 예외 상황을 작성하는 것은 이제는 몸에 베어버린 것 같다. 오히려 적지 않으면 구현에 들어가기 힘들 정도로 이제는 기능 목록 작성하는 것이 즐겁게 느껴졌다.
# 📄 기능 목록
- 입력 기능
- [x] 예상 방문 날짜 입력받는 기능
- [x] 주문메뉴와 개수를 입력받는 기능
- 출력 기능
- [x] 이벤트 시작 안내 문구를 출력하는 기능
- [x] 주문 메뉴와 개수를 출력하는 기능
- [x] 할인 전 총 주문 금액을 출력하는 기능
- [x] 증정 메뉴를 출력하는 기능
- [x] 혜택 내역을 출력하는 기능
- [x] 크리스마스 디데이 할인을 출력하는 기능
- [x] 평일 할인을 출력하는 기능
- [x] 주말 할인을 출력하는 기능
- [x] 특별 할인을 출력하는 기능
- [x] 증정 이벤트 할인을 출력하는 기능
- [x] 총 혜택 금액을 출력하는 기능
- [x] 할인 후 예상 결제 금액을 출력하는 기능
- [x] 12월 이벤트 배지를 출럭하는 기능
- 핵심 기능
- [x] 주문 메뉴에 따른 총 주문 금액을 계산하는 기능
- [x] 총 주문 금액에 따른 증정 메뉴의 여부 기능
- [x] 크리스마스 디데이 할인 금액을 계산하는 기능
- [x] 평일 할인 금액을 계산하는 기능
- [x] 주말 할인 금액을 계산하는 기능
- [x] 특별 할인 금액을 계산하는 기능
- [x] 증정 이벤트 할인 금액을 계산하는 기능
- [x] 총 혜택 금액을 계산하는 기능
- [x] 총 혜택 금액에 따른 배지를 부여하는 기능
- [x] 할인 후 예상 결제 금액을 계산하는 기능
# 🎯 예외 상황
- 예상 방문 날짜 입력
- [x] 1~31인 숫자가 아닌 경우
- 주문 메뉴와 개수를 입력
- [x] 메뉴판에 없는 메뉴인 경우
- [x] 개수가 1이상이 아닌 경우
- [x] 형식이 예시와 다른 경우
- [x] 중복 메뉴를 입력한 경우
- [x] 모든 메뉴가 음료인 경우
- [x] 메뉴의 총 개수가 20이 넘은 경우
- [x] 개수가 숫자가 아닌 경우
- 크리스마스 디데이 할인
- [x] 방문 날짜가 25일을 넘었을 경우
구현을 통해 달라진 최종 기능 목록이다. 특정 출력 기능에 대한 세부적인 기능들로 나뉘어지고, 테스트 과정에서 발견된 예외 상황들도 목록에 추가했다. 개인적으로 아쉬웠던 점은 핵심 기능 파트에서 관련된 기능들을 구분해놓았으면 클래스(객체)로 분리할 때보다 쉽게 분리했었을 것 같은데, 이 점이 가장 아쉬웠다.
🏄♂️ 플로우 리스트 작성하기
- 인사말을 출력한다.
- 예상 방문 날짜를 입력받는다.
- 주문할 메뉴와 개수를 입력받는다.
- 주문 메뉴를 출력한다.
- 할인 전 주문금액을 출력한다.
- 증정 메뉴를 출력한다.
- 크리스마스 디데이 할인을 출력한다.
- 평일 할인을 출력한다.
- 주말 할인을 출력한다.
- 특별 할인을 출력한다.
- 증정 이벤트 할인을 출력한다.
- 총 혜택 금액을 출력한다.
- 할인 후 예상 결제 금액을 출력한다.
- 12월 이벤트 배지를 출력한다.
기능 목록 작성과 마찬가지로 플로우 리스트를 작성했다. 개인적으로 플로우 리스트를 작성하는 과정에서 컨트롤러 작성에 도움이 되고, 기능 분리까지 구상되기 때문에 누구에게는 필요한 과정이 아닐 수도 있겠지만, 나에게는 큰 도움이 되었다.
🚫 제한된 메서드 라인
메소드 작성 시 공백을 포함하여 15줄로 제한하는데 신경을 썼다. 때문에 자바스크립트 API를 적극적으로 사용하였고, 자바스크립트 내장 API를 사용하면 코드의 양을 획기적으로 줄일 수 있다는 것을 깨닫게 되었다. 다음은 사용자 주문을 받을 때 필요한 도메인 로직에 해당하는 함수들이다.
getDayOfWeek() {
return new Date(new Date().getFullYear(), CONSTANTS.month.december, this.#visitDate).getDay();
}
#setMenuNames() {
return this.#orderMenus.map(orderMenu =>
Object.keys(MENU.menuName).find(key => MENU.menuName[key] === orderMenu[0]),
);
}
#setMenuPrices(menuNames) {
return menuNames.map(menuName => MENU.menu[MENU.menuName[menuName]].price);
}
#setMenuCount() {
return this.#orderMenus.map(orderMenu => Number(orderMenu[1]));
}
절차 지향적이기 보단 파라미터값을 받아 API를 활용하여 바로 return될 수 있도록 간략화 하는데 신경쓰였다. 확실히 자바스크립트 내장 API를 사용하면 코드의 양을 획기적으로 줄일 수 있다는 것을 깨달으면서 해당 API에 대한 역할에 대해서도 자세히 공부하고 많이 써보는 연습을 해야겠다고 생각했다.
🗂️ 책임과 역할에 따른 클래스(객체) 분리
클래스의 역할과 책임을 잘 부여하여 도메인 로직을 관리하는 데에 신경을 썼다. 이전 미션들과 마찬가지로 이번 미션에서도 MVC 패턴을 통해 분리하여 클래스와 객체들을 체계적으로 설계하는 데에 노력했다. 해당 디자인 패턴을 기반으로 사용자가 입력한 주문과 방문 날짜를 기준으로 총 5가지의 클래스로 구분하여 관리했다.
📂 Model
1️⃣ OrderManager
사용자로부터 입력받은 주문과 방문 날짜를 통해 필요한 형태의 값인 요일 및 전체 메뉴들의 정보, 유효성 검사를 수행할 수 있도록 책임을 부여했습니다. 해당 값들을 통해 혜택들을 계산할 때 필요한 값들을 제공하는 역할을 하도록 설계했다.
주문에 대한 정보를 구하는 setMenuNames, setMenuPrices, setMenuCount는 클래스 내부에서 동작되도록 private으로 설정했습니다. 이후 해당 함수들을 호출하여 하나의 배열로 반환하는 getMenusInfo는 외부에서 접근할 수 있도록 설정했다.
class OrderManager {
#visitDate;
#orderMenus;
constructor(visitDate, orderMenus) {
this.#validate(visitDate, orderMenus);
this.#visitDate = visitDate;
this.#orderMenus = orderMenus;
}
#validate(visitDate, orderMenus) {
VisitDateValidator.validateVisitDate(visitDate);
OrderMenuValidator.validateOrderMenu(orderMenus);
}
getMenusInfo() {
const menuNames = this.#setMenuNames();
const menuPrices = this.#setMenuPrices(menuNames);
const menuCount = this.#setMenuCount();
return menuNames.map((element, index) => [element, menuPrices[index], menuCount[index]]);
}
getDayOfWeek() {
return new Date(new Date().getFullYear(), CONSTANTS.month.december, this.#visitDate).getDay();
}
#setMenuNames() {
return this.#orderMenus.map(orderMenu =>
Object.keys(MENU.menuName).find(key => MENU.menuName[key] === orderMenu[0]),
);
}
#setMenuPrices(menuNames) {
return menuNames.map(menuName => MENU.menu[MENU.menuName[menuName]].price);
}
#setMenuCount() {
return this.#orderMenus.map(orderMenu => Number(orderMenu[1]));
}
}
export default OrderManager;
2️⃣ OrderAmount
변동될 수 있는 금액들을 계산하는 로직을 이벤트별로 관리하였다. 할인 전 주문 금액, 총 할인 금액, 할인 후 주문 금액과 같은 도메인 로직들을 모아 수행하도록 역할을 부여했다.
class OrderAmount {
static calculateAmountBeforeDiscount(menusInfo) {
return menusInfo.reduce((acc, cur) => acc + cur[1] * cur[2], 0);
}
static calculateDiscountTotalAmount(visitDate, orderMenusInfo, dayOfWeek) {
return [
Benefit.calculateDDayDiscount(visitDate),
Benefit.calculateWeekDayDiscount(orderMenusInfo, dayOfWeek),
Benefit.calculateWeekEndDiscount(orderMenusInfo, dayOfWeek),
Benefit.calculateSpecialDiscount(visitDate, dayOfWeek),
]
.filter(benefitDiscount => benefitDiscount !== false)
.reduce((acc, cur) => acc + cur, 0);
}
static calculateOrderAmountAfterDiscount(orderAmountBeforeDiscount, discountTotalAmount) {
return orderAmountBeforeDiscount + discountTotalAmount;
}
}
export default OrderAmount;
3️⃣ Benefit
OrderManager 클래스를 통해 얻을 수 있는 값들을 활용하여 크리스마스 이벤트에 대한 혜택들을 계산하도록 구현했다. 디데이 할인, 평일/주말 할인, 특별 할인, 증정 이벤트 할인 등과 같은 도메인 로직들을 모아 수행하도록 역할을 부여했다.
class Benefit {
static calculateDDayDiscount(visitDate) {
if (visitDate <= CONSTANTS.day.christmasDay)
return -(
CONSTANTS.price.dDayDiscountStartAmount +
(visitDate - CONSTANTS.day.initialOffset) * CONSTANTS.price.dDayMultiplier
);
return false;
}
static calculateWeekDayDiscount(orderMenusInfo, dayOfWeek) {
if (dayOfWeek >= CONSTANTS.week.sunday && dayOfWeek <= CONSTANTS.week.thursday)
return this.#calculateWeeksDiscount(orderMenusInfo, MENU.type.dessert);
return false;
}
static calculateWeekEndDiscount(orderMenusInfo, dayOfWeek) {
if (dayOfWeek >= CONSTANTS.week.friday && dayOfWeek <= CONSTANTS.week.saturday)
return this.#calculateWeeksDiscount(orderMenusInfo, MENU.type.main);
return false;
}
static calculateSpecialDiscount(visitDate, dayOfWeek) {
if (dayOfWeek === CONSTANTS.week.sunday || visitDate === CONSTANTS.day.christmasDay)
return -CONSTANTS.price.specialDiscountAmount;
return false;
}
static #calculateWeeksDiscount(orderMenusInfo, curType) {
return -orderMenusInfo
.filter(orderMenuInfo => MENU.menu[MENU.menuName[orderMenuInfo[0]]].type === curType)
.reduce((acc, cur) => acc + cur[2] * CONSTANTS.price.discountAmount, 0);
}
static getEventBadge(benefitTotalAmount) {
const { starThreshold, treeThreshold, santaThreshold } = CONSTANTS.badgeMinPrice;
const positiveBenefitTotalAmount = Math.abs(benefitTotalAmount);
if (positiveBenefitTotalAmount >= starThreshold && positiveBenefitTotalAmount < treeThreshold)
return 'star';
if (positiveBenefitTotalAmount >= treeThreshold && positiveBenefitTotalAmount < santaThreshold)
return 'tree';
if (positiveBenefitTotalAmount >= santaThreshold) return 'santa';
return undefined;
}
}
export default Benefit;
4️⃣ Giveaway
OrderAmount 클래스를 통해 얻을 수 있는 할인 전 금액을 기반으로 증정 이벤트와 관련된 기능을 수행하도록 역할을 부여했다. 총 주문 금액에 따른 증정 이벤트 혜택 여부와 증정품의 혜택 금액을 반환하는 도메인 로직들을 관리했다.
class Giveaway {
static checkAddGiveaway(orderAmountBeforeDiscount) {
if (orderAmountBeforeDiscount >= CONSTANTS.price.minTotalAmountForGiveaway) return true;
return false;
}
static calculateGiveawyDiscountAmount(orderAmountBeforeDiscount) {
const isAddGiveaway = this.checkAddGiveaway(orderAmountBeforeDiscount);
if (isAddGiveaway) return -MENU.menu[MENU.menuName.champagne].price;
return false;
}
}
export default Giveaway;
5️⃣ OrderProcessor
각 클래스의 인스턴스를 생성하고 반환하는 역할을 담당하는 클래스를 작성했다. 이를 통해 ChristmasEventController에서 processOrder 함수를 호출하여 인스턴스를 할당하고, 해당 반환값들을 구조 분해 할당하여 OutputView에서 파라미터로 전달하여 출력할 수 있는 프로세스를 간소화했다.
class OrderProcessor {
static async processOrder() {
const { visitDate, orderMenus } = await this.#inputOrder();
const orderManager = this.#createOrderManager(visitDate, orderMenus);
const orderAmount = this.#createOrderAmount(visitDate, orderManager);
const giveaway = this.#createGiveaway(orderAmount);
const benefit = this.#createBenefit(visitDate, orderManager, orderAmount, giveaway);
return { visitDate, orderMenus, orderManager, orderAmount, giveaway, benefit };
}
static async #inputOrder() {
return {
visitDate: await InputView.readVisitDate(),
orderMenus: await InputView.readOrderMenu(),
};
}
static #createOrderManager(visitDate, orderMenus) {
const orderManager = new OrderManager(visitDate, orderMenus);
const menusInfo = orderManager.getMenusInfo();
const dayOfWeek = orderManager.getDayOfWeek();
return { menusInfo, dayOfWeek };
}
static #createOrderAmount(visitDate, orderManager) {
const { menusInfo, dayOfWeek } = orderManager;
const beforeDiscount = OrderAmount.calculateAmountBeforeDiscount(menusInfo);
const totalDiscount = OrderAmount.calculateDiscountTotalAmount(visitDate, menusInfo, dayOfWeek);
const afterDiscount = OrderAmount.calculateOrderAmountAfterDiscount(
beforeDiscount,
totalDiscount,
);
return { beforeDiscount, totalDiscount, afterDiscount };
}
static #createGiveaway(orderAmount) {
const { beforeDiscount } = orderAmount;
const isAddGiveaway = Giveaway.checkAddGiveaway(beforeDiscount);
const giveawayDiscount = Giveaway.calculateGiveawyDiscountAmount(beforeDiscount);
return { isAddGiveaway, giveawayDiscount };
}
static #createBenefit(visitDate, orderManager, orderAmount, giveaway) {
const { menusInfo, dayOfWeek } = orderManager;
const { totalDiscount } = orderAmount;
const { giveawayDiscount } = giveaway;
const dDay = Benefit.calculateDDayDiscount(visitDate);
const weekDay = Benefit.calculateWeekDayDiscount(menusInfo, dayOfWeek);
const weekEnd = Benefit.calculateWeekEndDiscount(menusInfo, dayOfWeek);
const special = Benefit.calculateSpecialDiscount(visitDate, dayOfWeek);
const eventBadge = Benefit.getEventBadge(totalDiscount + giveawayDiscount);
return { dDay, weekDay, weekEnd, special, eventBadge };
}
}
export default OrderProcessor;
👀 View
1️⃣ InputView
입력과 출력에 관련된 로직들은 이번 요구사항에서 추가된 InputView와 OutputView에서 관리했다. InputView에서는 reTry 함수를 통해 입력에 대한 유효성 검사를 실시하여 올바른 값이 들어올 때까지 입력을 받고, 올바르지 않을 경우 에러 메시지를 안내하도록 구현했다.
import { Console } from '@woowacourse/mission-utils';
import MESSAGE from '../constants/message.js';
import VisitDateValidator from '../validators/VisitDateValidator.js';
import OrderMenuValidator from '../validators/OrderMenuValidator.js';
import reTry from '../utils/reTry.js';
const InputView = {
async readVisitDate() {
return reTry(async () => {
const input = await Console.readLineAsync(MESSAGE.read.visitDate);
const visitDate = Number(input);
VisitDateValidator.validateVisitDate(visitDate);
return Number(visitDate);
});
},
async readOrderMenu() {
return reTry(async () => {
const input = await Console.readLineAsync(MESSAGE.read.orderMenu);
const orderMenus = input
.split(',')
.map(menu => menu.trim())
.map(menu => menu.split('-'))
.map(menuAndCount => menuAndCount.map(str => str.trim()));
OrderMenuValidator.validateOrderMenu(orderMenus);
return orderMenus;
});
},
};
export default InputView;
2️⃣ OutputView
OutputView에서는 컨트롤러에서의 도메인을 통해 받은 값들을 출력의 역할로서 담당하는 로직들만 모아서 관리하였다. 이후 해당 기능들을 controller에서 호출하면서 기존에 작성했던 플로우 리스트를 참고하여 작성하였다.
const OutputView = {
printGreeting() {
Console.print(MESSAGE.print.greeting);
},
printNotification(visitDate) {
Console.print(`12월 ${visitDate}일에 우테코 식당에서 받을 이벤트 혜택 미리 보기!\\n`);
},
printOrderMenu(orderMenus) {
Console.print(MESSAGE.print.orderMenuResult);
orderMenus.forEach(orderMenu => Console.print(`${orderMenu[0]} ${orderMenu[1]}개`));
},
printOrderAmountBeforeDiscount(beforeDiscount) {
Console.print(MESSAGE.print.orderAmountBeforeDiscountResult);
Console.print(`${formatPriceWithCommas(beforeDiscount)}`);
},
printGiveawayMenu(isAddGiveaway) {
Console.print(MESSAGE.print.giveawayMenuResult);
if (isAddGiveaway) return Console.print(MESSAGE.print.giveawayMenuTrueResult);
return Console.print(MESSAGE.print.noResult);
},
printDDayDiscountAmount(dDayDiscount) {
if (dDayDiscount !== false)
return Console.print(`크리스마스 디데이 할인: ${formatPriceWithCommas(dDayDiscount)}`);
return false;
},
printWeekDayDiscountAmount(weekDayDiscount) {
if (weekDayDiscount !== false && weekDayDiscount !== 0)
return Console.print(`평일 할인: ${formatPriceWithCommas(weekDayDiscount)}`);
return false;
},
printWeekEndDiscountAmount(weekEndDiscount) {
if (weekEndDiscount !== false && weekEndDiscount !== 0)
return Console.print(`주말 할인: ${formatPriceWithCommas(weekEndDiscount)}`);
return false;
},
printSpecialDiscountAmount(specialDiscount) {
if (specialDiscount !== false)
return Console.print(`특별 할인: ${formatPriceWithCommas(specialDiscount)}`);
return false;
},
printGiveawyDiscountAmout(giveawayDiscount) {
if (giveawayDiscount !== false)
return Console.print(`증정 이벤트: ${formatPriceWithCommas(giveawayDiscount)}`);
return false;
},
printBenefitHistory(dDay, weekDay, weekEnd, special, giveawayDiscount) {
Console.print(MESSAGE.print.benefitHistoryResult);
const discountAmounts = [
OutputView.printDDayDiscountAmount(dDay),
OutputView.printWeekDayDiscountAmount(weekDay),
OutputView.printWeekEndDiscountAmount(weekEnd),
OutputView.printSpecialDiscountAmount(special),
OutputView.printGiveawyDiscountAmout(giveawayDiscount),
];
const allFalse = discountAmounts.every(amount => amount === false);
if (allFalse) Console.print(MESSAGE.print.noResult);
},
printBenefitTotalAmount(totalDiscount, giveawayDiscount) {
Console.print(MESSAGE.print.benefitTotalAmountResult);
const benefitTotalAmount = totalDiscount + giveawayDiscount;
Console.print(formatPriceWithCommas(benefitTotalAmount));
},
printEventBadge(eventBadge) {
Console.print(MESSAGE.print.eventBadgeResult);
if (eventBadge !== undefined) return Console.print(CONSTANTS.badge[eventBadge]);
return Console.print(MESSAGE.print.noResult);
},
printOrderAmountAfterDiscount(afterDiscount) {
Console.print(MESSAGE.print.orderAmountAfterDiscountResult);
Console.print(formatPriceWithCommas(afterDiscount));
},
};
export default OutputView;
🕹️ Controller
ChristmasEventController를 통해 프로그램의 전체적인 프로세스를 수행하도록 구현했습니다. Controller를 작성할 때는 구현 전에 작성한 플로우 리스트를 참고하여 순차적으로 작성했습니다. OrderProcessor의 processOrder 함수를 호출하여 인스턴스들을 반환하도록 설정하였고, 반환된 인스턴스들의 값을 OutputView에 있는 함수들의 파라미터로 전달하여 출력문을 반환하도록 호출했습니다. 특히, 같은 의미의 내용과 역할을 가지고 있다고 판단되는 혜택 내역과 같은 경우에는 별도의 함수printBenefitHistory로 분리하였습니다.
class ChristmasEventController {
static async start() {
OutputView.printGreeting();
const processedOrder = await OrderProcessor.processOrder();
this.#printOutput(processedOrder);
}
static #printOutput(processedOrder) {
const { visitDate, orderMenus, orderAmount, giveaway, benefit } = processedOrder;
OutputView.printNotification(visitDate);
OutputView.printOrderMenu(orderMenus);
OutputView.printOrderAmountBeforeDiscount(orderAmount.beforeDiscount);
OutputView.printGiveawayMenu(giveaway.isAddGiveaway);
this.#printBenefitHistory(benefit, giveaway);
OutputView.printBenefitTotalAmount(orderAmount.totalDiscount, giveaway.giveawayDiscount);
OutputView.printOrderAmountAfterDiscount(orderAmount.afterDiscount);
OutputView.printEventBadge(benefit.eventBadge);
}
static #printBenefitHistory(benefit, giveaway) {
OutputView.printBenefitHistory(
benefit.dDay,
benefit.weekDay,
benefit.weekEnd,
benefit.special,
giveaway.giveawayDiscount,
);
}
}
export default ChristmasEventController;
😊 객체는 객체스럽게
class Lotto {
#numbers;
constructor(numbers) {
this.#validate(numbers);
this.#numbers = numbers;
}
#validate(numbers) {
Validator.validateLotto(numbers);
}
getNumbers() {
return this.#numbers;
}
}
export default Lotto;
이전 로또 미션에서 작성한 Lotto 클래스에는 문제가 있었다. private으로 설정된 numbers 상태값을 getNumbers 함수를 통해 외부에서 호출할 수 있도록 접근 가능하게 작성한 것이 문제였다. 때문에 이번 미션에서의 공통 피드백에서 언급된 '객체는 객체스럽게 사용한다'는 개념에 위배되는 코드인 것을 알 수 있었다. 따라서, 상태값을 그대로 반환하는 것이 아닌, 객체에게 메시지를 전달하여 객체 스스로 일을 할 수 있도록 클래스를 다시 작성해야 했다.
class OrderManager {
#visitDate;
#orderMenus;
constructor(visitDate, orderMenus) {
this.#validate(visitDate, orderMenus);
this.#visitDate = visitDate;
this.#orderMenus = orderMenus;
}
#validate(visitDate, orderMenus) {
VisitDateValidator.validateVisitDate(visitDate);
OrderMenuValidator.validateOrderMenu(orderMenus);
}
getMenusInfo() {
const menuNames = this.#setMenuNames();
const menuPrices = this.#setMenuPrices(menuNames);
const menuCount = this.#setMenuCount();
return menuNames.map((element, index) => [element, menuPrices[index], menuCount[index]]);
}
getDayOfWeek() {
return new Date(new Date().getFullYear(), CONSTANTS.month.december, this.#visitDate).getDay();
}
#setMenuNames() {
return this.#orderMenus.map(orderMenu =>
Object.keys(MENU.menuName).find(key => MENU.menuName[key] === orderMenu[0]),
);
}
#setMenuPrices(menuNames) {
return menuNames.map(menuName => MENU.menu[MENU.menuName[menuName]].price);
}
#setMenuCount() {
return this.#orderMenus.map(orderMenu => Number(orderMenu[1]));
}
}
export default OrderManager;
위 코드는 이번 미션에서의 OrderManager 클래스이다. 마찬가지로 사용자로부터 입력받은 주문(orderMenu)과 방문 날짜(visitDate)를 private으로 관리하여 외부에서 변경하지 못하도록 설정하였다. 이후 해당 클래스 내부에서 동작하는 메뉴 이름, 가격, 개수를 반환하는 로직들도 private으로 관리하였고, 이를 통해 주문 정보와 방문 날짜의 요일을 반환하는 로직들은 외부에서 접근 가능하도록 public으로 설정하였다. 즉 상태값을 그대로 반환하는 것이 아닌, 상태값을 활용하여 클래스 내부에서 동작하는 로직을 통해 나온 값들을 반환할 수 있도록 작성하였다.
✅ 이제는 익숙해진 테스트코드 (feat. TDD)
이전과는 다르게 테스트 코드 작성에대한 부담감이 많이 줄어들었다. 도메인 로직과 유효성 검사에 대한 성공과 실패 케이스를 모두 고려하여 단위 테스트를 진행하였다. 또한, given, when, then의 순서를 따라가며 테스트 코드를 작성하는 데 신경을 썼고, 다양한 입력값들을 테스트할 수 있는 test.each() 메서드를 알게 되면서 더 간결하게 작성할 수 있다는 것을 알게 되었다. 또한, 단위별 테스트를 먼저 작성한 후에 도메인 로직을 구현하는 TDD 방식을 적용해보았다. 이는 각 도메인 로직과 유효성 검증들을 즉각적으로 테스트 할 수 있어 실시간으로 피드백을 받을 수 있는 장점이 있다는 것을 알게 되었다.
describe('주문 기능', () => {
test('주문 메뉴에 따른 총 주문 금액이 맞으면 금액을 반환한다.', () => {
const value = [
['해산물파스타', '2'],
['레드와인', '1'],
['초코케이크', '1'],
];
const expectedValue = 145000;
const visitDate = 5;
const orderManager = new OrderManager(visitDate, value);
const menusInfo = orderManager.getMenusInfo();
const result = OrderAmount.calculateAmountBeforeDiscount(menusInfo);
expect(result).toEqual(expectedValue);
});
// 성공에 따른 테스트
test('총 주문 금액에 따른 증정 메뉴가 있으면 true를 반환한다.', () => {
const value = 120000;
const expectedValue = true;
const result = Giveaway.checkAddGiveaway(value);
expect(result).toEqual(expectedValue);
});
// 실패에 따른 테스트
test('총 주문 금액에 따른 증정 메뉴가 없으면 false를 반환한다.', () => {
const value = 80000;
const expectedValue = false;
const result = Giveaway.checkAddGiveaway(value);
expect(result).toEqual(expectedValue);
});
});
💡 회고
특히 3주차 공통 피드백에서 언급된 '객체를 객체답게'라는 피드백이 매우 와닿았다. 해당 피드백을 통해 객체의 역할과 책임에 대해 다시 한 번 깊게 고민하게 되었고, 클래스 분리의 필요성을 더욱 명확하게 이해할 수 있었다. 이를 알지 못했더라면 상태값을 외부에서 접근 가능하도록 getter을 통해 노출시키는 객체로서의 역할만 이해하고 코드를 작성했을 것이다.
테스트 코드 작성에도 많이 익숙해진 것 같다. 이번 미션에서 TDD를 적용해보면서 실시간으로 피드백을 받을 수 있는 장점을 깨달았다. 비록 시간이 많이 소요되는 부분이 있지만, 더 견고한 로직을 작성하는 데 큰 도움이 된다고 생각한다. 따라서 앞으로도 해당 방법론을 활용하여 로직 작성에 접근할 것 같다. 뿐만 아니라, 성공과 실패에 대한 테스트 케이스를 둘다 작성하면서 앞으로의 변경사항에도 쉽게 대응할 수 있다는 점을 깨달았다.
마지막으로 메서드 라인을 15자 이내로 제한하면서 자바스크립트의 내장 API들의 역할에 대해 배우고 사용에 익숙해질 수 있었다. 이는 자동적으로 함수의 단일 책임 원칙을 준수할 수 있게 되었으며, 코드의 가독성 도한 높일 수 있다는 것을 알게 되었다.
😢 프리코스 마치며
벌써 한 달 동안의 프리코스 과정이 끝났다. 한 달이라는 기간은 사람마다 다르겠지만, 확실한 것은 이번 프리코스를 통해 많은 것을 배울 수 있었다는 것이다.
먼저, 프로그래밍 기술적인 부분에서 많은 것을 배웠다.
테스트 코드 작성을 통해 작성해야 하는 이유와 이에 익숙해 지고 올바른 객체지향 설계에 대한 이해도 얻을 수 있었다. 도메인 로직을 책임과 역할에 따라 클래스(객체)로 분리하고, 책임을 수행할 수 있도록 작성하는 방법과 더불어 캡슐화에 대한 내용도 학습했다.
상수화에 대해서도 배웠다. 조건문과 반복문에서 뿐만 아니라 변경될 수 있는 매직 넘버와 다양한 에러 메시지들을 상수화하여 유지보수를 쉽게 할 수 있다는 점을 배웠다.
기능 목록 작성에 대한 중요성과 작성법에 대해서도 알게 되었다. 프리코스 이전에는 항상 구현 전에 노트북에 손부터 올렸었는데, 이번에 기능 목록 작성을 통해 이러한 악습관을 고칠 수 있었다.
이 외에도 다양한 내용을 배웠지만, 위 4가지가 지금까지 작성한 코드를 다시 뜯어 고치고 싶을 정도로 가장 크게 와닿았던 내용들이었다.
회고에 대한 중요성과 작성하는 습관을 가질 수 있게 되었다. 기존에는 배운 내용을 정리하는 정도로만 기록했었는데, 미션을 진행하면서 공들인 부분과 시행착오를 적으면서 내가 어떤 부분이 부족하고 어떤 부분을 배울 수 있었는지를 명확하게 확인할 수 있었다. 이런한 부분에서 회고에 대해 큰 매력을 느낄 수 있었다. 하지만 내가 겪고 배우고 느낀 것들을 글로 작성하는 것은 매우 고통스러운 과정이기도 했다. 하지만 이 고통을 겪으면서 생긴 상처들이 아물면서 굳은살이 배기는 과정은, 온전한 내 것으로 만들어 성장할 수 있는 과정이라는 것을 배웠다.
지원부터 프리코스까지 오직 우테코에만 몰입한 것 같다.
인생은 여러 선택지가 있고 중요한 선택과 집중의 연속이라고 생각한다. 그만큼 우테코에 진심이었기 때문에 학과 공부, 동아리 활동, 팀 프로젝트(팀원들에겐 미안하지만…)를 모두 뒤로 미루고 프리코스에만 몰입했던 것 같다. 비록 다른 이야기일 수도 있지만, 매일 학교 정문 앞 할리스 카페에서 프리코스에만 몰입했던 나머지 어느새 할리스 멤버십 등급이 GOLD로 올랐다.
그래서 이 한 달간의 과정이 얼마나 빠르게 지나갔는지 모르겠다. 처음에는 프리코스가 끝나면 후련할 것 같다는 생각도 들었지만 오히려 공허한 마음이 더 큰 것 같다. 이 공허함을 어떻게 해결할 수 있을까? 라고 생각하면 답은 이미 정해진 것 같다.
앞으로의 배움에 끊임없이 몰입하는 것이다. 그래서 다음으로는 지금까지 배운 내용들을 1~4주차 미션들을 수정해보고 이에 대한 회고록을 작성해볼 예정이다. 몰입의 즐거움을 깨닫게 해준 우아한테크코스에게 매우 감사하다고 생각한다! 😊
⛳️ 다음 목표
- 여태까지 배운 내용들과 코드리뷰를 통해 모든 미션을 다시 구현해보기
🏃♂️ 구현 코드 보러가기
GitHub - llbllhllk/javascript-christmas-6-llbllhllk
Contribute to llbllhllk/javascript-christmas-6-llbllhllk development by creating an account on GitHub.
github.com
'🚀 우아한테크코스 6기 프리코스' 카테고리의 다른 글
[우아한테크코스 6기] 최종 코딩 테스트 - 온콜 리팩토링 회고록 (0) | 2023.12.21 |
---|---|
[우아한테크코스 6기] 1차 합격 및 최종 코딩 테스트 후기 (4) | 2023.12.17 |
[우아한테크코스 6기] 프리코스 3주차 - 로또 회고록 (0) | 2023.11.10 |
[우아한테크코스 6기] 프리코스 2주차 - 자동차 경주 회고록 (0) | 2023.11.02 |
[우아한테크코스 6기] 프리코스 1주차 - 숫자 야구 회고록 (0) | 2023.10.26 |