본문 바로가기
개발관련/이것저것

우테코 프리코스 풀어보기 - 로또

by yjoo_ 2023. 11. 8.
 

GitHub - Jym-lab/java-lotto-6

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

github.com

어유 토나와...

이번거는 생각보다 오래 걸렸다. 바쁜 시즌이라 건들 시간이 없던 것도 한몫했지만

 

구조를 짜는데 집중해서 오래걸렸다.

 

이번에는 코드가 좀 길어졌는데 객체 지향과 MVC 패턴대로 설계하려다보니 좀 길다.

 

조금 귀찮더라도 코드들도 포함해서 작성해보도록 하겠다.


문제 요구사항 파악

 

 

이번 문제 요구사항도 사실 별거 없다.

 

1. 1000원짜리 로또를 입력 금액만큼 발행한다.

2. 로또가 발행되었으면 당첨 번호와 보너스 번호를 입력한다

3. 발행된 로또가 총 몇개가 당첨되었는지, 수익률이 얼마인지 출력한다.

4. 잘못된 입력을 받을 경우 [ERROR] 이 포함된 에러메시지를 출력하며 다시 입력 받는다.

 

 

이것만 따지면 참 별거없는데...

풀이 과정

일단 내가 구현하기 위해 구조를 설계하면서 docs를 작성했다.

 

추가 요구 사항에서 집중한 부분은 도메인 로직에 단위 테스트를 구현하는 것.

 

도메인 로직이 무엇인가부터 알아야 했다.

도메인 로직은 도메인을 수행할 로직을 뜻한다.

 

그래서 패키지 자체를 domain으로 따로 나눠두고, 해당 패키지에 대한 테스트 코드만 작성하고자 했다.

 

 View, Controller 패키지도 따로 나눠서 코드를 작성하기로 했다.

구조 설계를 이렇게 끝냈다.

 

class와 method 설계에 대한 내용은 이 링크에서 확인하면 된다.

https://github.com/Jym-lab/java-lotto-6/tree/yjoo/docs

 

1.  UserAmount - 로또 구입 금액 처리

TDD(Test Driven Development) - 테스트 주도 개발

테스트 코드를 먼저 작성 한 뒤 기능을 구현하는 방법론이다.

 

협업 시 테스트 코드를 보고, 해당 프로그램이 무엇을 의도하는지 쉽게 알아볼 수 있고

 

튼튼한 객체 지향 설계를 할 수 있게 된다. 물론 개발 시간이 그만큼 느려지긴 하지만

 

난 도메인 단위로 구현할 때 마다 테스트 코드를 먼저 작성하고 기능을 구현했다.

public class UserAmountTest {
    /*
     * 여기서부터 로또 구입 금액 테스트
     * 1. 숫자가 음수인지
     * 2. 1000으로 나누어 떨어지는지
     */
    @DisplayName("구입 금액이 음수인 경우 예외가 발생한다.")
    @Test
    void amountIsNegative() {
        assertThatThrownBy(() -> new UserAmount(-180000))
                .isInstanceOf(IllegalArgumentException.class);
    }

    @DisplayName("구입 금액이 1000으로 나누어 떨어지지 않으면 예외가 발생한다.")
    @Test
    void amountNotDivisibleByThousand() {
        assertThatThrownBy(() -> new UserAmount(180100))
                .isInstanceOf(IllegalArgumentException.class);
    }
}

 

제일 먼저 입력받는 로또 구매 금액

 

입력을 담당하는 클래스에서 int형으로 변환해서 넘겨 줄 예정이니

 

두가지 케이스만 테스트하면 됐다. 입력 값이 음수인지, 1000원으로 나누어 떨어지지 않는지

 

그렇게 생각하니 실제 UserAmount 클래스에서 예외 처리 해주는 것도 위 두가지로 정리할 수 있었다.

 

public class UserAmount {
    private final int LOTTO_PRICE = 1_000;
    private final int amount;

    public UserAmount(int amount) {
        validateNegative(amount);
        validateDivide(amount);
        this.amount = amount;
    }

    public int lottoCount() {
        return this.amount / LOTTO_PRICE;
    }

    private void validateNegative(int amount) {
        if (amount <= 0) {
            ErrorMessage.NEGATIVE_NUMBER.print();
            throw new IllegalArgumentException();
        }
    }

