우아한테크코스/프리코스

우아한테크코스 프리코스 3주차 과제 후기 - 로또 게임

leexx 2023. 11. 8. 23:51

과제

 

GitHub - snaag/java-lotto-6

Contribute to snaag/java-lotto-6 development by creating an account on GitHub.

github.com

 

아래 네 단계를 수행하는 과제였다.

  1. 구매 금액에 따라 로또를 구매
  2. 로또 장수만큼 로또 번호를 생성
  3. 당첨번호 및 보너스 번호를 지정
  4. 수익이 얼마나 되는지 구현

로또는 왠지 익숙해서 (낙첨도 익숙^^...) 간단할거라고 생각했지만 오산이었다. 생각보다 구현 조건은 고려할 게 많았다. 로또 살때는 쉬웠는데 당첨 규칙이 이리 복잡할줄이야... 맨날 등수랑 돈만 봐서 몰랐다 ㅎ;

 

후기

구조

 

 

고려했던 것들

지난번 과제를 통해 배운 점들을 적용했다. 구조를 보다 빠르고 디테일하게 설계하려고 했다.

그리고 다른 분들과 코드리뷰를 하며 배운 점들을 적용했다. 테스트 코드나 Enum 사용, 적절한 변수명, controller 와 service 의 의존성 등 많은 것을 배울 수 있었다.

 

 

구조에 대한 고민

2주차 과제를 하며 구조에 대해 많이 고민하고 배웠다. 그래서 요번 3주차 과제에서는 적용해보는 연습을 했다.

  1. 먼저 일이 어떤 순서로 일어나는지에 대한 flow 를 생각한다.
  2. 무엇이 필요할지 정확하게는 모르지만, 일단은 두루뭉술하게 역할을 나눈다.
  3. 역할들이 해야 할 일과, 그를 위해 어떤 것을 주고받아야 하는지 생각한다. 이 과정에서 각 역할은 하는 일이 뚜렷해져 커플링이 적게 분리가 된다. 그리고 역할 간의 주고받을 값들을 구체적으로 정할 수 있다.

각 역할의 분리는 내공만큼 잘 나오는 것 같다. 2주차 과제보다 3주차 과제의 구조 설계가 더 빠르게 끝났고, 분리도 더 잘 되었다.

4주차 과제에서는 과연..! *_*

 

여담이지만 이런 설계 흐름이 어떤 책에 써있다는 글을 보게 되었다... *_* 기분이 좋아졌다~~~끼요홋

 

 

테스트 코드 작성

지난번 코드리뷰를 하며 테스트 코드를 상세히 짠 코드들을 봤었다. 그리고 테스트 코드 실행 시 변수를 넣어주는 등 다양한 방법도 알게 되었다. 그래서 이번엔 그 방법을 적용해보았다.

 

// CsvSource 로 변수를 넣어주는 방법

@DisplayName("적절한 금액이 아닌 경우")
@ParameterizedTest
@CsvSource({
        "1500",
        "2100",
        "310j",
        "-1000"
})
void testPriceValidate(String price) {
    assertThatThrownBy(() -> buySystem.validatePrice(price))
            .isInstanceOf(IllegalArgumentException.class);
}

 

// Stream 을 만들어서 넣어주는 방법 

@DisplayName("갯수 및 컴마 구분 테스트")
@ParameterizedTest
@MethodSource("providedNumbersCount")
void testCheckWinningNumbersCount(String numbers) {
    assertThatThrownBy(() -> Validations.checkWinningNumbersCount(numbers))
            .isInstanceOf(IllegalArgumentException.class);
}

static Stream<Arguments> providedNumbersCount() {
    return Stream.of(
            Arguments.of("1,2,3,4,5,6,7,8"),
            Arguments.of("123,4,5,6,7,8"),
            Arguments.of("123_4,5,6,7,8")
    );
}

 

몰랐을 때는 하나의 테스트 안에서 반복문으로 처리하거나, 값을 출력해서 비교하거나, 아니면 매번 테스트를 만들었었다.

