JAVA/우아한테크코스 4기

[211211] 우아한테크코스 프리코스 3주차 후기

kth990303 2021. 12. 11. 20:19
반응형

프리코스는 총 3주동안 3개의 미션이 존재한다.

1,2주차 미션은 비교적 가볍고 작년 기수들이랑 같은 주제면서 난이도도 부담가지 않는 한도 내에서 미션을 내준다고 한다.

그러나... 3주차 미션은 꽤 높은 난이도로 나온다는 소문이 많았으며, 작년 기수와 같았던 1,2주차 미션과 달리 아예 새로운 미션으로 낸다는 소문이 있었기 때문에 적당히 긴장하면서 기다리고 있었다.

 

그리고 12월 8일 수요일! 3주차 미션이 공개되었다.

미션 주소는 아래와 같다.

https://github.com/woowacourse/java-vendingmachine-precourse

 

자판기? 작년 3기 프리코스 3주차 미션 치고는 간단한거 같은데?

맞다. 직접 해보지는 않았지만, 작년 기수에 비해선 간단한 것 같다. 

 

그렇다고 절대 만만하게 볼 난이도는 아니었다.

특히나, 지난 2주차 미션의 피드백을 받고 난 후 그동안 가볍게 클래스 분리하면서 코드를 짰던 1,2주차 미션과 달리, 3주차 미션에 mvc 패턴을 도입해보려는 시도를 했던 나에게는 더더욱 만만치 않은 미션이었다.

내 코드는 https://github.com/kth990303/java-vendingmachine-precourse/tree/kth990303 여기서 볼 수 있다.


Mvc 패턴 도입

먼저 지난 2주차 프리코스 미션 후 비즈니스 로직과 UI 로직을 분리하라는 피드백을 받았다. 

자세한 내용은 아래 포스팅을 참고하자.

https://kth990303.tistory.com/226

 

[211206] 우아한테크코스 프리코스 2주차 후기

1주차 후기: https://kth990303.tistory.com/220 이번 2주차 미션은 3기 기수분들도 작년에 진행했던 '자동차 경주 게임'이었다. 자세한 요구사항은 https://github.com/woowacourse/java-racingcar-precourse 로..

kth990303.tistory.com

이러한 이유로 view와 model의 비즈니스 로직이 분리되는 mvc 패턴을 도입하기로 결정.

특히 이번 자판기 프로젝트는 Coin, Machine, Goods(Products) 등 클래스가 꽤 많기 때문에 mvc패턴을 도입함으로써 얻는 이득이 클 것이라 판단했다.

 

MVC 패턴이란, Model, View, Controller로 분리하는 패턴이며, model은 특정 객체의 필드값들을 담은 도메인, View는 클라이언트들에게 정보를 보여주며, Controller는 Model과 View끼리 직접 소통하며 사용자가 원하는 기능을 처리하도록 model에게 전달하고, view에게 사용자가 원하는 기능을 출력하도록 전달하는 중개자 역할이다.

 

이번 프로젝트에서 나는

domain, Controller, View, Validator

로 나누어서 진행했으며,

domain에서 비즈니스 로직이 많을 경우 service를 만들어 domain과 service를 분리해주었다.

 

MVC 패턴을 도입하면서 느낀 점은,

일단 클래스 분리가 역할에 따라 세세하게 분리되다보니, 지난 주차 미션 프로젝트 코드에 비해서 하나의 클래스가 차지하는 라인 수가 많이 줄어들게 되었다.

굉장히 많은 클래스, 그에 반비례하는 클래스 당 라인 수

물론 그만큼 클래스가 많이 생겨나긴 했지만, 오히려 특정 이슈가 발생했을 때 유지보수하기엔 훨씬 편해진 것 같다.

단일 책임 원칙의 중요성인 듯.

 

하지만 초기 설계에 시간이 좀 걸린다는 단점이 있다.

따라서 최종 코테 전까지 행동양식을 준비하면서 열심히 연습해야 될 듯하다.


비즈니스 로직과 UI 로직을 분리하기 위한 View 생성 및 toString 사용

그동안은 비즈니스 로직을 처리하는 클래스 내에 이에 대한 정보를 출력하는 UI 로직 메소드를 작성했었다. 그러나 이 경우는 단일 책임의 원칙에 위배된다.

따라서, 로직을 분리하도록 각 도메인마다 InputView, OutputView를 만들어주었다. 입력과 출력 또한 분리해주는 것이 좋다고 판단했기 때문.

inputView, outputView가 존재하는 디렉토리 구조

그리고 특정 객체의 상태를 출력하는 것은 toString을 이용해주는 것이 좋다는 피드백 덕분에 이번 기회에 toString을 이용해보았다.

toString으로 코인의 상태를 나타내준다.


