[우아한테크코스 6기] 프리코스 4주차 회고 && 최종 리팩토링 후기

조경찬 (Jo Gyungchan)·2023년 12월 10일
0

우아한테크코스

목록 보기
4/4
post-thumbnail

이 글은 프리코스 4주차 회고 글, 그리고 프리코스가 끝난 뒤 진행한 리팩토링 결과까지 포함하고 있다는 것을 미리 말씀드립니다!! 😀
리팩토링 후 달라진 점도 궁금하신 분들을 끝까지 다 봐주세요!~🧐🧐


프리코스 3주 차 공통 피드백

4주 차 미션에 대한 글을 작성하기 이전에 먼저 3주 차 공통 피드백 내용들은 다음과 같습니다.

  • 함수(메서드) 라인에 대한 기준
  • 발생할 수 있는 예외 상황에 대해 고민한다
  • 비즈니스 로직과 UI 로직을 분리한다
  • 연관성이 있는 상수는 static final 대신 enum을 활용한다
  • final 키워드를 사용해 값의 변경을 막는다
  • 객체의 상태 접근을 제한한다
  • 객체는 객체스럽게 사용한다
  • 필드(인스턴스 변수)의 수를 줄이기 위해 노력한다
  • 성공하는 케이스 뿐만 아니라 예외에 대한 케이스도 테스트한다
  • 테스트 코드도 코드다
  • 테스트를 위한 코드는 구현 코드에서 분리되어야 한다
  • 단위 테스트하기 어려운 코드를 단위 테스트하기
  • private 함수를 테스트 하고 싶다면 클래스(객체) 분리를 고려한다

3주 차 공통 피드백에 대해 자세히 보고 싶은 분은 3주 차 공통 피드백에서 볼 수 있습니다.

4주 차 미션을 구현하기 이전 3주 차 공통 피드백 내용들을 준수할 수 있도록 노력했습니다.

4주차


크리스마스 프로모션

4주차 미션은 크리스마스 프로모션을 구현하는 것이었습니다.
미션을 진행하는 방식은 다음과 같이 나와있었습니다.

🔍 진행 방식

  • 미션은 기능 요구 사항, 프로그래밍 요구 사항, 과제 진행 요구 사항 세 가지로 구성되어 있다.
  • 세 개의 요구 사항을 만족하기 위해 노력한다. 특히 기능을 구현하기 전에 기능 목록을 만들고, 기능 단위로 커밋 하는 방식으로 진행한다.
  • 기능 요구 사항에 기재되지 않은 내용은 스스로 판단하여 구현한다.

매주 마찬가지로 미션을 진행하기 전, 코수타 영상을 보게 되었습니다.
코수타 영상에서 4주 차 미션이 엄청 어렵다고 언급해주셨고, 실제로 미션을 확인했을 때는 충격에 빠져버렸습니다...

3주 차 미션까지 그래도 시간 안에 구현을 완성하면서 구현 못할 정도로 어렵진 않겠지??라고 생각했었는데... 정말 정말 미션을 처음 확인하고 이게 맞나?? 라고 생각이 들었을 정도로 충격에 빠졌었습니다.

주어진 요구 사항 중 일부를 가져와보았습니다!!

하지만 얼른 정신을 차리고 해낼 수 있다는 마음 가짐을 통해 미션을 구현해나가기 시작했습니다. (근데 정말 어려웠어요....)


요구 사항 분석

이전 주차 공통 피드백을 통해, 기능 목록을 작성할 때 변경 가능한 클래스명이나 메서드명 등을 작성하지 않는 것이 좋다는 것을 알게 되었습니다.

이번 주차에서도 이를 기억하고 최대한 변경 가능한 부분들에 대해선 자세히 작성하지 않도록 신경쓰면서 요구 사항을 작성해볼 수 있었습니다.

# 🎄 미션 - 크리스마스 프로모션 🎄

## 12월 이벤트를 위한 개발 요청

