나를 기록하다
article thumbnail
반응형

프리코스

길다면 길었고, 짧다면 짧았던 4주간의 프리코스가 오늘부로 모두 끝났다. 3주차 회고록도 쓰려했으나, 프리코스 진행과 바빴던 개인 일정이 겹쳐서 작성하지 못하였기에 3주차, 4주차 회고록을 한번에 작성하려 한다.

 

3주차 과제 - 로또 게임

기능 요구 사항

기능 요구 사항

실행 결과 예시는 다음과 같다.

구입금액을 입력해 주세요.
8000

8개를 구매했습니다.
[8, 21, 23, 41, 42, 43] 
[3, 5, 11, 16, 32, 38] 
[7, 11, 16, 35, 36, 44] 
[1, 8, 11, 31, 41, 42] 
[13, 14, 16, 38, 42, 45] 
[7, 11, 30, 40, 42, 43] 
[2, 13, 22, 32, 38, 45] 
[1, 3, 5, 14, 22, 45]

당첨 번호를 입력해 주세요.
1,2,3,4,5,6

보너스 번호를 입력해 주세요.
7

당첨 통계
---
3개 일치 (5,000원) - 1개
4개 일치 (50,000원) - 0개
5개 일치 (1,500,000원) - 0개
5개 일치, 보너스 볼 일치 (30,000,000원) - 0개
6개 일치 (2,000,000,000원) - 0개
총 수익률은 62.5%입니다.

 

위와 같이 진행을 하며, 신경써야할 추가된 요구사항은 다음과 같다.

  • 함수의 길이 15라인 이하
  • else 예약어 및 switch/case 허용 x
  • Enum 적용
  • 단위 테스트 구현
  • 제공된 Lotto 클래스 활용 및 Lotto에 인스턴스 변수 추가 불가능

 

1, 2주차 미션을 하면서 많이 발전했다고 느낀 것이, 문제를 보고나서 구현을 하는 것은 그리 어렵지 않겠다고 느낀 것이다. 실제로도 구현을 하는 데는 그리 오랜 시간이 걸리지 않았다. 하지만 문제는 리팩토링이었다. 실제 코딩 테스트에서는 돌아가는 쓰레기라도 만드는 것이 백번 낫지만, 프리코스는 1주일의 시간이 주어지는데 돌아가는 쓰레기처럼 코드를 만들고 나니 리팩토링이 너무 힘들었다. 그래서 갈아엎고 다시 만들었다(후에 프리코스를 참여하시는 분이 보신다면 프리코스 때는 설계를 제대로 하고 만드는 것이 더 나을 것 같다고 전하고 싶다).

 

고민

DTO

DTO를 어떻게 활용할지에 대한 고민을 가장 많이했다. 2주차 과제까지는 DTO를 제대로 활용하지 못한 채 제출을 하였기에 이번에는 정보를 전달하는 DTO를 사용해보고 싶었다. 먼저 도메인 설계를 끝내고 각 도메인마다 나오는 정보를 모두 DTO에 넣었다. 이 당시에는 이게 제대로 된 구조라고 생각했었는데, 마치고 다른 코드를 보면서 뒤돌아보니, 무분별하게 많은 DTO를 생성했던 것 같다.

dto

위 사진처럼 무분별하게 많은 DTO를 생성했다. 다시 돌아간다면, bonus 번호는 정답 번호에 포함시켜두고, 보너스 번호를 포함하는지 여부를 boolean으로 확인하게끔 하는게 나을 것 같다. 그리고 수익률을 계산하여 최종 결과 DTO에 함께 담는 것이 보다 직관적일 것 같다고 느꼈다.

 

단위 테스트

이전 과제까지는 AssertJ와 Junit에 대해 기본기가 없었기에 테스트는 구현을 모두 마친 뒤 간단한 테스트만 진행을 했었지만, 우아한테크코스에서 테스트를 거듭 강조하는 것을 보고 AssertJ와 Junit을 공부하면서 기능을 하나씩 구현할 때마다 단위 테스트를 진행했다. 아무래도 아직 테스트가 익숙하지 않아서 시간이 많이 소요되었지만, 모든 기능이 정상적으로 테스트하고 테스트 성공 후 컨트롤러에서 각 기능들을 조립하니 에러 없이 쉽게 조립할 수 있었다.

 

