4주 차에는 크리스마스 프로모션 이벤트를 개발하는 미션이 나왔다.
고객이 예상 방문 날짜와 주문 메뉴를 입력하면, 해당 정보를 가지고 이벤트를 적용하는 내용이다.
4주 차 미션 저장소
https://github.com/woowacourse-precourse/java-christmas-6
내가 제출한 저장소
https://github.com/Hanjaemo/java-christmas-6-Hanjaemo
3주 차 미션이 끝나고 새로운 코수타가 시작되었는데, 내가 봤던 코수타 중에서 가장 많은 것을 배웠던 것 같다.
그중에서 가장 인상 깊었던 건 어려운 문제를 마주쳤을 때, 이를 구현하는 순서에 대한 조언이었다.
그래서 이를 이번 미션에 적용해보면서 나만의 구현 순서를 재정립했다. 가장 먼저 요구 사항을 분석하고, 기능을 나열하면서 동작 과정을 그림으로 그렸다.
그리고나서 나열한 기능들을 작게 쪼개 핵심 기능 딱 한 가지를 정의했으며, 이는 내게 지도가 되어주었다. 개발을 어디서부터 시작해야 할지, 어떤 순서로 진행해야 할지에 대한 방향성을 제공해주었기 때문이다.
또한, 2-30분마다 내가 하고 있는 작업의 목적을 의도적으로 인지함으로써 나의 집중력을 유지하여 작업의 효율성을 높일 수 있었다.
객체를 객체스럽게 사용하라는 공통 피드백을 보고, 이번 미션에서는 getter를 최대한 사용하지 않는 것을 목표로 세웠다.
하지만 역시 현실은 이상과 다르다고..
getter를 사용하지 않으면 쓸데없는 의존 관계가 생기는 문제가 발생했다. 의존 관계가 생기는 것보다는 getter를 사용하는 것이 더 낫겠다는 판단으로 결국 getter를 사용한 객체가 좀 있었다.
지금 오브젝트를 막 읽으면서 알게 되었는데, getter를 무작정 사용하지 않는 것은 오히려 독이 된다는 말이 있었다.
즉, 내가 미션 수행 당시 결정했던 방식은 결코 틀린 것이 아니며, 오히려 더 좋은 방식이었던 것이다.
앞으로 이러한 수많은 트레이드오프를 만날 생각에.. 벌써 험난하면서도 기대된다🤤
아래는 3주 차 미션(로또) 중 작성한 코드다. HashMap의 key인 Rank는 당첨 등수를 나타내는 enum이고, value에는 해당 등수에 당첨된 횟수를 담는다.
private final Map<Rank, Integer> details = new HashMap<>();
그런데 3주차 미션 코드 리뷰에서 어떤 분이 EnumMap을 사용해보라는 말을 해주셨다.
EnumMap? 처음 들어봐서 뭔지 찾아봤더니, Enum을 key로 사용하는 Map을 사용하는 경우 HashMap보다 성능상 이점이 있다고 한다.
그래서 이번에는 아래와 같이 EnumMap을 사용해서 코드를 작성했다.
private final Map<Event, Integer> appliedEvents = new EnumMap<>(Event.class);
나는 지금까지 8,000원과 같이 숫자 세자리마다 쉼표를 찍고싶을 때 String.format의 "%,d"를 사용했다.
그런데 3주 차 다른 분들의 코드를 보는데 대부분 DecimalFormat이란 것을 사용하여 쉼표를 찍더라?
그래서 나는 DecimalFormat이 뭔지 찾아봤고, 그제서야 이 친구의 존재를 알게 되었다.
그런데 문득 "String.format도 있는데 왜 DecimalFormat을 사용할까?"라는 의문이 생겼고, 둘에 대해 더 학습하면서 고민해본 결과 다음과 같은 판단을 하게 되었다.
String.format은 숫자뿐만 아니라 다른 문자열에 대해서도 쉼표를 찍을 수 있다. 반면에 DecimalFormat은 오직 숫자를 다루기 위해서만 사용된다.
이 차이를 봤을 때, 숫자에 관련된 경우에는 DecimalFormat을 사용하는 편이 더 명확한 의도를 전달할 수 있겠다는 생각이 들었다.
이번 미션의 핵심 기능은 이벤트 적용이라고 볼 수 있다. 이벤트는 크리스마드 디데이 할인, 평일/주말 할인, 특별 할인, 그리고 증정까지 총 4개로 구성되어 있다.
이 요구사항을 보면서 딱 든 생각이 있는데, 그것은 공통된 역할에 대한 분류였다.
비슷한 역할을 수행하는 객체들은 하나의 타입으로 묶어주라는 말이 생각나자마자, 이에 대한 설계를 진행했다.
그런데 "고객의 정보(방문 날짜, 주문 메뉴)에 따라 이벤트를 적용한다"라는 공통 기능은 찾았는데.. 상속과 인터페이스 중 어떤 방식을 사용해야 할지 꽤 고민됐다. 그러고보니 상속과 인터페이스의 명확한 차이와 각 방식이 어떤 상황에 사용되는지 잘 몰랐던 것이다.
그래서 이에 대해 알아보기 위해 블로그와 유튜브를 열심히 찾아봤고, 대충 상속은 is-a, 인터페이스는 can-do라는 것을 알게 되었다.
그래도 아직 완전히 안다고 할 수는 없기에 더 학습해서 이를 블로그에 정리해보려고 한다.
나는 처음에 주문 메뉴를 관리하는 객체 Order를 아래와 같이 구현했다.
public class Order {
private final Map<Menu, Integer> orderMenus = new EnumMap<>(Menu.class);
...
그런데 이렇게 Order 객체가 Map<Menu,Integer>
를 관리하다보니, 메뉴가 중복되는 경우와 음료만 주문하는 경우처럼 주문 메뉴들에 대한 검증까지 처리해야 했다. 이는 SRP를 위반한다는 생각이 들었고, 그래서 주문 메뉴를 나타내는 OrderMenu 클래스와 List<OrderMenu>
를 가지는 OrderMenus 클래스를 작성했다.
이렇게 클래스를 분리함으로써, 나중에 주문 메뉴와 관련된 정보가 변경되는 경우 OrderMenu 클래스만 수정하면 된다는 장점이 생겼다.
이러한 과정은 다시 한 번 객체지향 설계가 주는 변경의 용이함을 체감할 수 있었다.
주문 메뉴를 입력 받을 때, "-"를 사용해서 메뉴와 수량을 구분한다.
(e.g. 티본스테이크-1,아이스크림-2)
이를 보고 "split()
을 사용하면 되겠다!"라는 생각은 바로 들었는데, 도대체 어디에 위치시켜야 할지 정말 고민이 많이 되었다.
처음에는 아무 생각없이 InputView에 위치시켰다가, 아무래도 주문 메뉴 객체에 대한 내용이니까 다음과 같이 OrderMenu로 옮겼다.
public class OrderMenu {
private final Menu menu;
private final int quantity;
public OrderMenu(String orderMenu) {
String[] split = orderMenu.split("-");
this.menu = Menu.from(split[0]);
this.quantity = Integer.parseInt(split[1]);
}
...
그런데 OrderMenu의 생성자 매개변수 타입을 String으로 한다면, 나중에 어떤 값이 들어가는지 명확하지 않다는 문제가 발생할 수도 있다고 판단했다. 그래서 이를 컨트롤러 또는 서비스 계층에서 처리하도록 작성했다.
그런데 또 생각해보면.. 나중에 메뉴와 수량을 "-"가 아닌 다른 문자열로 구분하라는 요구사항이 생겼을 때 나는 어느 클래스를 먼저 살펴볼까?
OrderMenu 또는 InputView같다.
그런데 OrderMenu에 위치하기엔 더 좋은 방법이 떠오르지 않았고, 결국 InputView에 해당 로직을 위치시켰다...
이 과정이 이번 미션에서 가장 아쉬운 부분이고, 개선하기 위해 정말 많은 고민이 필요할 듯 싶다.
모든 기능을 구현하고나서 애플리케이션을 실행했는데 UnsupportedOperationException
이 발생했다...😱
예외 이름을 직역해보니 지원되지 않는 작업..이라고 한다. 뭐가 지원되지 않는건지 예외 메시지를 살펴보다가, ImmutableCollection
이라는 단어를 발견했다.
이는 불변 컬렉션에 잘못 접근해서 발생한 문제였고, 천천히 디버깅해본 결과 새로운 사실을 알게 되었다.
얼마 전부터 나는 Stream.collect(Collectors.toList())
보다 더 간결하다는 이유로 Stream.toList()
를 즐겨 사용했는데, 이는 불변 컬렉션을 반환한다는 것이었다.
이러한 경험을 바탕으로 어떤 기능이던지 얕게 알아보고 사용하는 것이 아니라, 그 기능이 왜 나오게 되었고 어떤 차이가 있는지 명확하게 이해한 뒤에 사용해야겠다는 교훈을 얻었다..😅
결국 4주간의 프리코스가 끝이 났다..
작년에 우테코를 처음 알게 된 이후로 이번 기수에 꼭 합격하겠다는 생각으로 프리코스에 정말 열심히 임했다.
그래서 그런지 4주가 4시간으로 생각될만큼 정말 시간이 빠르게 갔던 것 같다.
1차 합격까지 또 4주.. 프리코스 4주는 시간이 너무 빨랐는데, 합격 발표까지 4주는 너무 길어질 듯하다.
남은 4주동안은 혹시 모를 최종 코딩 테스트를 연습해보면서, 오브젝트나 열심히 읽어보려고 한다.
4주간 정말 고생 많았다.