- 이벤트 예산은 걱정 안해도 된다.

### 메뉴
- 에피타이저: 양송이수프(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/3, 12/10, 12/17, 12/24, 12/25, 12/31
- 증정 이벤트: 할인 전 총주문 금액이 12만 원 이상일 때, 샴페인 1개 증정
- 이벤트 기간: '크리스마스 디데이 할인'을 제외한 다른 이벤트는 2023.12.1 ~ 2023.12.31 동안 적용

### 혜택 금액에 따른 12월 이벤트 배지 부여
- 총혜택 금액에 따라 다른 이벤트 배지를 부여
  - 5천 원 이상: 별
  - 1만 원 이상: 트리
  - 2만 원 이상: 산타

### 고객에게 안내할 이벤트 주의 사항
- 총주문 금액 10,000원 이상부터 이벤트가 적용
- 음료만 주문 시, 주문할 수 없음
- 메뉴는 한 번에 최대 20개까지만 주문 가능 (e.g.시저샐러드-1, 티본스테이크-3의 총개수는 4)

### '12월 이벤트 플래너' 개발 요청 사항
- 고객들이 식당에 방문할 날짜와 메뉴를 미리 선택하면 이벤트 플래너가 주문 메뉴, 할인 전 총주문 금액, 증정 메뉴, 혜택 내역, 총혜택 금액, 할인 후 예상 결제 금액, 12월 이벤트 배지 내용을 보여주기를 기대합니다.

#### 입력
- 12월 중 식당 예상 방문 날짜는 언제인가요? (숫자만 입력해 주세요!)
  - 방문할 날짜는 1 이상 31 이하의 숫자로만 입력받아 주세요.
  - 1 이상 31 이하의 숫자가 아닌 경우, "[ERROR] 유효하지 않은 날짜입니다. 다시 입력해 주세요."라는 에러 메시지를 보여 주세요.
  - 모든 에러 메시지는 "[ERROR]"로 시작하도록 작성해 주세요.
- 주문하실 메뉴와 개수를 알려 주세요. (e.g. 해산물파스타-2,레드와인-1,초코케이크-1)
  - 고객이 메뉴판에 없는 메뉴를 입력하는 경우, "[ERROR] 유효하지 않은 주문입니다. 다시 입력해 주세요."라는 에러 메시지를 보여 주세요.
  - 메뉴의 개수는 1 이상의 숫자만 입력되도록 해주세요. 이외의 입력값은 "[ERROR] 유효하지 않은 주문입니다. 다시 입력해 주세요."라는 에러 메시지를 보여 주세요.
  - 메뉴 형식이 예시와 다른 경우, "[ERROR] 유효하지 않은 주문입니다. 다시 입력해 주세요."라는 에러 메시지를 보여 주세요.
  - 중복 메뉴를 입력한 경우(e.g. 시저샐러드-1,시저샐러드-1), "[ERROR] 유효하지 않은 주문입니다. 다시 입력해 주세요."라는 에러 메시지를 보여 주세요.
  - 모든 에러 메시지는 "[ERROR]"로 시작하도록 작성해 주세요.

#### 출력
- 주문 메뉴의 출력 순서는 자유롭게 출력해 주세요.
- 총혜택 금액에 따라 이벤트 배지의 이름을 다르게 보여 주세요.
- 총혜택 금액 = 할인 금액의 합계 + 증정 메뉴의 가격
- 할인 후 예상 결제 금액 = 할인 전 총주문 금액 - 할인 금액
- 증정 메뉴
  - 증정 이벤트에 해당하지 않는 경우, 증정 메뉴 "없음"으로 보여 주세요.
- 혜택 내역
  - 고객에게 적용된 이벤트 내역만 보여 주세요.
  - 적용된 이벤트가 하나도 없다면 혜택 내역 "없음"으로 보여 주세요.
  - 혜택 내역에 여러 개의 이벤트가 적용된 경우, 출력 순서는 자유롭게 출력해주세요.
- 이벤트 배지
  - 이벤트 배지가 부여되지 않는 경우, "없음"으로 보여 주세요.

---

## 🕹 구현할 기능 목록

### 변환 기능
- [x] 입력 형식에 맞게 입력된 메뉴와 개수를 통해 주문 메뉴를 알 수 있다. 

### 날짜 기능
- [x] 12월 중 식당 예상 방문 날짜를 알 수 있다.
  - [x] **[예외 처리]** 예상 방문 날짜가 1보다 작거나 31보다 크면 예외가 발생한다.

### 메뉴 기능
- [x] 메뉴에 대한 타입을 알 수 있다.
  - [x] 타입은 애피타이저, 메인, 디저트, 음료로 총 4가지로 구성되어있다.
- [x] 타입별 메뉴를 알 수 있다.
  - [x] 에피타이저: 양송이수프(6,000), 타파스(5,500), 시저샐러드(8,000)
  - [x] 메인: 티본스테이크(55,000), 바비큐립(54,000), 해산물파스타(35,000), 크리스마스파스타(25,000)
  - [x] 디저트: 초코케이크(15,000), 아이스크림(5,000)
  - [x] 음료: 제로콜라(3,000), 레드와인(60,000), 샴페인(25,000)
- [x] 이름을 통해 메뉴를 찾을 수 있다.

### 주문 기능
- [x] 주문할 메뉴와 개수를 알 수 있다.
  - [x] **[예외 처리]** 주문한 메뉴의 개수가 1보다 작을 경우, "[ERROR] 유효하지 않은 주문입니다. 다시 입력해 주세요."라는 메시지와 함께 예외가 발생한다.
  - [x] **[예외 처리]** 메뉴판에 없는 메뉴가 입력된 경우, "[ERROR] 유효하지 않은 주문입니다. 다시 입력해 주세요."라는 메시지와 함께 예외가 발생한다.
- [x] 전체 주문 내역을 알 수 있다.
  - [x] 전체 주문 내역이 올바르지 검증할 수 있다. 
  - [x] **[예외 처리]** 주문한 메뉴가 없는 경우, 예외가 발생한다.
  - [x] **[예외 처리]** 중복 메뉴가 존재하는 경우, "[ERROR] 유효하지 않은 주문입니다. 다시 입력해 주세요."라는 메시지와 함께 예외가 발생한다.
  - [x] **[예외 처리]** 음료만 주문 시, 예외가 발생한다.
  - [x] **[예외 처리]** 메뉴의 총 개수가 20개를 넘을 시, 예외가 발생한다.
- [x] 할인 전 총주문 금액을 계산할 수 있다.

- [x] 주문을 할 수 있다.

### 할인 기능
- [x] 진행하는 할인 종류를 알 수 있다.
  - [x] 크리스마스 디데이 할인
  - [x] 평일 할인(일요일 ~ 목요일)
  - [x] 주말 할인(금요일, 토요일)
  - [x] 특별 할인
  - [x] 증정 이벤트

- [x] 할인 종류 별 적용 가능한 날짜를 알 수 있다.
  - [x] 크리스마스 디데이 할인
    - [x] 이벤트 기간: 2023.12.1 ~ 2023.12.25
  - [x] 평일 할인(일요일 ~ 목요일)
    - [x] 12월의 모든 평일 (일요일 ~ 목요일)
  - [x] 주말 할인(금요일, 토요일)
    - [x] 12월의 모든 주말 (금요일, 토요일)
  - [x] 특별 할인
    - [x] 달력에 별이 있는 날: 12/3, 12/10, 12/17, 12/24, 12/25, 12/31
  - [x] 증정 이벤트
    - [x] 12월의 모든 날짜

### 혜택 기능
- [x] 고객에게 적용 가능한 이벤트 내역을 알 수 있다.
  - [x] 총주문 금액 10,000원 이상부터 이벤트가 적용

- [x] 고객에게 적용 가능한 총혜택 내역을 확인할 수 있다.
  - [x] 크리스마스 디데이 할인
    - [x] 1,000원으로 시작하여 크리스마스가 다가올수록 날마다 할인 금액이 100원씩 증가
    - [x] 총 주문 금액에서 해당 금액만큼 할인 (e.g.시작일인 12월 1일에 1,000원, 2일에 1,100원, ... 25일엔 3,400원 할인)
    - [x] 평일 할인(일요일 ~ 목요일)
      - [x] 평일에는 디저트 메뉴를 메뉴 1개당 2,023원 할인
    - [x] 주말 할인(금요일, 토요일)
      - [x] 주말에는 메인 메뉴를 메뉴 1개당 2,023원 할인
    - [x] 특별 할인
      - [x] 이벤트 달력에 별이 있으면 총주문 금액에서 1,000원 할인
    - [x] 증정 이벤트
      - [x] 할인 전 총주문 금액이 12만 원 이상일 때, 샴페인 1개 증정 
    
- [x] 총혜택 금액을 알 수 있다. (할인 금액의 합계 + 증정 메뉴의 가격)
- [x] 할인 후 예상 결제 금액을 알 수 있다.
  - [x] 할인 전 총주문 금액 - 할인 금액

- [x] 총혜택 금액에 따라 고객에게 적용 가능한 이벤트 배지를 알 수 있다.
  - [x] 5천 원 이상: 별
  - [x] 1만 원 이상: 트리
  - [x] 2만 원 이상: 산타

- [x] 증정 메뉴를 받을 수 있다.

### 입력 기능
- [x] **[공통 예외 처리]** 입력이 공백이면 예외가 발생한다.
- [x] 12월 중 식당 예상 방문 날짜를 입력받는다.
  - [x] **[예외 처리]** 예상 방문 날짜에 대한 입력이 숫자가 아니라면 예외가 발생한다.
- [x] 주문하실 메뉴와 개수를 입력받는다. (e.g. 해산물파스타-2,레드와인-1,초코케이크-1)
  - [x] **[예외 처리]** 메뉴 입력 형식이 예시와 다른 경우, "[ERROR] 유효하지 않은 주문입니다. 다시 입력해 주세요."라는 메시지와 함께 예외가 발생한다.
- [x] 사용자가 잘못된 값을 입력한 경우 예외를 발생시키고, 그 부분부터 다시 입력을 받는다.

### 출력 기능
- [x] 시작 메세지를 출력한다.
- [x] 이벤트 혜택을 미리 보여준다는 메시지를 출력한다.
- [x] 총 주문 메뉴를 출력한다.
  - [x] 총 주문 메뉴들을 순서 상관없이 자유롭게 출력한다.
- [x] 식당에서 받을 이벤트 혜택을 출력한다.
  - [x] 할인 전 총 주문 금액을 출력한다.
  - [x] 증정 메뉴를 출력한다.
    - [x] 증정 이벤트에 해당하지 않는 경우, 증정 메뉴 "없음"으로 출력한다.
  - [x] 혜택 내역을 출력한다.
    - [x] 고객에게 적용된 이벤트 내역만 출력한다.
    - [x] 적용된 이벤트가 하나도 없다면 혜택 내역 "없음"으로 출력한다.
    - [x] 혜택 내역에 여러 개의 이벤트가 적용된 경우, 출력 순서는 자유롭게 출력한다.
  - [x] 총 혜택 금액을 출력한다.
  - [x] 할인 후 예상 결제 금액을 출력한다.
  - [x] 총혜택 금액에 따라 이벤트 배지의 이름을 다르게 출력한다.
    - [x] 이벤트 배지가 부여되지 않는 경우, "없음"으로 출력한다.
- [x] 예외가 발생한 경우, 예외 메세지를 출력한다.
  - [x] 모든 예외 메세지는 "[ERROR]"로 시작한다.

---

## 🚨 과제 제출 전 체크 리스트
- [x] 요구 사항에 명시된 출력값 형식을 지켰는지 확인
- [x] 모든 테스트가 성공하는지 확인
  - [x] 터미널에서 `./gradlew clean test`가 통과하는지 확인
  - [x] `ApplicationTest`의 모든 테스트가 통과하는지 확인 
- [x] 자바 버전이 17인지 확인
- [x] indent depth가 3을 넘지 않는지 확인 (2까지만 허용)
- [x] 3항 연산자를 썼는지 확인 (3항 연산자를 허용 x)
- [x] 함수의 길이가 15라인을 넘어가는지 확인 (15라인 넘어가는 것을 허용 x)
- [x] else 예약어를 썼는지 확인 (else 예약어를 허용 x)
- [x] 도메인 로직에 단위 테스트를 구현했는지 확인
- [x] 예외가 발생한 경우, IllegalArgumentException을 발생시키고 "[ERROR]"로 시작하는 에러 메시지를 출력 후 그 부분부터 입력받는지 확인
- [x] 사용자가 입력하는 값은 `camp.nextstep.edu.missionutils.Console`의 `readLine()`을 활용하는지 확인

이건 제가 4주 차 미션을 진행하기 이전 크리스마스 프로모션 규칙구현할 기능 목록을 작성한 내용들입니다.


핵심 기능

절차지향적 검증

이번 미션에서 주어진 예외 상황들을 빠뜨리지 않기 위해 꼼꼼히 살펴보고 작성해보았습니다.

그 중, 전체 주문 내역에 대해서 다음과 같이 예외 상황들을 작성할 수 있었습니다.

  • 전체 주문 내역 검증
    • [예외 처리] 주문한 메뉴가 없는 경우, 예외가 발생한다.
    • [예외 처리] 중복 메뉴가 존재하는 경우, "[ERROR] 유효하지 않은 주문입니다. 다시 입력해 주세요."라는 메시지와 함께 예외가 발생한다.
    • [예외 처리] 음료만 주문 시, 예외가 발생한다.
    • [예외 처리] 메뉴의 총 개수가 20개를 넘을 시, 예외가 발생한다.

이를 Orders라는 전체 주문 내역 객체 안에서 검증하는 기능을 함께 구현하면 Orders 객체는 실제 주문 관련 메서드보다 검증 관련 메서드들이 대부분 위치하게 되었습니다.

이를 해결하기 위해, OrderValidator라는 주문 내역을 검증하는 객체를 새로 만들어보았습니다.

public class OrderValidator {
    protected static final String INVALID_ORDER_EXCEPTION_MESSAGE = "유효하지 않은 주문입니다. 다시 입력해 주세요.";
    private static final String ORDER_ONLY_DRINKS_EXCEPTION_MESSAGE = "음료만 주문할 수 없습니다. 다시 입력해 주세요.";
    private static final String LARGER_THAN_MAXIMUM_ORDER_COUNT_EXCEPTION_FORMAT =
            "메뉴는 최대 20개까지만 주문할 수 있습니다. 다시 입력해 주세요. (현재 주문한 메뉴 개수 = %d)";

    private static final int MAXIMUM_ORDER_COUNT = 20;

    public void validate(List<Order> orders) {
        validateEmptyOrder(orders);
        validateDuplicatedMenu(orders);
        validateOrderOnlyDrinks(orders);
        validateTotalOrderCount(orders);
    }

    private void validateEmptyOrder(List<Order> orders) {
        if (orders.isEmpty()) {
            throw new IllegalArgumentException(INVALID_ORDER_EXCEPTION_MESSAGE);
        }
    }

    private void validateDuplicatedMenu(List<Order> orders) {
        if (hasDuplicatedMenu(orders)) {
            throw new IllegalArgumentException(INVALID_ORDER_EXCEPTION_MESSAGE);
        }
    }

    private boolean hasDuplicatedMenu(List<Order> orders) {
        return orders.stream()
                .map(Order::getMenu)
                .distinct()
                .count() != orders.size();
    }

    private void validateOrderOnlyDrinks(List<Order> orders) {
        if (hasOrderedOnlyDrinks(orders)) {
            throw new IllegalArgumentException(ORDER_ONLY_DRINKS_EXCEPTION_MESSAGE);
        }
    }

    private boolean hasOrderedOnlyDrinks(List<Order> orders) {
        return orders.stream()
                .allMatch(Order::isDrink);
    }

    private void validateTotalOrderCount(List<Order> orders) {
        int totalOrderCount = calculateTotalOrderCount(orders);

        if (isLargerThanMaximumOrderCount(totalOrderCount)) {
            throw new IllegalArgumentException(
                    String.format(LARGER_THAN_MAXIMUM_ORDER_COUNT_EXCEPTION_FORMAT, totalOrderCount));
        }
    }

    private int calculateTotalOrderCount(List<Order> orders) {
        return orders.stream()
                .map(Order::getCount)
                .reduce(Constants.INITIAL_COUNT, Integer::sum);
    }

    private boolean isLargerThanMaximumOrderCount(int totalOrderCount) {
        return totalOrderCount > MAXIMUM_ORDER_COUNT;
    }
}

이처럼 OrderValidator라는 주문 내역을 검증하는 클래스를 따로 만들어 Orders 객체의 대한 책임을 줄일 수 있었습니다.

그러면 다음과 같이 궁금하신 분들도 있을 거 같았습니다.

어?? 그러면 모든 객체에 대해 검증하는 클래스를 다 따로 만들면 모든 객체가 해당 역할만 수행할 수 있어 무조건 따로 만드는게 좋은 거 아닌가요❓❓❓

다음 질문에 대한 답을 간단하게 해보겠습니다.

제가 검증하는 클래스를 분리하면서 느낀 장점은 위에서 설명드린대로 객체들이 관련된 기능만을 가질 수 있어 가독성이 향상되고 클래스의 역할과 책임이 명확히 드러나며 유지보수성을 높일 수 있다는 것이었습니다.

반대로, 단점으로 전체 주문 내역을 검증하기 위해서 OrderValidatorvalidate 메서드를 필수적으로 호출해야만 전체 주문 내역이 올바른지 검증할 수 있다는 점이었습니다.
만약 개발을 나 혼자서 진행하는 것이라면 까먹지 않고 validate 메서드를 호출할 수 있겠지만, 만약 팀 프로젝트로 진행하는 중이고 다른 구성원이 validate 메서드를 호출하지 않았다면 올바르지 않은 주문 내역을 통해 프로그램을 동작할 수 있다는 치명적인 단점을 포함할 수 있다는 것을 느꼈습니다.

결론적으로 장점과 단점 모두 존재하고, 무분별하게 검증 클래스를 따로 만드는 것은 올바르지 않다고 생각할 수 있었습니다.


Enum && 함수형 인터페이스 활용

3주 차 공통 피드백에 나온대로, 연관성이 있는 상수는 static final 대신 enum을 활용하기 위해 노력해보았습니다.

요구 사항 중 총혜택 금액에 따라 다른 이벤트 배지를 부여해야 하는 요구 사항이 있었고, 이를 Enum을 이용하여 구현해볼 수 있었습니다.

public enum EventBadge {
    SANTA("산타", totalBenefitAmount -> totalBenefitAmount >= 20000),
    TREE("트리", totalBenefitAmount -> totalBenefitAmount >= 10000 && totalBenefitAmount < 20000),
    STAR("별", totalBenefitAmount -> totalBenefitAmount >= 5000 && totalBenefitAmount < 10000),
    NOTHING("없음", totalBenefitAmount -> totalBenefitAmount < 5000);

    private final String name;
    private final Predicate<Integer> predicate;

    EventBadge(String name, Predicate<Integer> predicate) {
        this.name = name;
        this.predicate = predicate;
    }

    public static EventBadge of(int totalBenefitAmount) {
        return Arrays.stream(values())
                .filter(eventBadge -> eventBadge.isMatch(totalBenefitAmount))
                .findFirst()
                .orElse(NOTHING);
    }

    private boolean isMatch(int totalBenefitAmount) {
        return predicate.test(totalBenefitAmount);
    }

    public String getName() {
        return name;
    }
}

또한, 총혜택 금액에 따라 이벤트 배지를 부여하기 위해 Predicate라는 함수형 인터페이스를 적용해보았습니다.

@FunctionalInterface
public interface Predicate<T> {

    boolean test(T t);
}    

이는 1개의 인자를 통해 주어진 검증을 진행하고 결과를 참, 거짓으로 반환해주는 함수형 인터페이스입니다.

이를 통해, 총혜택 금액에 따른 이벤트 배지를 쉽게 찾고 반환할 수 있었습니다.


추상 클래스 활용

이번 미션에서 각각의 이벤트에 대한 클래스를 분리하였습니다.
이러한 이벤트 클래스들은 공통적인 메서드와 변수를 가지게 되었고, 이를 효과적으로 처리하기 위해 추상 클래스를 활용해보았습니다.

public abstract class AbstractEvent implements Event {
    private final List<Integer> applicableDates;

    public AbstractEvent() {
        this.applicableDates = initializeApplicableDates();
    }

    abstract List<Integer> initializeApplicableDates();

    @Override
    public boolean isApplicable(VisitDate visitDate, Orders orders) {
        return visitDate.isIncludedIn(applicableDates);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        AbstractEvent that = (AbstractEvent) o;
        return Objects.equals(applicableDates, that.applicableDates);
    }

    @Override
    public int hashCode() {
        return Objects.hash(applicableDates);
    }
}

이러한 추상 클래스의 장점으로 느낀 것은, 구현 클래스에서 중복된 코드를 제거할 수 있어 코드의 가독성을 향상시킨다는 점이었습니다.


테스트 코드

모든 도메인 로직에 대한 테스트 코드를 작성하기 위해 노력했습니다.

여러 입력값을 이용하여 테스트를 진행하고 이를 통과함으로써, 프로그램의 안정성향상시킬 수 있다는 느낌을 받을 수 있었습니다.

테스트 코드를 작성하고 보니 총 120개의 테스트를 작성했고,
모두 통과되었습니다. 😀😀


마치며

이게 마지막 미션이라는 생각이 안 믿길 정도로, 프리코스를 진행하며 많은 몰입과 노력을 통해 시간이 정말 빨리 갔다고 느꼈습니다.

다시 한번, 우아한테크코스를 진행한 나의 선택 스스로를 칭찬할 수 있었습니다. 👍

이번 미션에 대한 느낀점은 처음 미션을 보았을 때 정말 충격에 빠졌었지만, 막상 미션을 진행하는 과정이 너무 즐거웠습니다. 뿌듯!!

제발.... 합격할 수 있길... 🤣🤣🤣🤣

저의 자세한 코드가 궁금하신 분은 아래 링크에서 확인할 수 있습니다.
https://github.com/jcoding-play/java-christmas-6-jcoding-play/tree/main


최종 리팩토링 후 달라진 부분

이 부분은 프리코스가 끝나고 난 뒤 리팩토링을 진행하고 이전과 크게 달라진 점을 알려드리기 위해 작성해보았습니다!!

이번 미션에서 메뉴에 대한 종류는 애피타이저, 메인, 디저트, 음료 이렇게 4가지로 구성되어있었습니다.

그래서 이를 리팩토링 이전에는, 각 타입에 대해 하나씩 클래스를 구현하였습니다.

public enum Appetizer implements Menu {
    MUSHROOM_SOUP("양송이수프", 6000),
    TAPAS("타파스", 5500),
    CAESAR_SALAD("시저샐러드", 8000);

    private final String name;
    private final int price;

    Appetizer(String name, int price) {
        this.name = name;
        this.price = price;
    }

    @Override
    public boolean isMatchName(String name) {
        return this.name.equals(name);
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public int getPrice() {
        return price;
    }
}

이에 대한 문제점으로 모든 타입에 대해서 isMatch(), getName(), getPrice() 메서드를 중복으로 작성하는 문제가 발생하였었습니다.

이를 다음과 같이 리팩토링하여, 중복되는 코드를 제거할 수 있었습니다.

public enum Menu {
    MUSHROOM_SOUP("양송이수프", 6_000, Type.APPETIZER),
    TAPAS("타파스", 5_500, Type.APPETIZER),
    CAESAR_SALAD("시저샐러드", 8_000, Type.APPETIZER),

    T_BONE_STEAK("티본스테이크", 55_000, Type.MAIN),
    BARBECUE_LIP("바비큐립", 54_000, Type.MAIN),
    SEAFOOD_PASTA("해산물파스타", 35_000, Type.MAIN),
    CHRISTMAS_PASTA("크리스마스파스타", 25_000, Type.MAIN),

    CHOCOLATE_CAKE("초코케이크", 15_000, Type.DESSERT),
    ICE_CREAM("아이스크림", 5_000, Type.DESSERT),

    ZERO_COLA("제로콜라", 3_000, Type.DRINK),
    RED_WINE("레드와인", 60_000, Type.DRINK),
    CHAMPAGNE("샴페인", 25_000, Type.DRINK),

    NOTHING("없음", 0, null);

    private final String name;
    private final int price;
    private final Type type;

    Menu(String name, int price, Type type) {
        this.name = name;
        this.price = price;
        this.type = type;
    }

    public static Menu findByName(String name) {
        return Arrays.stream(values())
                .filter(menu -> menu.isMatchName(name))
                .findFirst()
                .orElseThrow(() -> new IllegalArgumentException("일치하는 메뉴가 존재하지 않습니다."));
    }

    private boolean isMatchName(String name) {
        return this.name.equals(name);
    }

    public boolean isDrink() {
        return type.isDrink();
    }

    public boolean isMatchType(Type type) {
        return this.type == type;
    }

    public int getPrice() {
        return price;
    }
}

DayOfWeek Enum 객체 활용

이번 미션에서 이벤트들은 각각 적용 가능한 날짜들이 명시되어있었습니다.

한 가지 예로, 평일 할인을 통해 설명하겠습니다.
평일 할인은 요일이 일요일~목요일 사이라면 적용이 되는 할인이었습니다.

이를 구현하기 위해 리팩토링 이전에는, 일일이 일요일~ 목요일에 대한 날짜를 for문을 이용하여 구하였습니다.

private static final int FIRST_SUNDAY_DATE = 3;
private static final int FIRST_THURSDAY_DATE = 7;
private static final int LAST_DAY_OF_THE_EVENT = 31;
private static final int WEEK_DATE = 7;

List<Integer> initializeApplicableDates() {
	List<Integer> applicableDates = new ArrayList<>();

	for (int firstDate = FIRST_SUNDAY_DATE; firstDate <= FIRST_THURSDAY_DATE; firstDate++) {
		for (int date = firstDate; date <= LAST_DAY_OF_THE_EVENT; date = date + WEEK_DATE) {
			applicableDates.add(date);
		}
	}
	Collections.sort(applicableDates);
	return applicableDates;
}

하지만, DayOfWeek라는 Java에서 제공하는 Enum 객체를 알게 되었고, 이를 통해 쉽게 적용 가능한 날짜를 구할 수 있었습니다.

private static Set<DayOfWeek> discountDaysOfWeek = Set.of(DayOfWeek.SUNDAY, DayOfWeek.MONDAY, DayOfWeek.TUESDAY, DayOfWeek.WEDNESDAY, DayOfWeek.THURSDAY);

이를 이용하여 쉽게 할인이 적용 가능한지를 파악할 수 있었습니다.

profile
한걸음씩 성장하는 개발자

0개의 댓글