예외처리

이번에는 예외처리를 Exception 클래스를 생성하여 핸들링할 수 있도록 구현해봤다. 사실 이 부분에 대해 확실하게 이해를 하지 못하여서 추가적인 공부가 필요한 부분이다. 명확하게 이해하지 못한 코드를 쓴 나 자신을 반성한다.

 

이것 외에도 많은 고민과 수정을 거쳐서 겨우 제출에 성공했다. 나보다 훨씬 명확한 클래스 분리와 깔끔한 로직 작성, 구체적인 테스트 코드를 작성하신 다른 분들의 코드를 보면서 감탄하고 내 코드가 허술해보이긴 했다만, 프리코스 시작 전의 내 코드와 비교했을 때는 확실하게 많이 성장했다는 것을 느낄 수 있었다.


4주차 - 크리스마스 프로모션

  • 적용된 이벤트가 없는 경우
안녕하세요! 우테코 식당 12월 이벤트 플래너입니다.
12월 중 식당 예상 방문 날짜는 언제인가요? (숫자만 입력해 주세요!)
26 
주문하실 메뉴를 메뉴와 개수를 알려 주세요. (e.g. 해산물파스타-2,레드와인-1,초코케이크-1)
타파스-1,제로콜라-1 
12월 26일에 우테코 식당에서 받을 이벤트 혜택 미리 보기!
 
<주문 메뉴>
타파스 1개
제로콜라 1개

<할인 전 총주문 금액>
8,500원
 
<증정 메뉴>
없음
 
<혜택 내역>
없음
 
<총혜택 금액>
0원
 
<할인 후 예상 결제 금액>
8,500원
 
<12월 이벤트 배지>
없음
  • 기대하는 '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월 이벤트 배지>
산타

 

요구사항은 기존의 요구사항에 InputView와 OutputView를 구현하라는 요구사항이 추가되었지만, 기존에도 구현을 해왔었기에 별다른 특이사항은 없었다.

 

고민

사실 마지막 주에 개인적인 일 때문에 너무 바빠서 월요일이 되어서야 과제를 시작할 수 있었다. 프리코스 기간에는 정말 프리코스에만 몰입하고 싶었는데, 스스로 통제할 수 없는 일들이 발생함으로 그러지 못했던게 아쉽다. 마지막 과제인 만큼 최종 코딩테스트를 친다는 마음으로 시간을 재면서 구현을 하였다. 5시간 내에 구현을 하는 것을 목표로 최대한 시간을 아끼려 테스트 코드도 작성하지 않고 구현에 몰두하였으나, 예외 처리하는 부분에서 너무 많은 시간을 소비하여 결국 8시간 정도 소비하였다.

유효성 검사의 위치(예외처리)

이번 과제는 다른 과제들보다 시간적으로 많이 투자하지 못하였지만 가장 많이 고민했던 것은 유효성 검사의 위치였다. 구글링을 하니 사람들마다 다르지만 보통 도메인 모델, DTO에서 많이 하는 것 같았다. 나는 우선 모델은 모델만의 일을 하고 데이터를 전달할 때 유효성 검사를 하여 올바른 데이터만 전달할 수 있도록 하는 것이 더 나은 로직이라는 판단 하에 DTO에 유효성 검사 로직을 넣어서 구현하였다.

예를 들어 손님이 주문한 데이터를 전달하는 OrderDTO만 예시로 보인다면 다음과 같다.

package christmas.dto;

import christmas.domain.Menu;
import java.util.Map;

