6기 우아한테크코스 프리코스 4주차 회고
4주차 미션에 들어가기 전
3주차 피드백
함수 라인은 15줄을 넘기지 말 것
→ 함수 라인이 15줄을 넘기면 분리하는 것을 고민해봐야 한다.
발생 가능성이 있는 예외 상황을 고민할 것
→ 정상적인 경우를 구현하는 것보다 예외 상황을 고려하는 것이 더 어렵다. 예외 상황을 고려해 프로그래밍하는 습관을 들여야 한다.
테스트 코드를 작성할 때, 발생 가능성이 있는 모든 예외 상황을 고민하자
비즈니스 로직과 UI 로직을 분리할 것
→ 비즈니스 로직과 UI 로직을 하나의 클래스가 담당하지 않도록 해야 한다. 단일 책임의 원칙을 지켜야 한다.
View 클래스가 UI 로직을 담당하도록 구성한다.
객체의 상태 접근을 제한할 것
→ 필드는 private class 필드로 구현해야 한다.
모든 접근을 private class 필드로 구성한다. 이 후, 외부에서 접근해야 하는 필드만 public으로 전환해 사용한다.
객체는 객체답게 사용할 것
→ 무분별한 getter 사용을 지양하고 객체에 메시지를 보내 객체가 로직을 수행하도록 해야 한다.
class Lotto {
#numbers
constructor(numbers) {
this.#numbers = numbers
}
getNumbers() {
return this.#numbers
}
}
위 처럼, 로직에 대한 구현은 없고 getter 메서드만 가진 객체를 만들지 말아야 한다. 객체가 일할 수 있도록 하자.
필드의 수를 줄이기 위해 노력할 것
→ 필드의 수가 많은 것은 객체의 복잡도를 높이고, 버그 발생 가능성을 높일 수 있다. 필드에 중복이 있거나, 불필요한 필드가 없는지 확인하여 필드의 수를 줄이도록 해야 한다.
class LottoResult {
#map = new Map()
#profit
#totalAmount
}
class LottoResult {
#map = new Map()
calculateProfitRate() { ... }
calculateTotalAmount() { ... }
}
필드를 선언할 때는 필요한 지에 대해 고민해보자.
예외 케이스도 테스트할 것
→ 성공 케이스만 작성하지 말아야 한다. 예외에 대한 부분도 처리해야 한다. 프로그램에서 결함이 자주 발생하는 부분 중 하나가 경계값이기 때문에 이 부분을 잘 검토해야 한다.
개발자 중심의 시선에서 벗어나서 다양한 시선을 가지도록 노력해야 한다.
테스트 코드도 코드처럼 작성할 것
→ 테스트 코드도 코드이다. 리팩토링을 통해 개선해야 하며, 반복적인 부분을 중복되지 않게 구현해야 한다.
beforeEach() 등 같은 메소드 사용의 중복되는 부분을 줄이는 노력을 해야 한다.
단위 테스트하기 어려운 코드를 단위 테스트할 것
→ 단위 테스트를 할 때, 어려운 부분은 분리하고 테스트 가능한 부분을 단위 테스트하는 노력이 필요하다. 테스트하기 어렵다면 넘어가도 된다.
작업 흐름도
첫 인사 출력
↓
예상 방문 날짜 입력 → 방문 날짜 유효성 검사
↓
주문 메뉴와 개수 입력 → 주문 메뉴 및 개수 유효성 검사
↓
이벤트 혜택 계산 및 주문 메뉴 저장
↓
총 주문 금액, 증정 메뉴, 총 혜택 금액 계산
↓
이벤트 혜택 미리보기 메시지 출력
↓
주문 메뉴, 혜택 등 내용 출력
기능 요구 사항
<애피타이저>
양송이수프(6,000), 타파스(5,500), 시저샐러드(8,000)
<메인>
티본스테이크(55,000), 바비큐립(54,000), 해산물파스타(35,000), 크리스마스파스타(25,000)
<디저트>
초코케이크(15,000), 아이스크림(5,000)
<음료>
제로콜라(3,000), 레드와인(60,000), 샴페인(25,000)
이벤트 목표
- 중복된 할인과 증정을 허용해서, 고객들이 혜택을 많이 받는다는 것을 체감할 수 있게 하는 것
- 올해 12월에 지난 5년 중 최고의 판매 금액을 달성
- 12월 이벤트 참여 고객의 5%가 내년 1월 새해 이벤트에 재참여하는 것
12월 이벤트 계획
- 크리스마스 디데이 할인
- 이벤트 기간: 2023.12.1 ~ 2023.12.25
- 1,000원으로 시작하여 크리스마스가 다가올수록 날마다 할인 금액이 100원씩 증가
- 총주문 금액에서 해당 금액만큼 할인
(e.g. 시작일인 12월 1일에 1,000원, 2일에 1,100원, ..., 25일엔 3,400원 할인)
- 평일 할인(일요일~목요일): 평일에는 디저트 메뉴를 메뉴 1개당 2,023원 할인
- 주말 할인(금요일, 토요일): 주말에는 메인 메뉴를 메뉴 1개당 2,023원 할인
- 특별 할인: 이벤트 달력에 별이 있으면 총주문 금액에서 1,000원 할인
- 증정 이벤트: 할인 전 총주문 금액이 12만 원 이상일 때, 샴페인 1개 증정
- 이벤트 기간: '크리스마스 디데이 할인'을 제외한 다른 이벤트는 2023.12.1 ~ 2023.12.31 동안 적용
크리스마스 이벤트 기간을 판단할 수 있는 값이 필요하다.
→ 배열로 값을 담아서 조건문을 활용해볼까?
평일&주말 할인 계산
→ 전체 메뉴를 객체로 표현해서 처리하는 게 편할까?
특별 할인 계산
→ 이벤트 달력에 별이 있는 날만 배열에 담아서 조건문으로 판단해볼까?
증정 이벤트
→ 총 할인 금액이 필요하다. 그리고 총 할인 금액으로 증정 이벤트를 판단할 로직이 필요하다.
혜택 금액에 따른 12월 이벤트 배지 부여
- 총혜택 금액에 따라 다른 이벤트 배지를 부여합니다. 이 배지는 2024 새해 이벤트에서 활용할 예정입니다. 배지에 따라 새해 이벤트 참여 시, 각각 다른 새해 선물을 증정할 예정입니다.
- 5천 원 이상: 별
- 1만 원 이상: 트리
- 2만 원 이상: 산타
총 혜택 금액을 값으로 배지를 판단할 수 있는 로직이 필요하다.
고객에게 안내할 이벤트 주의 사항
- 총주문 금액 10,000원 이상부터 이벤트가 적용됩니다.
- 음료만 주문 시, 주문할 수 없습니다.
- 메뉴는 한 번에 최대 20개까지만 주문할 수 있습니다.
(e.g. 시저샐러드-1, 티본스테이크-1, 크리스마스파스타-1, 제로콜라-3, 아이스크림-1의 총개수는 7개)
총 주문 금액이 만 원 이상인지 판단하는 예외 처리로 만들까?
주문한 메뉴가 음료만 있다면 예외 처리를 할까?
메뉴를 주문한 개수가 총 20개를 넘으면 예외 처리를 할까?
'12월 이벤트 플래너' 개발 요청 사항
- 고객들이 식당에 방문할 날짜와 메뉴를 미리 선택하면 이벤트 플래너가 주문 메뉴, 할인 전 총주문 금액, 증정 메뉴, 혜택 내역, 총혜택 금액, 할인 후 예상 결제 금액, 12월 이벤트 배지 내용을 보여주기를 기대합니다.
- 12월 중 식당 예상 방문 날짜는 언제인가요? (숫자만 입력해 주세요!)
- 방문할 날짜는 1 이상 31 이하의 숫자로만 입력받아 주세요.
- 1 이상 31 이하의 숫자가 아닌 경우, "[ERROR] 유효하지 않은 날짜입니다. 다시 입력해 주세요."라는 에러 메시지를 보여 주세요.
- 모든 에러 메시지는 "[ERROR]"로 시작하도록 작성해 주세요.
- 주문하실 메뉴와 개수를 알려 주세요. (e.g. 해산물파스타-2,레드와인-1,초코케이크-1)
- 고객이 메뉴판에 없는 메뉴를 입력하는 경우, "[ERROR] 유효하지 않은 주문입니다. 다시 입력해 주세요."라는 에러 메시지를 보여 주세요.
- 메뉴의 개수는 1 이상의 숫자만 입력되도록 해주세요. 이외의 입력값은 "[ERROR] 유효하지 않은 주문입니다. 다시 입력해 주세요."라는 에러 메시지를 보여 주세요.
- 메뉴 형식이 예시와 다른 경우, "[ERROR] 유효하지 않은 주문입니다. 다시 입력해 주세요."라는 에러 메시지를 보여 주세요.
- 중복 메뉴를 입력한 경우(e.g. 시저샐러드-1,시저샐러드-1), "[ERROR] 유효하지 않은 주문입니다. 다시 입력해 주세요."라는 에러 메시지를 보여 주세요.
- 모든 에러 메시지는 "[ERROR]"로 시작하도록 작성해 주세요.
- 주문 메뉴의 출력 순서는 자유롭게 출력해 주세요.
- 총혜택 금액에 따라 이벤트 배지의 이름을 다르게 보여 주세요.
- 총혜택 금액 = 할인 금액의 합계 + 증정 메뉴의 가격
- 할인 후 예상 결제 금액 = 할인 전 총주문 금액 - 할인 금액
- 증정 메뉴
- 증정 이벤트에 해당하지 않는 경우, 증정 메뉴 "없음"으로 보여 주세요.
- 혜택 내역
- 고객에게 적용된 이벤트 내역만 보여 주세요.
- 적용된 이벤트가 하나도 없다면 혜택 내역 "없음"으로 보여 주세요.
- 혜택 내역에 여러 개의 이벤트가 적용된 경우, 출력 순서는 자유롭게 출력해주세요.
- 이벤트 배지
- 이벤트 배지가 부여되지 않는 경우, "없음"으로 보여 주세요.
적용된 이벤트가 없는 경우
안녕하세요! 우테코 식당 12월 이벤트 플래너입니다.
12월 중 식당 예상 방문 날짜는 언제인가요? (숫자만 입력해 주세요!)
26
주문하실 메뉴를 메뉴와 개수를 알려 주세요. (e.g. 해산물파스타-2,레드와인-1,초코케이크-1)
타파스-1,제로콜라-1
12월 26일에 우테코 식당에서 받을 이벤트 혜택 미리 보기!
<주문 메뉴>
타파스 1개
제로콜라 1개
<할인 전 총주문 금액>
8,500원
<증정 메뉴>
없음
<혜택 내역>
없음
<총혜택 금액>
0원
<할인 후 예상 결제 금액>
8,500원
<12월 이벤트 배지>
없음
적용된 이벤트가 있는 경우
안녕하세요! 우테코 식당 12월 이벤트 플래너입니다.
12월 중 식당 예상 방문 날짜는 언제인가요? (숫자만 입력해 주세요!)
3
주문하실 메뉴를 메뉴와 개수를 알려 주세요. (e.g. 해산물파스타-2,레드와인-1,초코케이크-1)
티본스테이크-1,바비큐립-1,초코케이크-2,제로콜라-1
12월 3일에 우테코 식당에서 받을 이벤트 혜택 미리 보기!
<주문 메뉴>
티본스테이크 1개
바비큐립 1개
초코케이크 2개
제로콜라 1개
<할인 전 총주문 금액>
142,000원
<증정 메뉴>
샴페인 1개
<혜택 내역>
크리스마스 디데이 할인: -1,200원
평일 할인: -4,046원
특별 할인: -1,000원
증정 이벤트: -25,000원
<총혜택 금액>
-31,246원
<할인 후 예상 결제 금액>
135,754원
<12월 이벤트 배지>
산타
프로그래밍 요구 사항
- indent(인덴트, 들여쓰기) depth를 3이 넘지 않도록 구현한다. 2까지만 허용한다.
- 예를 들어 while문 안에 if문이 있으면 들여쓰기는 2이다.
- 힌트: indent(인덴트, 들여쓰기) depth를 줄이는 좋은 방법은 함수(또는 메서드)를 분리하면 된다.
- Jest를 이용하여 본인이 정리한 기능 목록이 정상 동작함을 테스트 코드로 확인한다.
- 함수(또는 메서드)의 길이가 15라인을 넘어가지 않도록 구현한다.
- 함수(또는 메서드)가 한 가지 일만 하도록 최대한 작게 만들어라.
- else를 지양한다.
- 힌트: if 조건절에서 값을 return하는 방식으로 구현하면 else를 사용하지 않아도 된다.
- 때로는 if/else, switch문을 사용하는 것이 더 깔끔해 보일 수 있다. 어느 경우에 쓰는 것이 적절할지 스스로 고민해 본다.
- 도메인 로직에 단위 테스트를 구현해야 한다. 단, UI(Console.readLineAsync, Console.print) 로직에 대한 단위 테스트는 제외한다.
- 핵심 로직을 구현하는 코드와 UI를 담당하는 로직을 구분한다.
- 사용자가 잘못된 값을 입력할 경우 throw문을 사용해 예외를 발생시킨다. 그런 다음, "[ERROR]"로 시작하는 에러 메시지를 출력하고 해당 부분부터 입력을 다시 받는다.
추가 요구 사항
아래에 제공되는 InputView, OutputView 객체를 활용해 구현한다.
- 입력과 출력을 담당하는 객체를 별도로 구현한다.
- InputView, OutputView의 파일 경로는 변경할 수 있다.
- InputView, OutputView의 메서드의 이름과 인자는 필요에 따라 추가하거나 변경할 수 있다.
- 값 출력을 위해 필요한 메서드를 추가할 수 있다.
const InputView = {
async readDate() {
const input = await Console.readLineAsync("12월 중 식당 예상 방문 날짜는 언제인가요? (숫자만 입력해 주세요!)");
// ...
},
// ...
};
export default InputView;
const OutputView = {
printMenu() {
Console.print("<주문 메뉴>");
// ...
},
// ...
};
export default OutputView;
InputView, OutputView를 제외하고 이전 미션들에 주어진 요구 조건과 같았다.
InputView, OutputView도 로또 미션에서 다른 사람들의 정보 공유를 통해 미리 알고 있었기 때문에 당황하지 않았다.
어려운 것은, OutputView에 이벤트 내역들을 어떻게 처리해야 할까에 대한 고민이었다.
지저분하게 처리하고 싶지 않았다. 그러나, 깨끗하게 처리하는 방법을 떠올리지 못했다.
구현
이전 미션들과 마찬가지로 주어진 기능 요구 사항에서 나타난 순서대로 작업을 시작했다.
입력을 받고, 유효성 검사를 진행하고, 입력한 값을 활용해 필요한 계산을 진행하고, 결과물을 출력하도록.
고민한 것들
Model, View, Controller를 어떻게 구현해야 하는가?
- 예외 처리하는 부분을 구현할까?
- 주어진 View를 사용하면 되겠다.
- Controller는 모두 따로 구현해야 하는 걸까? App.js에서 구현해도 될까?
이벤트 할인은 어떻게 처리해야 할까?
위 이미지를 보면, 우리가 아는 주말이라는 개념이 조금 다르다. 토요일, 일요일이 아닌 금요일과 토요일이 주말로 되어있다.
미션이 모두 종료되고 나서야 다른 사람들에 의해 알게 되었지만, 위의 달력들은 new Date()로 값을 가져와 처리할 수 있도록 되어 있었던 것이었다.
이걸 몰랐던 나는 어떻게 처리해야 하는 걸까? 고민이 많았다.
결국 위의 방법이 떠오르지 않아 상수로 주말 날짜를 배열로 선언하고, 별이 있는 특별 날짜를 배열로 선언하고, 이벤트가 적용되지 않는 날짜를 배열로 선언하여 총 3개의 배열을 만들어 해당 배열에 날짜가 포함되어 있는지 아닌지로 판별하였다.
(참 비효율적이고, 많이 부족하다는 게 바로 드러난다.)
메뉴는 어떻게 저장하고 비교해야 할까?
사실, 이 부분은 고민하는 시간이 짧았다. 처음에는 Key, Value로 저장해야 한다는 것은 바로 떠올랐지만, 그래서 어떤 식으로 구현해야 하는지가 바로 떠오르지 않았다.
기능 요구 사항을 보면 <애피타이저>, <메인>, <디저트>, <음료>로 나타낸 것을 보고, 이 네 가지로 나눠서 저장하면 되겠다는 생각을 했다.
export const MENUS = Object.freeze({
appetizer: {
'양송이수프': 6000,
'타파스': 5500,
'시저샐러드': 8000,
},
main: {
'티본스테이크': 55000,
'바비큐립': 54000,
'해산물파스타': 35000,
'크리스마스파스타': 25000,
},
dessert: {
'초코케이크': 15000,
'아이스크림': 5000,
},
beverage: {
'제로콜라': 3000,
'레드와인': 60000,
'샴페인': 25000,
},
})
부족한 것들
MVC 패턴을 제대로 구현하지 못했다
Model에서 구현해야 할 로직들을 Utils에 구현하고 나서 미션을 마쳤다.
src
-utils
- calBadge
- calPresentation
- calSpecial
- calTotalAmount
- calTotalBenefitAmount
- calWeekday
- calWeekend
- getAllBenefit
Model에서는 데이터의 유효성 검사만 진행하여, 로또 미션에서 더 발전하지 못한 채 마무리된 것이 가장 부족한 점 중 하나이다. 이를 Model에서 로직으로 구현하는 것으로 개선해 나갈 계획이다.
OutputView 처리가 비효율적이다
주문 메뉴와 개수들을 문자열로 저장한 뒤, 출력 메소드에서 전달받아 내보내는 식으로 구현했다.
printMenu(menuCount) {
MissionUtils.Console.print(OUTPUT_MESSAGE.printOrderedMenu + menuCount);
},
그것보다 더 비효율적인 것은 혜택 내역을 출력하는 것이었다.
혜택 내역을 담은 메소드를 전달받아 각 혜택을 출력하는 식으로 구현했다.
또한, 이벤트에 해당하는지, 해당하지 않는지 여부를 출력에서 판단하였다.
printBenefit(benefits, isEvent) {
const output = isEvent
? OUTPUT_MESSAGE.printBenefit +
(benefits.christmasDiscount !== 0 ? `${OUTPUT_MESSAGE.printChristmasDiscount} -${benefits.christmasDiscount.toLocaleString()}원` + LINE_SEPARATOR : '') +
(benefits.weekdayDiscount !== 0 ? `${OUTPUT_MESSAGE.printWeekdayDiscount} -${benefits.weekdayDiscount.toLocaleString()}원` + LINE_SEPARATOR : '') +
(benefits.weekendDiscount !== 0 ? `${OUTPUT_MESSAGE.printWeekendDiscount} -${benefits.weekendDiscount.toLocaleString()}원` + LINE_SEPARATOR : '') +
(benefits.starSpecialDiscount !== 0 ? `${OUTPUT_MESSAGE.printSpecialDiscount} -${benefits.starSpecialDiscount.toLocaleString()}원` + LINE_SEPARATOR : '') +
(benefits.presentationEvent[1] !== 0 ? `${OUTPUT_MESSAGE.printPresentationDiscount} -${benefits.presentationEvent[1].toLocaleString()}원` : '')
: OUTPUT_MESSAGE.printBenefit + OUTPUT_MESSAGE.printNothing;
MissionUtils.Console.print(output);
},
미션이 종료되고 나서, 다른 사람들의 코드를 보고 나서 배움을 얻었다.
Model에서 계산하는 로직을 구현하는데, 이 때 각각 혜택을 "Key, Value"로 저장하여 처리하는 방법을 볼 수 있었다.
결국, Model로 로직을 처리하지 못한 것이 가장 부족한 점이라고 생각이 든다.
MVC 패턴의 이해도를 키우고, 로직을 구현하는 역량와 습관을 들이는 노력이 더욱 필요하다.