개발자는 기록이 답이다

우테코 6기 프리코스 3주차 회고 본문

우아한테크코스

우테코 6기 프리코스 3주차 회고

slow-walker 2023. 11. 15. 12:14

 

벌써 3주차가 다가왔습니다! 이번 미션은 지난번 1,2주차 미션들보다 훨씬 저한테 더 어렵게 느껴졌는데요


객체 분리를 맞게 한건지 잘 모르겠을 정도로 정신이 없었습니다.😂

왜냐하면 view단에서 입력을 받고 출력하는 부분이랑 도메인 로직을 연결하는 부분에서 어떻게 객체를 분리해야할지 애매하게 느껴졌기 때문입니다.

 

게다가 로또 테스트 부분에서 에러관련된 실패 테스트 코드를 만들어놨던게 분명히 작동되는걸 확인했었는데, 마감 1시간전에 갑자기 안되는걸을 확인해서 급하게 삭재했습니다. 마지막 테스트로 ./gradlew clean test를 안해봤으면 큰일날뻔했습니다.

 

이번 미션을 하면서 느꼈던 점 중 가장 큰 고민거리는, 과연 이걸 내가 최종 코딩테스트때 5시간안에 끝낼 수 있을까 걱정하게 되는 계기가 되었습니다. 성장은 하고 있지만, 역시 최종 합격은 더 잘하시는 분들이 될것같다는 마음에 위축되버렸습니다 하하

 

3주차 로또 PR링크 

 

로또

 

 

🚀 기능 요구 사항

 

로또 게임 기능을 구현해야 한다. 로또 게임은 아래와 같은 규칙으로 진행된다.

 

  • 로또 번호의 숫자 범위는 1~45까지이다.
  • 1개의 로또를 발행할 때 중복되지 않는 6개의 숫자를 뽑는다.
  •  당첨 번호 추첨 시 중복되지 않는 숫자 6개와 보너스 번호 1개를 뽑는다.
  • 당첨은 1등부터 5등까지 있다. 당첨 기준과 금액은 아래와 같다.
    • 1등: 6개 번호 일치 / 2,000,000,000원
    • 2등: 5개 번호 + 보너스 번호 일치 / 30,000,000원
    • 3등: 5개 번호 일치 / 1,500,000원
    • 4등: 4개 번호 일치 / 50,000원
    • 5등: 3개 번호 일치 / 5,000원
구입금액을 입력해 주세요.
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 예약어를 쓰지 않는다.
    • 힌트: if 조건절에서 값을 return하는 방식으로 구현하면 else를 사용하지 않아도 된다.
    • else를 쓰지 말라고 하니 switch/case로 구현하는 경우가 있는데 switch/case도 허용하지 않는다.
  • Java Enum을 적용한다.
  • 도메인 로직에 단위 테스트를 구현해야 한다. 단, UI(System.out, System.in, Scanner) 로직은 제외한다.
    • 핵심 로직을 구현하는 코드와 UI를 담당하는 로직을 분리해 구현한다.
    • 단위 테스트 작성이 익숙하지 않다면 test/java/lotto/LottoTest를 참고하여 학습한 후 테스트를 구현한다.

디렉토리 구조

 

src                                        
├─ main                                    
│  └─ java                                 
│     └─ lotto                             
│        ├─ controller                     
│        │  ├─ InitDto.java                
│        │  └─ LottoController.java        
│        ├─ domain                         
│        │  ├─ Buyer.java                  
│        │  ├─ GenerateRandomNum.java      
│        │  ├─ Lotto.java                  
│        │  ├─ Prize.java                  
│        │  └─ Winning.java                
│        ├─ utils                          
│        │  ├─ Constants.java              
│        │  └─ ErrorMessages.java          
│        ├─ view                           
│        │  ├─ InputValidator.java         
│        │  ├─ InputView.java              
│        │  └─ OutputView.java             
│        └─ Application.java               
└─ test                                    
   └─ java                                 
      └─ lotto                             
         ├─ domain                         
         │  ├─ BuyerTest.java              
         │  ├─ GenerateRandomNumTest.java  
         │  ├─ LottoTest.java              
         │  └─ PrizeTest.java              
         ├─ view                           
         │  ├─ InputValidatorTest.java     
         │  └─ InputViewTest.java          
         └─ ApplicationTest.java

 

 

이번에는 기능목록 리스트를 제대로 작성하지 못해서 아쉬움이 남아있습니다.

내시경하고 나서 컨디션이 안좋아서 기능 구현에 좀 더 집중하는데 힘썼던것 같습니다.

 

이번에 어렵게 느껴졌던 부분은 input에서 에러가 날 경우 다시 입력받을 수 있도록 하거나, enum을 사용해서 로또의 등수대로 통계를 내는 부분이 어렵게 느껴졌습니다!

 

 

1. 예외 종류에 따른 try catch

 

이전에는 메소드 분리한 곳에 throw new 해서 익셉션을 던져주면 되었는데, 이번에는 해당 메소드를 호출한 곳에서 try catch로 잡아서 에러메세지를 출력해줘야했습니다. 입력 관련해서 사용한 익셉션도 NumberFormatException과 IllegalArgumentException 2개였는데, catch부분에서 NumberFormatException(하위클래스)를 IllegalArgumentException(상위클래스)보다 먼저 사용해야지 해당 익셉션을 잡을 수 있다고 생각했습니다.

 