    private void validateDivide(int amount) {
        if (amount % LOTTO_PRICE != 0) {
            ErrorMessage.NOT_DIVISIBLE_NUMBER.print();
            throw new IllegalArgumentException();
        }
    }
}

 

생성자에서 검사를 해준 뒤 값을 저장한다.

 

사실 처음에 생성자에서 String을 받아서 검사하는 코드를 작성했었는데

 

객체 지향 설계와는 좀 맞지 않기도 하고(1클래스 1역할), 검사 함수가 너무 많을 것 같아서

 

입력 받을 때 입력 클래스에서 검사까지 해서 int로 넘겨주기로 했다.

 

에러 메시지 출력은 Java Enum을 사용해 출력하도록 했다.

public enum ErrorMessage {
    NOT_A_NUMBER("[ERROR] 숫자만 입력해주세요."),
    NEGATIVE_NUMBER("[ERROR] 음수는 입력 불가합니다."),
    NOT_DIVISIBLE_NUMBER("[ERROR] 구매 금액은 1000원권만 사용해주세요."),
    DUPLICATED_NUMBER("[ERROR] 당첨 번호는 중복되지 않게 입력해주세요."),
    NUMBER_OUT_OF_RANGE("[ERROR] 입력 값은 1에서 45사이의 숫자를 입력해주세요."),
    LOTTO_SIZE_EXCEPTION("[ERROR] 로또 번호는 정확히 6개가 입력되어야 합니다.");

    private final String message;

    ErrorMessage(String message) {
        this.message = message;
    }

    public void print() {
        System.out.println(message);
    }
}

 

2.  InputView - 유저 입력 처리

사용자가 입력하는 값은 UI이므로 테스트 코드를 작성하지 않았다.

 

입력을 제대로 입력하지 않았다면 에러 출력 후 다시 입력받아야 했다.

 

그래서 try ~ catch로 에러 발생 시 다시 입력하도록 코드를 작성했다.

public class InputView {
    private final static String INPUT_AMOUNT = "구입금액을 입력해 주세요.";
    private final static String INPUT_WINNING_NUMBERS = "당첨 번호를 입력해 주세요.";
    private final static String INPUT_BONUS_NUMBER = "보너스 번호를 입력해 주세요.";

    public static int inputAmount() {
        try {
            System.out.println(INPUT_AMOUNT);
            return numberCasting(Console.readLine());
        } catch (IllegalArgumentException e) {
            return inputAmount();
        }
    }

    public static List<Integer> inputWinningNumbers() {
        try {
            System.out.println(INPUT_WINNING_NUMBERS);
            return listSplit(Console.readLine().split(","));
        } catch (IllegalArgumentException e) {
            return inputWinningNumbers();
        }

    }

    public static int inputBonusNumber() {
        try {
            System.out.println(INPUT_BONUS_NUMBER);
            return numberCasting(Console.readLine());
        } catch (IllegalArgumentException e) {
            return inputBonusNumber();
        }

    }

    private static int numberCasting(String number) {
        try {
            return Integer.parseInt(number);
        } catch (NumberFormatException e) {
            ErrorMessage.NOT_A_NUMBER.print();
            throw new IllegalArgumentException();
        }
    }

    private static List<Integer> listSplit(String[] numbers) {
        List<Integer> ret = new ArrayList<>();
        for (String number : numbers) {
            ret.add(numberCasting(number));
        }
        return ret;
    }
}

 

숫자 타입 변환 에러의 경우 IllegalArgumentException으로 넘겨줘야 했기 때문에 try문을 사용했다.

 

또한 제공해준 Lotto클래스에서는 생성자에서 바로 int형을 매개변수로 받았기 때문에

 

해당 코드를 유지하기 위해서 입력받을 때 파싱까지 완료해서 넘겨주었다.

3. Lotto - 로또 입력 및 생성 처리

이번거는 코드가 좀 길다