상당히 생각할 점이 많았던 예외처리

mvc패턴을 도입하면 항상 고민되는 점이 있다.

도대체 예외처리를 Controller 쪽에서 할지, Service쪽에서 할지.

이 주제는 꽤 많은 사람들이 고민하는 것이라 한다.

보통 예외처리는 Service에서 주로 하는 것이라 하는데, 이번 프로젝트에서 나는 Controller 쪽에서 예외처리를 해주었다.

Controller에서의 예외처리

그 이유는 사실 간단한데,

예외가 발생하면 다시 그 부분부터 재입력을 받아야 했기 때문이다.

입력을 다루는 쪽은 Controller와 InputView인데, 

View는 오로지 입력받는 역할에 충실해야되지 않을까 생각하여 Controller에서 예외를 처리하도록 했다.


또, 이번 프로젝트는 예외처리해야 할 상황이 상당히 많았기 때문에 아예 에러메시지 내용을 하나의 클래스에 담았다.

package vendingmachine;

public class ValidatorMessage {
    public static final String ERROR_MESSAGE = "[ERROR] ";
    public static final String NUMBER_REGEX = "^(0|[1-9][0-9]*)$";
    public static final String NATURAL_NUMBER_REGEX = "^[1-9][0-9]*$";

    public static final String NULL_PRODUCT_MESSAGE = "존재하지 않는 상품입니다.";
    public static final String NOT_ENOUGH_AMOUNT = "잔액이 부족합니다.";
    public static final String NOT_ENOUGH_STOCK = "재고가 부족합니다. 다른 상품을 구매해주세요.";
    public static final String IS_NUMBER_MESSAGE = "음수나 소수점, 단위가 없는 숫자로만 입력해주세요.";
    public static final String IS_NATURAL_NUMBER_MESSAGE = "자연수로 입력해주세요.";
    public static final String NULL_MESSAGE = "비어있는 입력이 존재합니다.";

    public static final String AMOUNT_TENFOLD_NUMBER_MESSAGE = "보유 금액은 0을 제외한 10의 배수여야 합니다.";

    public static final String PRODUCT_NAME_INVALID_MESSAGE = "상품명에는 쉼표(,)나 괄호가 들어갈 수 없습니다.";
    public static final String PRODUCT_NAME_DUPLICATE_MESSAGE = "중복된 상품명은 입력할 수 없습니다.";
    public static final String PRODUCT_INFO_INVALID_MESSAGE = "상품 정보를 올바른 형식으로 입력해주세요.";
    public static final String PRODUCT_MIN_PRICE_MESSAGE = "상품 금액은 100원 이상이어야 합니다.";
    public static final String PRODUCT_PRICE_TENFOLD_MESSAGE = "상품 금액은 10의 배수여야 합니다.";
    public static final String PRODUCT_PRICE_NUMBER_MESSAGE = "상품 가격은 숫자여야 합니다.";
    public static final String PRODUCT_COUNTS_NUMBER_MESSAGE = "상품 수량은 자연수여야 합니다.";

    public static final String INPUT_MONEY_NATURAL_NUMBER_MESSAGE = "투입 금액을 넣지 않으셨습니다. 0원보다 크게 입력해주세요.";

    public static void printError(String errorMessage) {
        System.out.println(errorMessage);
    }
}

정말 수많은 예외들이 있다.

아, 그리고 예외를 출력해주는 getMessage 부분이 중복되는 코드가 많아져, 아예 예외를 출력하는 메소드는 static메소드로 분리해주었다.

 

그럼 예외가 왜 이렇게 많냐,

바로 이 조건 때문이다.

이런 문자열 조건은 상당히 예외처리할 게 많아질 수 밖에 없다.

특히 특정 문자열 형식은 예외처리할 점이 상당히 많다는 점은, 코딩테스트를 치뤄봤다면 더더욱 뼈저리게 느껴질 것이다.

;로 구분이 잘 돼있는지, 쉼표(,)로 구분이 잘 돼있는지, 상품 가격이나 수량은 정수인지, 빈 문자열은 어떻게 처리할 것인지, 상품명은 모든 언어를 수용할 것인지, 공백을 수용할 것인지 등등...

 

나의 경우는 상품명은 크게 제약을 걸지않았다. 상품명이 일본어거나, '맛있는 오렌지'와 같이 공백일 가능성이 충분하다고 판단했기 때문이다.

단, 중복되는 상품명은 허용하지 않도록 했다. 사실상 id(key)나 다름 없는 것이 상품명이라 판단했기 때문이다.

 

이렇기 때문에 Validator 내의 메소드에 위 사진과 같이 함수분리를 굉장히 많이 하는 상황이 발생했다.

따라서 함수 로직의 가독성이 굉장히 중요했으며, 변수명과 로직을 최대한 이해할 수 있도록 코드를 작성하였다.