그렇게 생각한 이유는 '자바의 신' 으로 공부했을 때 예외는 총 3가지가 존재하는데, 그중 에서 런타임 예외는 계층구조로 상관관계가 존재한다고 배웠기 때문입니다.

 

- checked exception

- error

- runtime exception / unchecked exception

이렇게 계층구조로 있다면 분리된 메소드에서 어떤 익셉션을 잡는지 확실하게 하기 위해, 제일 하위 클래스에 있는 걸 먼저 catch해야 한다고 생각했습니다.

    public static Lotto inputWinningNum() {
        while (true) {
            String input = getWinningNumbersFromUser();
            try {
                return createLottoFromInput(input);
            } catch (NumberFormatException e) {
                System.out.println(e.getMessage());
            } catch (IllegalArgumentException e) {
                System.out.println(e.getMessage());
            }
        }
    }

 

 

 

 

그러나 NumberFormatException이 IllegalArgumentException을 상속하고 있습니다. 따라서 상위 클래스에서 하위 클래스로 예외를 처리할 수 있다는 것을 알게 되었습니다. 이미 IllegalArgumentException가 이미 NumberFormatException을 포함하고 있기 때문입니다.


따라서, 예외가 발생할 경우 IllegalArgumentException이 먼저 처리되어, NumberFormatException이 발생하더라도 이미 해당 예외를 처리하는 catch 블록에서 처리가 이루어지게 됩니다. 이렇게 하면 코드가 불필요하게 중복되지 않고 더 간결해지며, 관련된 예외가 혼동되지 않고 적절하게 처리될 수 있습니다.

    public static Lotto inputWinningNum() {
        while (true) {
            String input = getWinningNumbersFromUser();
            try {
                return createLottoFromInput(input);
            } catch (IllegalArgumentException e) {
                System.out.println(e.getMessage());
            }
        }
    }

 

 

2. 입력이 잘못되었을 경우 [ERROR] 메세지를 출력하고 다시 입력하게 한다.

 

위와 동일한 코드인데, 좀 더 코드 전반적인 내용을 가져왔습니다.

public class LottoController {
    private InitDto initDto;
    private final OutputView outputView = new OutputView();
    public void start() {
        Buyer buyer = InputView.payForLottery();
        Winning winning = InputView.inputBonusNum(InputView.inputWinningNum());
        initDto = new InitDto(buyer, winning);
    }

    public void running() {
        outputView.getStatistic(initDto.getBuyer(), initDto.getWinning());
    }
...
}

public class InputView {

    public static Buyer payForLottery() {
        while (true) { // --> 다시 입력받게 하기 위한 코드
            try {
                int paymentNumber = getPaymentNumber();
                int ticketCount = calculateTicketCount(paymentNumber);
                Buyer buyer = new Buyer(paymentNumber, ticketCount);
                printTickets(buyer);
                return buyer;
            } catch (IllegalArgumentException e) {
                System.out.println(e.getMessage());
            }
        }
    }

    public static Lotto inputWinningNum() {
        while (true) { // --> 다시 입력받게 하기 위한 코드
            String input = getWinningNumbersFromUser();
            try {
                return createLottoFromInput(input);
            } catch (IllegalArgumentException e) {
                System.out.println(e.getMessage());
            }
        }
    }
...
}

 

 

해당 코드는 getWinningNumbersFromUser()메서드에서 사용자로부터 로또 번호를 입력받는 상호작용을 담당하고 있습니다. while 루프를 통해 사용자가 올바른 입력을 할 때까지 계속해서 입력을 받습니다. 주요 포인트는 입력이 올바르지 않을 경우 발생할 수 있는 IllegalArgumentException을 적절히 처리하는 것입니다.

 

그래서 while (true) 루프를 통해 에러문이 출력될경우 다시 입력받고, return 문이 실행되기 전까지는 계속해서 반복됩니다.

저는 while(true)문을 나오기 위해서는 break만 생각해봤는데, return문을 통해서도 무한 루프를 벗어날 수 있다는것을 다시 한번 깨닫게 되었습니다.

 

while문을 사용하지 않는 다른 방법은 코드 리뷰를 통해 알게 되었는데, 에러메세지에 Enum을 활용하여 에러타입을 지정해서 reRun()함수를 만들어주는 것입니다.

 

제가 작성한 코드는 모듈이 다른 모듈에 의존하는식으로 결합도가 강한 느낌인데, 코드 리뷰를 통해 확인한 다른 분의 코드는 예외 처리와 사용자 인터페이스를 효과적으로 분리하고, 각 단계를 명확하게 나누어 가독성을 높이는 좋은 구조를 가지고 있는것 같습니다.

public class Game {
   
   public void run(){

        try{

            purchaseLotto();
            saveWinningNumbers();
            saveBonusNumbers();

            printPurchaseHistory();
            printWinningInfo();
            printYieldRate();

        }catch (Exception e){

            String errorType = e.getMessage();
            System.out.println(Message.valueOf(errorType).getMessage());

            reRun(errorType);
        }
    }
    