class LottoTest {
    /*
     * 2. 로또 당첨 번호 입력 에러
     * "1,2,3,4,5,6,7" - "로또 번호의 개수가 6개가 넘어가면 예외가 발생한다."
     * "1,1,2,2,3,3" - "로또 번호에 중복된 숫자가 있으면 예외가 발생한다."
     * "1,,2,3,4,5" - "연속적으로 콤마가 사용되면 예외가 발생한다." - Integer.parse에서 Exception
     * "1,2,3,4,5,3z2" - "숫자가 아닌 값이 입력되면 예외가 발생한다" - Integer.parse에서 Exception
     * "1,2,3,4,5,100" - "입력 값이 범위를 벗어나면 예외가 발생한다."
     * "1,2,3,4,5" - "로또 번호의 개수가 5개 이하라면 예외가 발생한다."
     */
    @DisplayName("로또 번호의 개수가 6개가 넘어가면 예외가 발생한다.")
    @Test
    void createLottoByOverSize() {
        assertThatThrownBy(() -> new Lotto(List.of(1, 2, 3, 4, 5, 6, 7)))
                .isInstanceOf(IllegalArgumentException.class);
    }

    @DisplayName("로또 번호에 중복된 숫자가 있으면 예외가 발생한다.")
    @Test
    void createLottoByDuplicatedNumber() {
        // TODO: 이 테스트가 통과할 수 있게 구현 코드 작성
        assertThatThrownBy(() -> new Lotto(List.of(1, 2, 3, 4, 5, 5)))
                .isInstanceOf(IllegalArgumentException.class);
    }

    @DisplayName("입력 값이 범위를 벗어나면 예외가 발생한다.")
    @Test
    void createLottoByOutOfRange() {
        assertThatThrownBy(() -> new Lotto(List.of(1, 2, 3, 4, 5, 100)))
                .isInstanceOf(IllegalArgumentException.class);
    }

    @DisplayName("로또 번호의 개수가 5개 이하라면 예외가 발생한다.")
    @Test
    void createLottoByUnderSize() {
        assertThatThrownBy(() -> new Lotto(List.of(1, 2, 3, 4, 5)))
                .isInstanceOf(IllegalArgumentException.class);
    }

    @DisplayName("보너스 번호가 입력 범위를 벗어났다면 예외가 발생한다.")
    @Test
    void createBounsNumberOutOfRange() {
        Lotto lotto = new Lotto(List.of(1, 2, 3, 4, 5, 6));
        assertThatThrownBy(() -> lotto.validateBonusRange(100))
                .isInstanceOf(IllegalArgumentException.class);
        assertThatThrownBy(() -> lotto.validateBonusRange(-1))
                .isInstanceOf(IllegalArgumentException.class);
    }

    @DisplayName("보너스 번호가 당첨 번호 리스트에 있다면 예외가 발생한다.")
    @Test
    void createBounsNumberDuplicate() {
        Lotto lotto = new Lotto(List.of(1, 2, 3, 4, 5, 6));
        assertThatThrownBy(() -> lotto.validateBonusDuplicate(6))
                .isInstanceOf(IllegalArgumentException.class);
    }
}

예외처리 핵심은 3가지.

1. 하나의 로또 리스트 내엔 중복 값이 없을 것

2. 숫자는 1에서 45사이의 범위를 벗어나지 않을 것

3. 하나의 로또는 6개의 숫자를 가질 것

 

이에 해당하는 테스트 코드를 작성한 뒤, 로또 클래스를 작성했다.

 


public class Lotto {
    /*
     * 2. 로또 당첨 번호 입력 에러
     * "1,2,3,4,5,6,7" - "로또 번호의 개수가 6개가 넘어가면 예외가 발생한다."
     * "1,1,2,2,3,3" - "로또 번호에 중복된 숫자가 있으면 예외가 발생한다."
     * "1,2,3,4,5,100" - "입력 값이 범위를 벗어나면 예외가 발생한다."
     */
    private final List<Integer> numbers;
    private final int LOTTO_MIN_NUMBER = 1;
    private final int LOTTO_MAX_NUMBER = 45;

    public Lotto(List<Integer> numbers) {
        validate(numbers);
        validateDuplicate(numbers);
        validateRange(numbers);
        this.numbers = numbers;
    }

    private void validate(List<Integer> numbers) {
        if (numbers.size() != 6) {
            ErrorMessage.LOTTO_SIZE_EXCEPTION.print();
            throw new IllegalArgumentException();
        }
    }