앞으론 더 간결하게 할 수 있을 것 같다.

 

Controller 와 Model 간의 의존 

이전에는 Controller 안에서 Model, View 를 선언해서 사용했다. 그러나 이렇게 하면 단위 테스트가 어려웠다. 왜냐하면 생성자에 특정 값을 넣어줘야 하기 때문이었다.

 

그래서 Controller 를 실행하는 Application 에서 Model, View 를 만들었다. 그리고 Controller 에서는 이들간에 setter 를 통해 값을 넘겨주도록 했다. 

 

public class Application {
    public static void main(String[] args) {
        LottoBuySystem buySystem = new LottoBuySystem();
        LottoDrawSystem drawSystem = new LottoDrawSystem();
        LottoView view = new LottoView();
        LottoController controller = new LottoController(buySystem, drawSystem, view);

        controller.run();

    }
}

 

 

Enum 사용

Enum 은 변하지 않을 값들을 관리하기에 적절하다고 한다. 그래서 텍스트 값만 있고, 변하지 않을 값인 에러 메시지를 Enum 으로 관리했다. 

 

package lotto.utils;

import static lotto.utils.Constants.ERROR_PREFIX;

public enum LottoMessages {
    WRONG_WINNING_NUMBERS(ERROR_PREFIX + "당첨번호 6개와 보너스 번호 1개를 입력해주세요. 번호는 숫자,로 이루어져있어야 합니다."),
    WRONG_NUMBER(ERROR_PREFIX + "숫자를 입력해주세요."),
    PRICE_ZERO(ERROR_PREFIX + "금액은 0이 될 수 없습니다."),
    PRICE_DIVIDE_BY_LOTTO_PRICE(ERROR_PREFIX + "로또 금액으로 나누어져야 합니다."),
    OUT_OF_LOTTO_NUMBER_BOUND(ERROR_PREFIX + "1 이상 45 이하의 숫자를 입력해야 합니다."),
    DUPLICATE_NUMBER(ERROR_PREFIX + "중복된 숫자는 입력할 수 없습니다."),
    DIVIDE_BY_ZERO(ERROR_PREFIX + "0으로 나눌 수 없습니다.")

    ;

    private final String kr;
    LottoMessages(String givenKr) {
        this.kr = givenKr;
    }

    public String getKr() {
        return this.kr;
    }
}

 

 

간결한 코드에 대한 고민

이번에도 마찬가지로 함수는 각 함수는 15줄 이하, depth 는 최대 2까지 였다. 2주차 과제까지는 여차저차 되었는데 요번 과제는 간결하게 하려고 머리를 꽤나 싸맸었다.

왜냐하면 복잡한 당첨 로직과 입력 시 에러가 발생해도, 계속 입력을 해야하는 조건이 추가되었기 때문이다. 

 

간결한 코드에 대한 고민 - 당첨 로직

당첨 로직은 얼핏 봤을 때 패턴화를 하기가 어려웠다. 2등 당첨조건과 보너스볼의 존재 때문이었다. 

- 1등: 6개 번호 일치 / 2,000,000,000원
- 2등: 5개 번호 + 보너스 번호 일치 / 30,000,000원 # 이녀석!!!!
- 3등: 5개 번호 일치 / 1,500,000원
- 4등: 4개 번호 일치 / 50,000원
- 5등: 3개 번호 일치 / 5,000원

 

 

알고있어야 하는 정보도 많이 있었다. 맞춰야 하는 공의 갯수, 보너스 공 당첨 여부, 당첨금, 그에 따른 메시지가 그것들이다. 무엇보다 2등의 당첨 조건과 보너스볼의 존재로 인해 간결한 분리가 쉽지 않아보였다.

 

단순 분기문으로는 로직을 구현할 수가 없었다. 왜냐하면 문제에 명시된 간결한 코드 규칙 이 있기 때문이었다. 분기문을 쓰자니 else 가 들어가고, 그렇다고 else 를 쓰지 않자고 return 을 쓰니 15줄을 넘어버린다.

 