	private void reRun(String errorType){

        if(errorType.contains("MONEY")) {

            purchaseLotto();
        }

        if(errorType.contains("WINNING")) {

            saveWinningNumbers();
        }

        if(errorType.contains("BONUS")) {

            saveBonusNumbers();
        }
    }
    
    private void saveWinningNumbers() {

        System.out.println(Message.WINNING_NUMBER_MESSAGE.getMessage());
        String input = Console.readLine();

        gameController.saveWinningNumbers(input);
    }
}

public enum Message {

    UNIT_MONEY_ERROR_MESSAGE("[ERROR] 구입 금액이 1000원 단위가 아닙니다."),
    MINIMUM_MONEY_ERROR_MESSAGE("[ERROR] 최소 구입 금액 미만입니다."),
    MAXIMUM_MONEY_ERROR_MESSAGE("[ERROR] 최대 구입 금액 초과입니다."),
    INPUT_MONEY_ERROR_MESSAGE("[ERROR] 구매 금액에 적합하지 않은 문자가 입력되었습니다."),
    WINNING_NUMBERS_SIZE_ERROR_MESSAGE("[ERROR] 당첨 번호의 갯수가 6개가 아닙니다."),
    WINNING_NUMBERS_OVER_RANGE_ERROR_MESSAGE("[ERROR] 1 ~ 45 범위를 벗어난 당첨 번호가 존재합니다."),
    WINNING_NUMBER_DUPLICATE_ERROR_MESSAGE("[ERROR] 입력한 당첨 번호와 중복된 번호가 존재합니다."),
    BONUS_NUMBER_DUPLICATE_ERROR_MESSAGE("[ERROR] 입력한 보너스 번호와 중복된 번호가 존재합니다."),
    BONUS_NUMBERS_OVER_RANGE_ERROR_MESSAGE("[ERROR] 1 ~ 45 범위를 벗어난 보너스 번호가 존재합니다."),
    BONUS_NUMBERS_SIZE_ERROR_MESSAGE("[ERROR] 보너스 번호의 갯수가 1개가 아닙니다.");
...
}

 

 

 

run`, `reRun`, `saveWinningNumbers` 등 메서드의 역할이 명확하게 구분되어 코드를 이해하기가 쉽습니다.  또한, 해당 코드는 모듈화되어 있고, 예외 처리 및 재시도 로직이 일반화 되어있어서 나중에 새로운 기능을 추가하거나 수정할때 코드의 변화가 최소화되어 있어서 추후에 참고하기에 좋은 코드인 것 같습니다.

 

 

3. Enum클래스에 있는 통계 카운팅하기


Buyer클래스에 있는 여러  List타입의 Lotto객체들을 순회하면서 Winning클래스의 Lotto와 일치하는지 확인하는 부분에서 Prize Enum을 어떻게 활용할까 고민했었습니다.
Map을 활용해서 enum을 모두 세팅해준다음 key값이 존재할 경우 value를 +1을 해주는 식으로 숫자 통계를 내도록 설정했습니다. 해당 내용은 프로그래머스의 '해쉬맵' 문제를 풀때 사용했던 방식인데, 이렇게 사용해볼 수 있어서 다행입니다.

    private int getTotalPrize(List<Lotto> lottos, Lotto winningLotto, int bonus, Map<Prize, Integer> statistics, int totalPrize) {
        for (Lotto lotto : lottos) {
            int matchCount = countMatchingNumbers(lotto, winningLotto);
            boolean bonusMatch = isBonusMatch(lotto, bonus);
            totalPrize = calculateStatistics(statistics, totalPrize, matchCount, bonusMatch);
        }
        return totalPrize;
    }
    
    private static int calculateStatistics(Map<Prize, Integer> statistics, int totalPrize, int matchCount, boolean bonusMatch) {
        Prize prize = matchPrize(matchCount, bonusMatch);
        if (prize != null) {
            statistics.put(prize, statistics.get(prize) + 1);
            totalPrize += prize.getPrizeAmount();
        }
        return totalPrize;
    }
    
    public static Prize matchPrize(int matchCount, boolean bonusMatch) {
        if (matchCount == 6) return Prize.SIX_MATCH;
        if (matchCount == 5 && bonusMatch) return Prize.FIVE_MATCH_BONUS;
        if (matchCount == 5) return Prize.FIVE_MATCH;
        if (matchCount == 4) return Prize.FOUR_MATCH;
        if (matchCount == 3) return Prize.THREE_MATCH;
        return null;
    }

 

4. 피드백 반영 - Controller랑 Dto사용하기


저는 이전 미션에서 MVC패턴을 따로 사용하지 않고 있었는데, main메소드에서 해당 로직들을 호출할때 정리가 되지 않아서 흐름 파악하는게 어렵다는 피드백을 받았습니다! 그래서 controller를 사용해서 시작, 진행중, 종료 메서드안에서 관련된 로직을 실행할 수 있도록 했고, Dto에 도메인 객체를 저장해서 활용했습니다!