    private void validateDuplicate(List<Integer> numbers) {
        if (numbers.stream().distinct().count() < numbers.size()) {
            ErrorMessage.DUPLICATED_NUMBER.print();
            throw new IllegalArgumentException();
        }
    }

    private void validateRange(List<Integer> numbers) {
        boolean isOver = numbers.stream().anyMatch(num -> num > LOTTO_MAX_NUMBER);
        boolean isUnder = numbers.stream().anyMatch(num -> num < LOTTO_MIN_NUMBER);
        if (isOver || isUnder) {
            ErrorMessage.NUMBER_OUT_OF_RANGE.print();
            throw new IllegalArgumentException();
        }
    }

    public void validateBonusRange(int bonus) {
        if (bonus > LOTTO_MAX_NUMBER || bonus < LOTTO_MIN_NUMBER) {
            ErrorMessage.NUMBER_OUT_OF_RANGE.print();
            throw new IllegalArgumentException();
        }
    }

    public void validateBonusDuplicate(int bonus) {
        if (this.numbers.contains(bonus)) {
            ErrorMessage.DUPLICATED_NUMBER.print();
            throw new IllegalArgumentException();
        }
    }

    public List<Integer> getLottoNumbers() {
        return this.numbers;
    }
}

사실상 Integer형 List일 뿐이지만, 생성자에서 유효성 검사를 마치면 하나의 로또가 된다.

 

당연히 사용할 때 간편하다는 장점이 있다.

 

4. LottoGenerator - 로또 랜덤 생성

public class LottoGeneratorTest {
    private static final int LOTTO_MIN_NUMBER = 1;
    private static final int LOTTO_MAX_NUMBER = 45;
    private static final int LOTTO_NUMBER_COUNT = 6;
    private final List<Integer> lotto = LottoGenerator.generate();

    @DisplayName("로또 번호 개수가 LOTTO_NUMBER_COUNT 개수만큼 있는지 검사")
    @Test
    void testGenerateCount() {
        Assertions.assertEquals(LOTTO_NUMBER_COUNT, lotto.size());
    }

    @DisplayName("생성된 숫자들이 유니크한지 테스트")
    @Test
    void testUniqueNumber() {
        Assertions.assertEquals(LOTTO_NUMBER_COUNT, new HashSet<>(lotto).size());
    }

    @DisplayName("생성된 숫자 리스트가 정렬되어 있는지 검사")
    @Test
    void testLottoSorted() {
        List<Integer> sortedLotto = new ArrayList<>(lotto);
        Collections.sort(sortedLotto);
        Assertions.assertEquals(sortedLotto, lotto);
    }

    @DisplayName("생성된 숫자들이 지정된 범위 내에 있는지 검사")
    @Test
    void testLottoRange() {
        for (int number : lotto) {
            Assertions.assertTrue(number >= LOTTO_MIN_NUMBER && number <= LOTTO_MAX_NUMBER);
        }
    }
}

 

로또 제네레이터의 역할을 잘 수행했는지 검사하는 코드이므로 4가지를 검사했다.

  1. 로또 번호가 6개가 잘 생성되었는지
  2. 리스트 내에 중복이 없는지
  3. 오름차순 정렬이 되어있는지
  4. 1에서 45사이의 값으로만 구성되었는지

이건 pickUniqueNumberInRange함수가 있어서 쉽게 구현했다.


public class LottoGenerator {
    private final static int LOTTO_MIN_NUMBER = 1;
    private final static int LOTTO_MAX_NUMBER = 45;
    private final static int LOTTO_NUMBER_COUNT = 6;

    public static List<Integer> generate() {
        List<Integer> numbers = new ArrayList<>(Randoms.pickUniqueNumbersInRange
                (LOTTO_MIN_NUMBER, LOTTO_MAX_NUMBER, LOTTO_NUMBER_COUNT));
        Collections.sort(numbers);
        return numbers;
    }
}

5. LottoResult - 로또 당첨 판정

다음은 당첨 처리에 관한 테스트 코드다.

 

JAVA ENUM을 사용하여 Rank를 구현하는 것을 생각하고

 

당첨 번호와 대조 결과를 Enum으로 리턴하기로 했다.

