우테코 프리코스

프리코스 3주차 미션

흰곰돌이 2023. 11. 7. 01:44

어느덧 절반을 넘어선 3주차 미션에 도달했다. 

저번과 마찬가지로 코드리뷰를 진행했는데 이번 코드는 저번 코드에 비해 많은 피드백이 들어왔고, 대부분의 피드백을 읽어보니 직접 코드에 다시 적용해보는게 더 좋을거 같았다. 그래서 새로운 미션이 이메일로 주어졌지만 내 코드에 대한 많은 분들의 수많은 피드백을 하나하나 코드에 적용해보는 시간을 먼저 가졌다. 아래는 프리코스 2주차 미션 코드와는 약간 다른, 이후 받은 피드백을 적용한 코드와 게시글이다.

 

https://github.com/JaeMin-1/java-racingcar-6/tree/study

 

GitHub - JaeMin-1/java-racingcar-6

Contribute to JaeMin-1/java-racingcar-6 development by creating an account on GitHub.

github.com

https://golden-retriever.tistory.com/8

 

2주차 코드에 대한 코드 리뷰

감사하게도 정말 많은 분들이 다양하게 코드 리뷰를 해주셨다. 이에 대해 일부 정리를 해보려한다. public String showExecutionResults(ArrayList carNames, ArrayList carGoingCount) { StringBuilder result = new StringBuilder()

golden-retriever.tistory.com

이제 이번 3주차 미션으로 돌아와서, 이번 미션은 로또 구현이었다. 기능 요구 사항은 다음과 같다.

 

 

기능 요구 사항에는 로또에 대한 설명이 담겨있었는데 읽다가 아래의 부분이 눈에 띄었다.

 

 

1, 2주차 미션은 예외 처리 후 프로그램을 종료하는 방식이었는데 3주차 미션은 예외 처리 후에도 프로그램은 작동하고 예외 처리한 입력 부분부터 다시 입력받는 새로운 방식이 도입되었다.

 

그리고 프로그래밍 요구 사항에서 아래와 같이 추가된 부분이 있었다.

 

그전부터 함수가 한가지 일만 잘하도록 구현하라고 말씀하셨지만 이번에는 길이를 명시해주셨다. 그리고 객체지향 생활체조 원칙에서도 언급되는 else 표현 쓰지 않기에 대한 조건이 있었고 2주차 코드리뷰 피드백에서도 있었던 enum 클래스 적용이 있었다. 마지막으로 모델, 즉 도메인 로직 단위 테스트 구현, UI 로직은 제외라는 조건이 있었다.

 

피드백에서 적용해본 Enum 클래스는 한 번 해봤기에 괜찮을 것 같다고 생각했고 나머지는 전 주차 미션에서도 어느정도 지키는 것들이기에 역시 큰 어려움은 없을 것으로 생각됐다.

 

코드 회고

구현하기 전에는 그냥 로또 구현만 하면 되니까 큰 어려움은 없겠구나 라고 생각했는데 예외 처리후 재입력을 받아야 하는 부분부터 꼬이기 시작했다. 그냥 예외 처리 후 종료하는 경우에 익숙해져있어서 방법이 쉽게 생각나지 않았고 검색 후 while문과 함께 try-catch 문을 써야함을 깨달았다. 

 

public static int getPurchaseAmount() {
    while (true) {
        try {
            String input = inputPurchaseAmount();
            return validateAllPurchase(input);
        } catch (IllegalArgumentException e) {
            System.out.println(e.getMessage());
        }
    }
}

 

위 코드의 try 부문에서 만약 IllegalArgumentException 이 발생하게 되면 catch 부문으로 넘어가서 해당하는 에러 메세지를 출력하고 다시 try 부문으로 가서 재입력을 받게된다.

 

그리고 아까 프로그래밍 요구 사항에서 언급하지 못했는데 아래와 같은 추가된 조건이 더 있었다.

 

이렇게 Lotto 클래스가 기본 코드에 같이 주어졌다.

인스턴스 변수를 추가할 수 없다는 것은 주어진 numbers 단 하나만 사용해야함을 뜻하고 이는 일급 컬렉션으로 코드가 작성되었음을 뜻한다. 따라서 이번 미션에서 일급 컬렉션을 적용해보게 되었다. 당첨 번호를 관리하는 Lotto 클래스 외에도 입력 금액을 관리하는 Purchase 클래스, 보너스 번호를 관리하는 Bonus 클래스를 일급 컬렉션으로 작성했다. 모두 정확히 하나의 인스턴스 변수만을 사용하며 객체를 만들때 유효성 검사를 진행한다.

자세히 설명하자면, 입력값 자체가 올바른지에 대해서는(int 유형의 값이 잘 들어왔는지) InputValidation에서 검증을 했고, 그 입력값이 로또 프로그램에서 올바르게 쓰일 수 있는지는 Lotto, Purchase, Bonus 에서 유효성 검사 후 객체를 생성했다.

아래 코드를 한 번 보자. InputValidation 클래스의 일부이다. 

 

public class InputValidation {
    public static int validatePurchaseAmount(String input) {
        try {
            int purchaseAmount = Integer.parseInt(input);
            return purchaseAmount;
        } catch (NumberFormatException e) {
            throw new IllegalArgumentException(ErrorMessage.INVALID_NON_NUMERIC_PURCHASE.getMessage());
        }
    }
    ...
}

 

정말 그냥 숫자가 입력이 됐는지 안됐는지만 검증한다. 추후 이 숫자가 로또 프로그램에서 적합한 숫자인지는 아래의 코드에서 검증한다. 아래의 코드는 일급 컬렉션으로 작성한 Purchase 클래스다.

 

package lotto.model;

import lotto.constants.ErrorMessage;

public class Purchase {
    private final int money;

    public Purchase(int money) {
        validateZerOrNegative(money);
        validateAmountNotDivisible(money);
        this.money = money;
    }

    private static void validateZerOrNegative(int money) {
        if (money <= 0) {
            throw new IllegalArgumentException(ErrorMessage.INVALID_ZERO_OR_NEGATIVE_PURCHASE.getMessage());
        }
    }

    private static void validateAmountNotDivisible(int money) {
        if (money % 1000 != 0) {
            throw new IllegalArgumentException(ErrorMessage.INVALID_AMOUNT_NOT_DIVISIBLE.getMessage());
        }
    }

    public int getPurchase() {
        return money;
    }
}

 

구입 금액이므로 돈으로서 양수인지, 그리고 로또 하나당 천원에 맞춰서 딱 맞아떨어지는 금액을 입력했는지에 대한 검사를 실시한다. 이 검사들을 모두 통과하면 비로소 객체가 생성된다. 즉, 구입 금액이 필요한 모든 로직은 이 일급 컬렉션을 통해서 객체를 만들면 된다. 일급 컬렉션으로 구입 금액을 관리함으로써 정해놓은 조건에 만족해야만 생성할 수 있는 자료구조가 되었고 이는 이후 발생할 문제를 최소화하며 추후 관리 역시 수월하다. 이외에도 많은 장점이 있는데 이에 대한건 아래의 글을 참고하자.

 

https://jojoldu.tistory.com/412

 

일급 컬렉션 (First Class Collection)의 소개와 써야할 이유

최근 클린코드 & TDD 강의의 리뷰어로 참가하면서 많은 분들이 공통적으로 어려워 하는 개념 한가지를 발견하게 되었습니다. 바로 일급 컬렉션인데요. 왜 객체지향적으로, 리팩토링하기 쉬운 코

jojoldu.tistory.com

 

한편, 위와 같이 리펙토링 하기 전에 기능 구현만 다 하고나서 제공되는 테스트를 실행했는데 아래와 같은 오류가 발생했다. 

 

List<Integer> oneLotto = Randoms.pickUniqueNumbersInRange(1, 45, 6);
Collections.sort(oneLotto);

 

구현 단계 코드 부분에서 무작위로 6 자리의 숫자 리스트를 생성 후 오름차순 정렬을 해줬다. 테스트 코드에서는 무작위 생성이 아닌, List.of()를 사용해서 값을 고정했는데 이렇게 값을 고정하면 Randoms.pickUniqueNumbersInRange(1, 45, 6); 이 실행될 때 List.of()로 고정해놓은 값을 가져온다. 이때 List.of()를 이용해 만든 리스트는 완전 불변이기에 수정을 가하는 메서드는 모두 안된다. 그런데 List.of()를 이용해 만든 list에서 Collections.sort를 통해 값을 변경하려고 했기 때문에 UnsupportOperationException 오류가 발생한 것이다. 이와 같은 문제를 해결하기 위해 리스트를 복사하는 방법을 택했다. 반환된 리스트를 수정하지 않고 사용하기 위해, 해당 리스트를 복사한 후 수정하거나 사용하는 방식이다. 이렇게 하면 원본 리스트가 변경되지 않으므로 오류를 피할 수 있다. 아래는 수정된 코드다.

 

List<Integer> oneLotto = new ArrayList<>(Randoms.pickUniqueNumbersInRange(1, 45, 6));
Collections.sort(oneLotto);

 

 

저번 주 미션에 대해서 enum 클래스를 추후 적용해봤지만 상수 관리 용도로만 사용했고 코드 리펙토링에 있어서 이름이 바뀌는거 외엔 변화를 주진 않았다. 하지만 이번에는 enum 클래스에서 제공하는 메서드들을 통해서 코드 리펙토링을 진행했다. 먼저 아래는 해당 enum 클래스다.

 

package lotto.constants;

public enum ResultMessage {
    THREE_MATCH(3, 5000, "3개 일치 (5,000원) - %d개"),
    FOUR_MATCH(4, 50000, "4개 일치 (50,000원) - %d개"),
    FIVE_MATCH(5, 1500000, "5개 일치 (1,500,000원) - %d개"),
    FIVE_MATCH_BONUS(5, 30000000, "5개 일치, 보너스 볼 일치 (30,000,000원) - %d개"),
    SIX_MATCH(6, 2000000000, "6개 일치 (2,000,000,000원) - %d개");

    private final int countMatch;
    private final int winningAmount;
    private final String message;

    ResultMessage(int countMatch, int winningAmount, String message) {
        this.countMatch = countMatch;
        this.winningAmount = winningAmount;
        this.message = message;
    }

    public int getCountMatch() {
        return countMatch;
    }

    public int getWinningAmount() {
        return winningAmount;
    }

    public String getMessage() {
        return message;
    }
}

 

아래는 리펙토링 하기 전 코드다. 비슷한 코드가 계속 반복되고 있다.

 

private double calculateSumPrize() {
    double sumPrize = 0;
    sumPrize += ResultMessage.THREE_MATCH.getWinningAmount() * result.get(0);
    sumPrize += ResultMessage.FOUR_MATCH.getWinningAmount() * result.get(1);
    sumPrize += ResultMessage.FIVE_MATCH.getWinningAmount() * result.get(2);
    sumPrize += ResultMessage.FIVE_MATCH_BONUS.getWinningAmount() * result.get(3);
    sumPrize += ResultMessage.SIX_MATCH.getWinningAmount() * result.get(4);
    return sumPrize;
}

 

아래는 enum 클래스를 활용해 수정한 코드다. 훨씬 깔끔해졌다.

 

private double calculateSumPrize() {
    double sumPrize = 0;
    for (ResultMessage resultMessage : ResultMessage.values()) {
        int winningAmount = resultMessage.getWinningAmount();
        int index = resultMessage.ordinal();
        sumPrize += winningAmount * result.get(index);
    }
    return sumPrize;
}

 

values() 메서드를 통해 enum 의 모든 상수를 배열로 반환해서 for문을 통해 하나씩 접근한다. 기존과 같이 getWinningAmount() 를 통해 상금을 저장하고 ResultMessage 배열에서의 상금 순서와 result 리스트에서의 공이 겹치는 개수의 순서가 일치함을 고려하여 ordinal() 메서드로 ResultMessage 배열의 인덱스에 접근한다. 이 인덱스는 곧 result 리스트에서의 인덱스와 일치하므로 공통적으로 사용할 수 있다. 

전체 코드

https://github.com/JaeMin-1/java-lotto-6/tree/JaeMin-1

 

GitHub - JaeMin-1/java-lotto-6

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

github.com

 

마치며

이번 미션은 예외 처리할 게 많았고 새로운 일급 컬렉션을 도입하다보니 리펙토링 과정에서 굉장히 헷갈리고 힘들었다. 하나로 모여있던 코드들을 시행착오를 겪으며 긴 시간동안 리펙토링을 했고 처음에만 해도 어디서부터 어떻게 해야할지 고민이 많았지만, 다 했을 때는 나름 뿌듯했다. 처음 작성해본 일급 컬렉션에 대해서는 일급 컬렉션을 도입함으로써 코드가 더 깔끔해지고 가독성이 올라감을 느꼈고, 테스트 코드를 작성할때도 코드가 더 간결해지는 것을 느꼈다. 또한 테스트 코드의 중요성을 깨달았다. 당첨 번호와 숫자가 3개 겹치는 경우부터 모두 겹치는 경우까지 세서 리스트에 저장하는 부분이 있는데, 테스트 코드 작성 전에는 직접 프로그램을 실행해서 몇 가지 경우를 대입해보고 맞았다고 생각했었는데 테스트 코드에서 좀 더 디테일한 경우를 작성하고 테스트를 하니 기대값과 다르게 나왔다. 이를 통해 내가 잘못 구현했음을 깨달았고 수정 후 비로소 테스트 코드를 통과할 수 있었다. 테스트 코드의 도움을 받으니 정말 필요하다는게 체감이 되는 순간이었다. 이번 과제는 혹시 모를 최종 코딩 테스트에 대비해서 처음으로 4~5시간 안에 해보려고 했지만 턱없이 부족한 시간이라는 것을 깨달았다. 간단한 로또 마저도 이렇게 오래걸리는데 과연 최종 코딩 테스트를 보게 되더라도 시간 안에 완수할 수나 있을지 생각해보며 나에 대한 아쉬움이 들었다. 그래도 분명한건 성장하고 있다는 사실이다. 과거의 나였다면 일급 컬렉션이라는 단어 자체도 몰랐을 것이다. 또한 학교에서 수강 중인 소프트웨어 아키텍처 수업에서 곧 수행해야할 프로젝트가 있는데 소식을 듣자마자 우테코 프리코스가 떠올랐다. 우테코 프리코스 기간동안 기능별로, 클래스별로 나누는 리펙토링 연습이 설계도, 다이어그램을 작성 후 코드를 구현하는 프로젝트와 잘 맞아떨어져서 큰 도움이 될 것으로 생각된다. 현재 느리지만 꾸준히 성장하고 있다. 기분탓인지, 아니면 더 쉬운 오류인지는 모르겠지만 오류를 해결하는 속도와 오류에 대한 이해도도 저번 주에 비해 더 나아졌다는 생각이 든다. 이렇게 사소한 부분이라도 마음속으로 스스로 잘하고 있다고 되뇌이며 마지막 미션인 4주차 미션을 잘 해낼 수 있다는 다짐과 함께 기다리겠다.