사실상 setter나 다름 없는 메소드

자, 아래 도메인 클래스 코드를 보자.

무엇이 문제일까~?

특정 메소드가 조금 껄끄럽게 보이지 않는가?

나는 저 public void plusCoin() 메소드가 좀 껄끄럽게 느껴졌다.

setter 메소드처럼 아예 특정 값을 설정한대로 바꿔버리는 메소드는 아니지만, Coin의 개수를 밖에서 함부로 증가시켜버릴 수 있는 위험한 메소드이다. 

 

저 메소드가 있어야 하는 이유는 자판기 보유금액을 입력받으면, 그에 따라 CoinService에서 COIN의 개수를 증가시키는 로직을 작성해야 했기 때문이다. 만약 private였다면 증가 로직을 작성할 수가 없게 된다.

 

CoinService는 CoinController에서 접근 가능하다.

그리고 CoinController는 자판기 로직을 총괄하는 VendingMachine 클래스 (사실상 이번 프로젝트에서의 Application 클래스)에서 접근 가능하다.

즉, VendingMachine 클래스에서 getter 메소드를 통해 COIN에 접근 후 plusCoin 메소드를 사용하면 코인을 함부로 증가시켜버릴 수 있다는 것이다.

(사실 이번 프로젝트에서의 COIN은 static으로 주어졌기 때문에 그냥 VendingMachine클래스에서 COIN_500.plusCoin()과 같이 바로 접근해버릴 수 있다.)

 

위와 같은 메소드는 코인 뿐만 아니라 상품재고 감소, 투입금액 감소와 같은 다른 도메인에도 어쩔 수 없이 들어가 있는 상황이다.

단지, setter의 위험성 때문에 직접 값을 원하는대로 설정해버리는 setter 대신, 특정 값을 증가/감소 시키는 로직을 넣어서 위험성을 줄였을 뿐, 위험성 자체는 남아있다고 생각한다.

 

사실 정말 필요한 경우는 도메인에 위와 같은 로직을 이용하는 메소드를 추가할 수밖에 없다고 한다.

스프링과 같은 프레임워크 스프링시큐리티와 같은 라이브러리를 통해 보안을 철저하게 하는 방법으로 위험성을 낮출 수 있다고 하긴 하는데, 우리와 같이 콘솔에서 자바 로직을 짤 때 어떻게 하면 더 안전하고 객체지향적은 코드를 짤 수 있을지 이번 프로젝트를 통해 더 깊게 생각해보는 계기가 되었다.

 

그리고 정답은 아직 찾지 못했다 ㅜㅜ


굉장히 편리한 stream, lambda

1주차 포스팅에도 썼지만, 구현량이 많이 요구되는 이번 미션에서 더더욱 stream과 lambda의 편리성을 체감할 수 있었다.

indent 2에서 indent 0으로 줄였을 때의 그 쾌감이란...

 

또한 이번 프로젝트를 통해 anyMatch와 allMatch의 기능을 새로 알게 됐다.

어렵지 않은 내용이므로 아래 포스팅으로 대체한다.

https://kth990303.tistory.com/228

 

[JAVA] 스트림 filter을 anyMatch, allMatch로 바꿔보자

코드 indent(들여쓰기)를 최소화하기 위해서 filter를 사용하면서 github에 commit하려던 와중에 아래 메시지가 발생했다. 자바 List 컬렉션에서 Stream api를 통해 특정 조건을 만족하는 원소가 하나라도

kth990303.tistory.com

처음 stream 과 lambda를 익혔을 땐 조금 가독성이 떨어지는 편 아닐까 걱정도 했지만,

요즘 자바8 내용은 기본 소양이나 마찬가지이고,

코드 생산성이나 편리성을 상당히 높여주는 내용이기 때문에, 쓸 수 있으면 stream과 lambda를 쓰자고 생각이 바뀌는 중이다 ㅎㅎ


와! 커밋수 내 전역일 디데이보다 많다!

구현량과 예외처리가 많았던 미션인만큼 재밌게 코드를 짰다.

난이도가 1,2주차 미션보다 높아져서 그만큼 흥미롭게 코드 작성도 하고, mvc 패턴을 이번 미션에 처음 도입해본 만큼 설레고 흥미롭게 다가온 듯하다.

 

최종 코테가 이번 3주차 미션과 유사하게 나온다는 소문이 있어서 꽤나 긴장된다.

이번 미션을 구현하는 데에 걸린 시간은 최소 8~9시간 정도.

5시간 내에 수많은 예외처리와 설계를 mvc 패턴으로 빠르게 잘 할 수 있을지 걱정도 된다.

열심히 연습해서 최종 코테에서 좋은 결과를 냈으면 좋겠다.

 

반응형