public class LottoResultTest {
    LottoResult winngingLotto = new LottoResult(new Lotto(List.of(1, 9, 16, 19, 33, 42)), 5);

    @DisplayName("1등")
    @Test
    void lottoFirst() {
        Assertions.assertEquals(Rank.FIRST,
                winngingLotto.compareLotto(new Lotto(List.of(1, 9, 16, 19, 33, 42))));
    }

    @DisplayName("2등")
    @Test
    void lottoSecond() {
        Assertions.assertEquals(Rank.SECOND,
                winngingLotto.compareLotto(new Lotto(List.of(1, 5, 9, 16, 19, 33))));
    }

    @DisplayName("3등")
    @Test
    void lottoThird() {
        Assertions.assertEquals(Rank.THIRD,
                winngingLotto.compareLotto(new Lotto(List.of(1, 9, 16, 19, 33, 44))));
    }

    @DisplayName("4등")
    @Test
    void lottoFourth() {
        Assertions.assertEquals(Rank.FOURTH,
                winngingLotto.compareLotto(new Lotto(List.of(1, 9, 16, 19, 35, 44))));
    }

    @DisplayName("5등")
    @Test
    void lottoFifth() {
        Assertions.assertEquals(Rank.FIFTH,
                winngingLotto.compareLotto(new Lotto(List.of(1, 9, 16, 22, 35, 44))));
    }

    @DisplayName("미당첨")
    @Test
    void lottoMiss() {
        //2개
        Assertions.assertEquals(Rank.MISS,
                winngingLotto.compareLotto(new Lotto(List.of(1, 3, 5, 9, 10, 11))));
        //1개
        Assertions.assertEquals(Rank.MISS,
                winngingLotto.compareLotto(new Lotto(List.of(1, 5, 7, 8, 10, 11))));
        //0개
        Assertions.assertEquals(Rank.MISS,
                winngingLotto.compareLotto(new Lotto(List.of(2, 4, 6, 8, 10, 12))));
    }
}

Enum으로 한 이유는 여러 값을 하나로 묶을 수 있다는 점을 사용하기 위해서였다.

 

물론 클래스도 String, int등을 선언해놓고 getter를 통해 호출하는 것도 가능하겠지만

 

클래스는 인스턴스롤 생성해야하고, 각 인스턴스를 어떻게 관리할 것인가? 에 대한 의문도 생겼다.

 

그리고 사실 우리가 진짜 원하는건 Rank를 판정한 뒤 랭크의 개수가 몇 개인지가 필요한거지

 

랭크 인스턴스 자체가 필요한게 아니었으니까.

 

여러모로 손해라고 생각해 Enum을 각 등수에 관련된 정보를 묶기로 했다.

 

먼저 당첨 결과를 담당하는 클래스를 작성했다.


public class LottoResult {
    private final Lotto winningLotto;
    private final int bonus;

    public LottoResult(Lotto winningLotto, int bonus) {
        this.winningLotto = winningLotto;
        this.bonus = bonus;
    }

    public Rank compareLotto(Lotto userLotto) {
    	//사용자 로또 번호
        List<Integer> userLottoNumbers = userLotto.getLottoNumbers();
        // 당첨 로또 번호
        List<Integer> winningLottoNumbers = winningLotto.getLottoNumbers();
        //사용자 로또 번호가 당첨 로또 번호에 몇개나 포함되었는지 저장
        int matchCount = (int) userLottoNumbers.stream()
                .filter(winningLottoNumbers::contains)
                .count();
        //보너스 번호가 포함되었는지 저장
        boolean isBonus = userLottoNumbers.contains(bonus);
        //Enum의 searchRank 메서드로 카운트 값과 보너스가 포함되었는지 전달
        return Rank.searchRank(matchCount, isBonus);
    }
}

 

LottoResult 클래스는 인스턴스가 생성되면 당첨 로또 번호와, 보너스 번호를 저장한다.

 

그리고 해당 인스턴스의 메서드를 호출하면 당첨 번호와 사용자가 입력한 번호를 비교하는 역할을 한다.

 

이렇게 저장된 겹치는 번호 개수와, 보너스가 포함되었는지 결과를 searchRank로 전달한다.

 