이에 꽤 오랜 고민을 했고, 알아야 하는 값들을 분리했다. 등수, 당첨 번호의 갯수, 상금, 메시지 로 분리하였다.

분리하고 나니 어떤 값들이 이들 값들을 결정하는 인자인지 알게 되었다. 그렇게 배열 로 관리하기로 했다.

  • 등수(k) 는 당첨번호의 갯수(x), 보너스번호 당첨 여부(y) 로 결정이 된다. 그래서 2차원 배열로 만들었다.
  • 당첨 번호의 갯수(k) 는 (일반 번호 기준) 등수(x) 로 결정이 된다. 1차원 배열로 만들었다.
  • 상금(k) 는 등수(x) 로 결정이 된다. 1차원 배열로 만들었다.
  • 메시지(k) 도 등수(x) 로 결정이 된다. 1차원 배열로 만들었다.
더보기
package lotto.model;

import lotto.utils.Utils;

import static lotto.utils.Constants.LOTTO_DRAW_NUMBER_COUNT;

public class Prize {
    public int[][] GRADE_BOARD;
    public int[] MONEY_BOARD;
    public int[] MATCH_COUNT_BOARD;

    public String[] MESSAGE_BOARD;

    public int GRADE_COUNT = 5;
    public int FIRST_SAME_COUNT = 6;
    public int SECOND_SAME_COUNT = 5;
    public int THIRD_SAME_COUNT = 5;
    public int FOURTH_SAME_COUNT = 4;
    public int FIFTH_SAME_COUNT = 3;
    public int BONUS_BALL_SAME_COUNT = 1;

    public int FIRST_MONEY = 2_000_000_000;
    public int SECOND_MONEY = 30_000_000;
    public int THIRD_MONEY = 1_500_000;
    public int FOURTH_MONEY = 50_000;
    public int FIFTH_MONEY = 5_000;

    public Prize() {
        this.setGradeBoard();
        this.setMoneyBoard();
        this.setMatchCountBoard();
        this.setMessageBoard();
    }


    private int getBonusSameCount(boolean bonusSame) {
        if(bonusSame) {
            return 1;
        }
        return 0;
    }

    public int getGrade(int sameNumbersCount, boolean bonusSame) {
        int bonusSameCount = getBonusSameCount(bonusSame);

        return this.GRADE_BOARD[sameNumbersCount][bonusSameCount];
    }

    public int getMoney(int grade) {
        return this.MONEY_BOARD[grade];
    }

    public String getMessage(int grade) {
        return MESSAGE_BOARD[grade];
    }


    public void setGradeBoard() {
        int rows = FIRST_SAME_COUNT + 1;
        int cols = BONUS_BALL_SAME_COUNT + 1;

        GRADE_BOARD = new int[rows][cols];

        GRADE_BOARD[FIRST_SAME_COUNT][0]
                = GRADE_BOARD[FIRST_SAME_COUNT-1][BONUS_BALL_SAME_COUNT] = 1;
        GRADE_BOARD[SECOND_SAME_COUNT][BONUS_BALL_SAME_COUNT] = 2;
        GRADE_BOARD[THIRD_SAME_COUNT][0]
                = GRADE_BOARD[THIRD_SAME_COUNT-1][BONUS_BALL_SAME_COUNT] = 3;
        GRADE_BOARD[FOURTH_SAME_COUNT][0]
                = GRADE_BOARD[FOURTH_SAME_COUNT-1][BONUS_BALL_SAME_COUNT] = 4;
        GRADE_BOARD[FIFTH_SAME_COUNT][0]
                = GRADE_BOARD[FIFTH_SAME_COUNT-1][BONUS_BALL_SAME_COUNT] = 5;
    }

    public void setMoneyBoard() {
        MONEY_BOARD = new int[GRADE_COUNT + 1];
        MONEY_BOARD[1] = FIRST_MONEY;
        MONEY_BOARD[2] = SECOND_MONEY;
        MONEY_BOARD[3] = THIRD_MONEY;
        MONEY_BOARD[4] = FOURTH_MONEY;
        MONEY_BOARD[5] = FIFTH_MONEY;
    }

