이번 과제는 개발자가 된 기분이 들어 더 재미있게 진행할 수 있었다. 기능이 하나씩 구현될 때마다 뿌듯했고, 괜시리 책임감까지 느껴졌던 것 같다. 그리고 포기하지 않고 기필코 개발자가 되겠다는 생각이 더욱 굳건해졌다. 지금보다 더 꾸준히 열심히 해서 비즈니스 로직을 통해 가치를 실현시키고, 기록하고 소통하는 주니어 개발자로 거듭나고싶다.
4주차 크리스마스 프로모션 - 미션 저장소
4주차 크리스마스 프로모션 - 작성한 코드
4주차 크리스마스 프로모션 - PR 주소
✔️ 기능 목록🚀
✅ 식당 예상 방문 날짜를 입력받고 검증하는 기능
- 입력 안내 메시지를 출력한다.
- 문자열을 예외처리한 후 다시 입력받는다.
- [예외상황] 방문할 날짜가 1 이상 31 이하의 숫자가 아닌 경우
- "[ERROR] 유효하지 않은 날짜입니다. 다시 입력해 주세요." 에러 메시지 출력
✅ 주문할 메뉴와 개수를 입력받고 검증하는 기능
- 입력 안내 메시지를 출력한다.
- 문자열을 콤마(,)로 구분한다.
- 메뉴와 개수를 대쉬(-)로 구분한다.
- 문자열을 예외처리한 후 다시 입력받는다.
- [입력 예외]
- [예외상황] 입력한 메뉴의 형식이 예시와 다른 경우
- "[ERROR] 유효하지 않은 주문입니다. 다시 입력해 주세요." 에러 메시지 출력
- [예외상황] 중복 메뉴를 입력한 경우
- "[ERROR] 유효하지 않은 주문입니다. 다시 입력해 주세요." 에러 메시지 출력
- [기능 예외]
- [예외상황] 고객이 메뉴판에 없는 메뉴를 입력하는 경우
- "[ERROR] 유효하지 않은 주문입니다. 다시 입력해 주세요." 에러 메시지 출력
- [예외상황] 메뉴의 개수가 1 이상의 숫자가 아닌 경우
- "[ERROR] 유효하지 않은 주문입니다. 다시 입력해 주세요." 에러 메시지 출력
- [예외상황] 음료만 주문한 경우
- "[ERROR] 유효하지 않은 주문입니다. 다시 입력해 주세요." 에러 메시지 출력
- [예외상황] 21개 이상의 메뉴를 주문한 경우(같은 메뉴도 개수 합산)
- "[ERROR] 유효하지 않은 주문입니다. 다시 입력해 주세요." 에러 메시지 출력
✅ 사용자가 입력한 메뉴를 출력하는 기능
✅ 할인 전 총 주문 금액을 계산하는 기능
- "총 주문 금액"을 출력한다.
✅ 증정 메뉴 제공 여부를 확인하는 기능
- 총 주문 금액이 12만원 이상일 경우, "샴페인 1개"를 출력한다.
- 해당하지 않을 경우 "없음"을 출력한다.
✅ 혜택 적용에 대한 내용을 관리하는 기능
- 총 주문 금액 10,000원 이상부터 이벤트를 적용한다.
- '크리스마스 디데이 할인'을 제외한 다른 이벤트는 2023.12.1 ~ 2023.12.31 동안 적용한다.
- 적용된 이벤트 내용만 보여준다.
- 적용된 이벤트가 하나도 없을 경우, 혜택 내역은 "없음"을 출력한다.
✅ 크리스마스 디데이 할인 금액을 계산하는 기능
- 크리스마스 디데이 할인은 2023.12.1 ~ 2023.12.25 동안 적용한다.
- 1,000원으로 시작해 날마다 할인 금액을 100원씩 증가시킨다.
- 총 주문 금액에서 해당 금액만큼 할인한다.
- "크리스마스 디데이 할인 금액"을 출력한다.
- 단, 해당 이벤트가 적용되지 않을 경우 출력하지 않는다.
✅ 평일/주말 할인 금액을 계산하는 기능
- 평일 할인(일요일~목요일) : 평일에는 디저트 메뉴 1개당 2,023원을 할인한다.
- 주말 할인(금요일, 토요일) : 주말에는 메인 메뉴 1개당 2,023을 할인한다.
- 평일이면 "평일 할인 금액"을 출력한다.
- 주말이면 "주말 할인 금액"을 출력한다.
- 단, 해당 이벤트가 적용되지 않을 경우 출력하지 않는다.
✅ 특별 할인 금액을 계산하는 기능
- 이벤트 달력에 별이 있으면 총 주문 금액에서 1,000원을 할인한다.
- "특별 할인 금액"을 출력한다.
- 단, 해당 이벤트가 적용되지 않을 경우 출력하지 않는다.
✅ 증정 이벤트 적용 여부를 확인하는 기능
- 할인 전 총 주문 금액이 12만원 이상일 경우, 샴페인 1개를 증정한다.
- 증정 이벤트 할인 금액 "-25,000원"을 출력한다.
- 해당하지 않을 경우, "없음"을 출력한다.
- 단, 해당 이벤트가 적용되지 않을 경우 출력하지 않는다.
✅ 총 혜택 금액을 계산하는 기능
- 총 혜택 금액 = 할인 금액의 합계 + 증정 메뉴의 가격
- "총 혜택 금액"을 출력한다.
- 적용된 이벤트가 하나도 없을 경우, "0원"을 출력한다.
✅ 할인 후 예상 결제 금액을 계산하는 기능
- 할인 후 예상 결제 금액 = 할인 전 총 주문 금액 - 할인 금액
- 총 혜택 금액이 없을 경우 "총 주문 금액"을 출력한다.
✅ 12월 이벤트 배지 지급 기준을 확인하고 이를 제공하는 기능
- 5천 원 이상일 경우, "별"을 출력한다.
- 1만 원 이상일 경우, "트리"를 출력한다.
- 2만 원 이상일 경우, "산타"를 출력한다.
- 해당하지 않는 경우는 "없음"을 출력한다.
✔️ 구현 과정에서 고민했던 점🤔
1. 검증은 어디서 구현해야하는가?
3주차를 진행하며 검증 로직은 비즈니스 로직이기에 도메인 또는 서비스 내부에서 구현해야한다고 생각했다. 하지만 이번 4주차 과제를 진행하며 검증 로직을 구현하는 과정에서 입력에 대한 예외와 기능에 대한 예외를 따로 분리해야한다는 생각이 들었다. 그렇다면 어떻게, 어디에서 각각을 구현해야할까? 이에 대해 고민한 결과, 다음과 같이 나만의 규칙을 정하였다.
입력에 대한 예외를 검증하는 로직 : InputView 클래스에서 구현한다.
기능에 대한 예외를 검증하는 로직 : 해당하는 도메인 클래스에서 구현한다.
위와같이 검증 로직을 분리하니 각각의 예외상황에 맞게 예외처리할 수 있었다.
2. 출력 메시지를 상수로 표현하는 것이 옳은가?
입력 안내 메시지, 출력 메시지, 에러 메시지 등 String과 int 타입의 출력해야하는 부분들은 항상 존재한다. 이와 같은 '메시지들을 상수화해야하는가, 그대로 출력해야하는가?'에 대한 고민이 많았다. 그렇다면 메시지를 상수화하는 이유는 무엇일까?
나의 개인적인 생각이지만 재사용하는 메시지들을 일괄적으로 처리해주기 위함인 것 같다. 또한 상수는 값이 변하지 않기 때문인 것 같다. 그렇다면 재사용하고, 값이 변하지않아야하는 부분들만 상수화 해주면 되지않을까? 그렇다. 다음과 같이 나만의 규칙을 정하였다.
1. 재사용하는 메시지를 상수화하자.
2. 값이 변하지 않아야하는 메시지를 상수화하자.
3. 데이터를 관리하는 클래스를 enum 객체로 만들고 싶을 때, 구체적으로 어떻게 구현해야하는가?
3주차를 진행하며 enum에 대한 개념이 많이 어려웠다. 기존에 프로젝트를 진행하며 enum을 사용할 때는 아래와 같이 단순하게 사용하곤 했다.
public enum RoleType {
ROLE_NOT_PERMITTED, ROLE_USER, ROLE_ADMIN
}
때문에 enum을 객체로 바라본다는 개념이 잘 이해가 되지 않았다. 하지만 3주차가 끝나갈 무렵 enum을 다룬 좋은 글을 발견해 조금씩 이해할 수 있었다. 이후 enum을 데이터를 관리하는 객체로서 바라보기 시작했다.
4주차 과제를 진행하며 enum 객체에 데이터를 담기위해 type, name, price 필드를 만들었다. 다음은 예시를 위해 임의로 작성한 코드이다. 사용자가 마트에서 장을 보려 한다. 마트에서 구입할 수 있는 물품은 다음과 같다.
- 주방 용품
- 냄비
- 국자
- 욕실 용품
- 치약
- 칫솔
- 전자 제품
- 청소기
- 세탁기
public enum Store {
POT("주방용품", "냄비", 30_000),
SCOOP("주방용품", "국자", 10_000),
TOOTH_PASTE("욕실용품", "치약", 3_000),
TOOTH_BRUSH("욕실용품", "칫솔", 2_000),
VACUUM_CLEANER("전자제품", "청소기", 100_000),
WASHING_MACHINE("전자제품", "세탁기", 500_000);
private final String type;
private final String name;
private final int price;
Menu(String type, String name, int price) {
this.type = type;
this.name = name;
this.price = price;
}
}
하지만 주어진 물품의 개수가 50개가 넘는다면 관리하기 힘들어질 것 같다는 생각이 들었다. 따라서 2가지 방법을 생각하였다.
(1) enum 확장하기
Store 인터페이스를 만들고 KitchenProduct, BathroomProduct, ElectronicProduct가 이를 구현하도록 한다.
(2) Store과 StoreDetail을 만든다.
전체 물품의 물품명, 가격을 StoreDeatil에 담은 후 이를 Store에서 유형별로 그룹화하여 사용한다.
(1)번은 Store 인터페이스에서 어떤 함수를 정의해야할지 현 상황에서 알 수 없었기에 (2)번 방법을 사용하기로 결정했다. 물품의 수가 방대해지면 (2)번을 사용하기 어렵겠지만 현재 프로젝트 규모에는 괜찮을 것 같다는 생각이 들었다. 따라서 다음과 같이 분류할 수 있었다.
public enum Store {
KITCHEN_PRODUCT("주방용품", Arrays.asList(StoreDetail.POT, StoreDetail.SCOOP)),
BATHROOM_PRODUCT("욕실용품", Arrays.asList(StoreDetail.TOOTH_PASTE, StoreDetail.TOOTH_BRUSH)),
ELECTRONIC_PRODUCT("전자제품", Arrays.asList(StoreDetail.VACUUM_CLEANER, StoreDetail.WASHING_MACHINE));
private final String type;
private final List<StoreDetail> storeDetail;
Store(String type, List<StoreDetail> storeDetail) {
this.type = type;
this.storeDetail = storeDetail;
}
...
}
public enum StoreDetail {
POT("냄비", 30_000),
SCOOP("국자", 10_000),
TOOTH_PASTE("치약", 3_000),
TOOTH_BRUSH("칫솔", 2_000),
VACUUM_CLEANER("청소기", 100_000),
WASHING_MACHINE("세탁기", 500_000);
private final String name;
private final int price;
StoreDetail(String name, int price) {
this.name = name;
this.price = price;
}
...
}
위 내용은 아래 블로그를 통해 고안해낼 수 있었다. 여러 번 정독하니 조금씩 이해가 되기 시작했던 것 같다. 다음에는 enum 객체에서 함수를 인자로 사용하는 방식을 사용해보고 싶다.
https://techblog.woowahan.com/2527/
4. 커밋 주기
커밋 주기에 대해서도 고민이 많았다. 깃을 통해 코드를 관리하는 이유는 협업에 있어서 코드를 관리하기 용이하다는 장점때문이 아닐까 싶다. 그렇다면 팀원분들이 나의 코드를 볼 때 이해하기 쉽고, 직접 실행하는데 오류가 발생하지 않아야한다는 생각이 들었다. 따라서 기능 구현과 해당 기능의 단위테스트까지 구현한 후 커밋하였다.
✔️ 어려웠던 점 💡
1. hashMap은 key 값 중복을 허용하지 않는다.
사용자로부터 입력받은 값(ex) ShoppingList)를 Map으로 변환하여 주요 로직에 넘겨주는 방식으로 구현하였다. 이 때 사용자로부터 입력을 받은 데이터 중 중복된 입력이 존재하는지 검증해야한다. 하지만 문제점이 있었다. HashMap은 key 값이 중복되는 것을 허용하지 않기 때문에 이를 사용하면 중복된 key의 해당 value 값을 덮어씌운다. 이렇게 되면 입력받은 값이 중복되었는지 확인할 수 없다는 치명적인 문제점이 발생한다.
이를 해결하기 위해서 몇가지 방법을 생각해보았다.
(1) 중복을 허용하는 multiValueMap을 사용하여 중복을 체크한다.
위 방법은 스프링에서 제공하는 방법이므로 사용할 수 없다.
(2) 입력을 받을 때부터 중복된 메뉴인지 확인한다.
1번 방법은 불가능하니 애초에 입력을 받을 때부터 중복을 체크해야한다는 생각이 들었다. 따라서 입력값이 String을 Map으로 변환하기 전에 중복된 값이 있는지 확인하여 문제를 해결하였다.
2. 객체에 메시지를 보내자.
최대한 getter를 지양하기 위해 객체에 메시지를 보내는 방식으로 로직을 구현하는 것은 주차가 지나도 여전히 쉽지않았다. 하지만 이전 주차들과 달라진 점은 어느 정도 감이 조금씩 잡히기 시작했다는 것이다. 역시 해서 안되는 건 없다.
3. 단위테스트를 구현하자.
4주차를 진행하며 왜 단위테스트를 구현해야하는지에 대한 이유가 전보다 더 이해되기 시작했다. 단위테스트를 구현하면 예기치못한 예외가 발생하는 것을 막을 수 있고, 만일 발생한다면 어디서 발생하는지 알 수 있다. 아직 많이 부족하지만 진행 중인 프로젝트에서도 이를 적용하며 배워가는 중이다.
✔️ 아쉬웠던 점 😶
1. Controller와 OutputView에게 책임을 많이 부여했다. 과연 단일 책임 원칙을 지켰다 말할 수 있을까?
구현 완료 후 리팩토링을 하기 시작했다. 하나의 함수가 15라인을 넘어가는 것을 막기 위해 함수를 분리하고, 상수명을 가독성이 좋도록 다시 짓고, 불필요하게 public으로 열려있는 함수가 있는지 확인했다. 하지만 Controller와 OutputView에 꽤 많은 함수가 들어가있었는데 이를 분리하지 못했다. 입력받은 데이터를 Controller를 통해 OutputView에 넘겨주는 함수가 많았기에 너무 엮여있다는 점이 문제였던 것 같다. 이를 해결하는 방안을 조금 더 깊이 생각해봐야할 것 같다.
2. 항상 "왜?"라는 질문을 던져야 한다.
클래스 생성 비용, static 사용 시 메모리 할당 등 단순히 구현을 잘하는 것보다 머릿속에서부터 손 끝까지 이어지는 모든 과정 안에서 "왜"라는 질문을 던지며 심도있게 고민하는 연습이 더 필요할 것 같다.
얼른 코드리뷰를 주고받으며 부족한 부분들을 보완하고 싶다!!!
✔️ 4주간의 프리코스를 마치며 🍀
항상 드는 생각이지만 구현하기에 앞서 대략적인 기능 목록을 도출하는 과정을 통해 머릿속에서 구현 로직이 정리가 되어 큰 도움이 되는 것 같다. 1주차까지만 해도 기능 목록을 도출하는 것이 어색했지만 프리코스가 끝나가는 현 시점에서 이는 크게 어렵지 않았다. 이번 4주차는 하나의 돌아가는 프로그램을 구현하기 위해 필요한 객체들이 무엇인지, 각각 어떤 행위를 해야하는지 등의 관점으로 생각하려고 노력했다. 1~3주차동안 고찰한 결과가 아닐까 하는 생각이 든다. 프리코스를 통해 Stream, Enum, 단위테스트, 커밋컨벤션 등을 배우고 디스코드 커뮤니티를 통해 코드리뷰를 나누는 등 정말 값진 경험을 얻고 가는 것 같다.
'우아한테크코스 > 6기 프리코스' 카테고리의 다른 글
[우아한테크코스 6기 프리코스] 프리코스를 마치며 (0) | 2023.11.18 |
---|---|
[우아한테크코스 6기 프리코스] 3주차 회고 (0) | 2023.11.09 |
[우아한테크코스 6기 프리코스] 2주차 코드 리뷰 종합해보기! (0) | 2023.11.06 |
[우아한테크코스 6기 프리코스] 2주차 회고 (0) | 2023.11.01 |
[우아한테크코스 6기 프리코스] 1주차 코드 리뷰 종합해보기! (2) | 2023.10.27 |