public enum Rank {
    /*
     * FIRST ((6, false), 2_000_000_000) - 6개 일치
     * SECOND ((5, true), 30_000_000) - 5개 일치 && 보너스 볼 일치
     * THIRD ((5, false), 1_500_000) - 5개 일치
     * FOURTH ((4, false), 50_000) - 4개 일치
     * FIFTH ((3, false), 5_000) - 3개 일치
     * MISS ((0, false), 0) - 당첨 실패
     */
    FIRST((matchCount, bonus) -> matchCount == 6,
            2_000_000_000,
            "6개 일치"),
    SECOND((matchCount, bonus) -> matchCount == 5 && bonus,
            30_000_000,
            "5개 일치, 보너스 볼 일치"),
    THIRD((matchCount, bonus) -> matchCount == 5 && !bonus,
            1_500_000,
            "5개 일치"),
    FOURTH((matchCount, bonus) -> matchCount == 4,
            50_000,
            "4개 일치"),
    FIFTH((matchCount, bonus) -> matchCount == 3,
            5_000,
            "3개 일치"),
    MISS((matchCount, bonus) -> matchCount < 3,
            0,
            "2개 일치");

    private final BiPredicate<Integer, Boolean> condition;
    private final int winningAmount;
    private final String message;

    Rank(BiPredicate<Integer, Boolean> condition, int winningAmount, String message) {
        this.condition = condition;
        this.winningAmount = winningAmount;
        this.message = message;
    }

    public static Rank searchRank(int matchCount, boolean isBonus) {
        return Arrays.stream(Rank.values())
                .filter(rank -> rank.condition.test(matchCount, isBonus))
                .findAny()
                .orElseThrow(IllegalArgumentException::new);
    }

    public int getWinningAmount() {
        return this.winningAmount;
    }

    public String getMessage(){
        return this.message;
    }
}

 

searchRank에서 받은 인자를가지고 열거형 리스트에 저장된 Predicate를 사용해 조건을 검사한다.

 

각 열거형에는 (matchCount, bonus) -> matchCount == 6 같은 함수형 조건문이 저장되어 있는데

 

이 결과 값이 참이 되면, 해당 랭크를 반환하게 된다.

 

자세한건 Predicate를 검색해보자. BiPredicate는 인자를 두가지 이상 사용할 때 사용한다.

 

Rank 구성

condition(조건문)

Predicate<T>는 하나의 인자를 받아 boolean 값을 반환하는 함수를 표현한다.

 

여기서 T는 입력되는 인자의 타입이다. 그건 Rank, String, Integer 등등이 될 수 있다.

예시)

Predicate<String> lengthIsGreaterThan5 = str -> str.length() > 5;
boolean result = lengthIsGreaterThan5.test("Hello, World!");

 

이 경우 result는 true가 된다.


BiPredicate는 인자를 두 개 이상 받을 때 사용된다.

BiPredicate<Integer, Integer> areBothEven = (num1, num2) -> num1 % 2 == 0 && num2 % 2 == 0;
boolean result = areBothEven.test(4, 8);

 

 

이 경우 result는 true가 된다.

이걸 Enum에 저장해서 활용했다.

winningAmount(당첨금)

그냥 해당 등수에 대한 당첨금이다. 나중에 출력할 때 사용하려고 저장했다.

message(출력용 메시지)

사실 이것도 나중에 조건문 여러개 써주기 귀찮아서 사용했다.

if (rank.equals(Rank.FIRST)){}

if (rank.equals(Rank.SECOND)){}

 

같은 꼴로 출력해주는 것보다 for문으로 객체 자체에서 출력해주는게 편하니까.

6. LottoController - 로또 프로그램 컨트롤러

좀 기니까 하나씩 설명해보자

 

6 - 1. run메서드


public void run() {
    UserAmount userAmount;
    List<Lotto> lottoList;
    Lotto winningNumbers;
    int bonus;

    userAmount = new UserAmount(InputView.inputAmount());
    OutputView.printLottoCount(userAmount.lottoCount());

    lottoList = createRandomLotto(userAmount.lottoCount());
    OutputView.printLottoList(lottoList);

    winningNumbers = new Lotto(InputView.inputWinningNumbers());
    bonus = InputView.inputBonusNumber();
    validateBonus(winningNumbers, bonus);
    lottoResult(lottoList, new LottoResult(winningNumbers, bonus), userAmount);
}

 