    public void setMatchCountBoard() {
        MATCH_COUNT_BOARD = new int[LOTTO_DRAW_NUMBER_COUNT+1];
        MATCH_COUNT_BOARD[1] = FIRST_SAME_COUNT;
        MATCH_COUNT_BOARD[2] = SECOND_SAME_COUNT;
        MATCH_COUNT_BOARD[3] = THIRD_SAME_COUNT;
        MATCH_COUNT_BOARD[4] = FOURTH_SAME_COUNT;
        MATCH_COUNT_BOARD[5] = FIFTH_SAME_COUNT;
    }

    public void setMessageBoard() {
        MESSAGE_BOARD = new String[GRADE_COUNT + 1];
        MESSAGE_BOARD[1] =
                MATCH_COUNT_BOARD[1] + "개 일치 (" + Utils.getFormattedMoney(MONEY_BOARD[1]) + "원)";
        MESSAGE_BOARD[2] =
                MATCH_COUNT_BOARD[2] + "개 일치, 보너스 볼 일치 (" + Utils.getFormattedMoney(MONEY_BOARD[2]) + "원)";
        MESSAGE_BOARD[3] =
                MATCH_COUNT_BOARD[3] + "개 일치 (" + Utils.getFormattedMoney(MONEY_BOARD[3]) + "원)";
        MESSAGE_BOARD[4] =
                MATCH_COUNT_BOARD[4] + "개 일치 (" + Utils.getFormattedMoney(MONEY_BOARD[4]) + "원)";
        MESSAGE_BOARD[5] =
                MATCH_COUNT_BOARD[5] + "개 일치 (" + Utils.getFormattedMoney(MONEY_BOARD[5]) + "원)";
    }


}

 

작성할 땐 최선을 다했다~ 였는데 지금보니 빈 곳이 많다 ㅎ... 상수화 할 곳도 보이고 조금더 줄일 곳도 보이고 ^^...... 함수 호출 중복도 보이고 ^^........................ 하 다음엔 더 좋은 코드로 찾아뵙겠습니다......

 

간결한 코드에 대한 고민 - 입력 로직

이번 과제에선 새로운 입력 조건이 추가되었다. 바로 입력 시 에러가 발생해도, 해당하는 에러 문구를 출력하고 계속 입력을 받는 것이었다.

  1. 입력을 받는다.
  2. IllegalArgumentException 에러가 발생한다. 
  3. 다시 1번으로 돌아가서 입력을 받는다.
  4. 잘 입력을 받았으면 입력 값을 반환한다.

 

에러 발생 여부 입력 값 두 가지만 분리하면 공통 로직인 것이다. 그래서 이 값을 DTO 로 만들고, controller 에서 입력 시에 DTO 를 보고 재입력 여부를 결정했다.

 

더보기
package lotto.dto;

public class PriceEnter {
    String price;
    boolean inputValid;

    public PriceEnter(String price, boolean inputValid) {
        this.price = price;
        this.inputValid = inputValid;
    }

    public String getPrice() {
        return price;
    }

    public boolean isInputValid() {
        return inputValid;
    }

    public void setPrice(String price) {
        this.price = price;
    }

    public void setInputValid(boolean inputValid) {
        this.inputValid = inputValid;
    }
}

 

package lotto.controller;

public class LottoController {
    // ...

    private String enterPrice() {
        String price = "";
        boolean validInput = false;
        PriceEnter dto = new PriceEnter(price, validInput);

        while (!dto.isInputValid()) {
            _enterPrice(dto);
        }

        return dto.getPrice();
    }

    private void _enterPrice(PriceEnter dto) {
        this.view.printEnterLottosCount();
        try {
            String price = readLine();
            dto.setPrice(price);
            lottoBuySystem.validatePrice(dto.getPrice());

            dto.setInputValid(true);
        } catch (IllegalArgumentException e) {
            System.out.println(e.getMessage());
        }
    }
}

 

 

 

반응형