public record OrderDTO(
        Map<String, Integer> orders
) {
    private static final String ERROR_FORMAT = "[ERROR] %s";
    private static final String CANNOT_ORDER_ONLY_JUICE = "음료만 주문할 수 없습니다.";
    private static final String INVALID_MAX_COUNT_FORMAT = "메뉴는 한 번에 최대 %d개까지만 주문할 수 있습니다.";
    private static final String INVALID_MIN_COUNT_FORMAT = "메뉴는 최소 %d개 이상부터 주문할 수 있습니다.";
    private static final int MAX_MENU_COUNT = 20;
    private static final int MIN_MENU_COUNT = 1;
    private static final String INVALID_ORDER = "유효하지 않은 주문입니다. 다시 입력해 주세요.";

    public OrderDTO {
        validateMenuNames(orders);
        validateOnlyJuice(orders);
        int totalCount = getTotalCount(orders);
        validateMaxMenuCount(totalCount);
        validateMinMenuCount(totalCount);
    }

    private void validateMenuNames(Map<String, Integer> orders) {
        if (!isMenuNamesPresent(orders)) {
            throw new IllegalArgumentException(String.format(ERROR_FORMAT, INVALID_ORDER));
        }
    }

    private boolean isMenuNamesPresent(Map<String, Integer> orders) {
        return orders.keySet().stream()
                .anyMatch(menuName -> Menu.findMenuByName(menuName.toUpperCase()).isPresent());
    }

    private void validateOnlyJuice(Map<String, Integer> orders) {
        if (orders.keySet().stream()
                .map(Menu::findMenuByName)
                .allMatch(menu -> menu.map(value -> "음료".equals(value.getCategory())).orElse(false))) {
            throw new IllegalArgumentException(String.format(ERROR_FORMAT, CANNOT_ORDER_ONLY_JUICE));
        }
    }

    private void validateMaxMenuCount(final int totalCount) {
        if (totalCount > MAX_MENU_COUNT) {
            throw new IllegalArgumentException(
                    String.format(ERROR_FORMAT, String.format(INVALID_MAX_COUNT_FORMAT, MAX_MENU_COUNT)));
        }
    }

    private void validateMinMenuCount(final int totalCount) {
        if (totalCount < MIN_MENU_COUNT) {
            throw new IllegalArgumentException(
                    String.format(ERROR_FORMAT, String.format(INVALID_MIN_COUNT_FORMAT, MIN_MENU_COUNT)));
        }
    }

    private int getTotalCount(Map<String, Integer> orders) {
        return orders.values().stream()
                .mapToInt(Integer::intValue)
                .sum();
    }

    public static int totalOrderPrice(Map<String, Integer> orders) {
        return orders.entrySet().stream()
                .mapToInt(entry -> Menu.findMenuByName(entry.getKey())
                        .map(menu -> menu.getMenuPrice() * entry.getValue())
                        .orElse(0))
                .sum();
    }
}

이와 같이 OrderDTO를 생성과 동시에 유효성 검사를 진행하여 올바른 데이터만 전달할 수 있다.

예외처리

이번 미션에서는 전 주차보다 다양한 예외가 발생했다. IllegalStateException 또한 처리했어야 했는데 전주차에 구현 시간 내에 개발을 하려고 몰두하다보니 따로 예외 클래스를 선언하지 않고 dto와 도메인 모델에서 유효성 검사를 진행하면서 해당 예외를 던지고 컨트롤러에서 해당 예외를 try - catch로 잡고 다시 입력하게끔 하였는데, 전주차에 적용했던 게임 전역의 예외를 처리하는 예외 클래스를 선언하는 방법이 더 나은 방법이라는 생각이 든다.

 

프리코스를 마치며

한달 내내 함께하던 프리코스가 끝나니 허전한 기분이 든다. 다시 마음을 붙잡고, 프리코스를 위해 제쳐뒀던 CS 공부, 웹 기초, Spring 강의 등을 열심히 들으면서 내년 취업을 위해 노력해야겠다. 이런 좋은 경험과 기회를 제공해준 우아한테크코스에 감사드리며 3, 4주차 리뷰를 마치겠다.

반응형
profile

나를 기록하다

@prao

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!

profile on loading

Loading...