앱이 시작되면 전반적인 세팅을 시작한다.

 

여기선 모든 입력을 처리하고 걸 맞는 출력을 해준다.

 

입력 세팅이 끝났다면 결과를 연산할 수 있도록 lottoResult 메서드로 보내주게 된다.

 

이때 입력은 구입 금액 에러 및 계산처리, 랜덤 로또 리스트 생성, 당첨 로또 입력 후 저장을 수행한다.

private void lottoResult(List<Lotto> lottoList,
                         LottoResult winningLotto,
                         UserAmount userAmount) {
    Map<Rank, Integer> result = setResult();
    for (Lotto currentLotto : lottoList) {
        Rank rank = winningLotto.compareLotto(currentLotto);
        result.put(rank, result.get(rank) + 1);
    }
    OutputView.printResult(result);
    OutputView.printRateReturn(calculateRateReturn(userAmount, result));
}

 

lottoResult에선 Map에 각 Rank를 Key값으로 저장해준다.

private Map<Rank, Integer> setResult() {
    Map<Rank, Integer> result = new LinkedHashMap<>();
    Rank[] rank = Rank.values();
    for (int i = rank.length - 1; i >= 0; i--) {
        result.put(rank[i], 0);
    }
    return result;
}

 

이 때 출력 결과는 내가 만든 Rank 순서의 역순이라 끝에서 시작 부분으로 저장해준다.

 

result에 그러면 다음과 같은 구조로 저장이 된다. [K : V]

[MISS : 0]

[FIFTH : 0]

[FOURTH : 0]

(...생략...)

 

그리고 다시 위의 메서드로 돌아와서 랜덤 생성된 lottoList를 모두 대조한 뒤

 

return 된 Rank값을 Map에서 찾아 value값을 +1 해준다.

 

당첨된 로또가 각 몇개인지 카운팅 된다.

 

해당 result값을 가지고 출력 결과를 만들어주면 된다.

 

출력 메서드다.

 

public static void printResult(Map<Rank, Integer> result) {
    System.out.println("당첨 통계");
    System.out.println("---");
    result.forEach(OutputView::printLottoResult);
}

private static void printLottoResult(Rank key, int value) {
    if (!key.equals(Rank.MISS)){
        NumberFormat numberFormat = NumberFormat.getInstance();
        System.out.println(MessageFormat.format(WINNING_COUNT,
                key.getMessage(),
                numberFormat.format(key.getWinningAmount()),
                value));
    }
}

 

수익률 계산 메서드다.

private double calculateRateReturn(UserAmount userAmount,
                                   Map<Rank, Integer> result) {
    int sum = 0;
    for (Map.Entry<Rank, Integer> m : result.entrySet()) {
        Rank key = m.getKey();
        int value = result.get(key);
        sum += key.getWinningAmount() * value;
    }
    return (((double)sum / (double) (userAmount.lottoCount() * LOTTO_PRICE)) * PERCENTAGE);
}

 

수익률 계산 출력 메서드다.

public static void printRateReturn(double rate) {
    System.out.println(MessageFormat.format(RATE_RETURN,
            String.format("%.1f", rate)));
}

 

출력할 때 MessageFormat이나 NumberFormat이란걸 사용했는데.

 

NumberFormat은 숫자에 자릿수에 ,를 추가해주는 기능을 갖고있다.

 

MessageFormat은 C의 printf같은 문법으로 가변인자를 사용하여 출력을 담당해준다.

 

그래서 출력 상수 값을 다음과 같이 지정해줄 수 있었다.

private static final String LOTTO_COUNT = "개를 구매했습니다.";
private static final String WINNING_COUNT = "{0} ({1}원) - {2}개";
private static final String RATE_RETURN = "총 수익률은 {0}%입니다.";

 

마무리

사실 이번 과제는 쓸때없이 품을 많이 들인 것 같긴 하다.

 

개발자로써 실력을 탄탄히 하기에 꽤 좋은 과제인 것 같다.

 

만약 내가 나중에 누군가 우테코 과정을 수료했다 그러면 그 사람에 대한 굉장히 좋은 인식을 가질 수 있을 것 같다.