우아한테크코스 7기 프리코스 3주차 회고

허준기·2024년 11월 4일
9

우테코

목록 보기
7/7
post-thumbnail

2주차 코드 리뷰

혼자 개발할 때와 다르게 이렇게 다양한 사람들이 코드에 대한 의견을 남기며, 그 의견에 대해서 동의를 하거나 반박을 하는 과정이 정말 재미있는 것 같다.

평소 생각하던 방식과 다른 코드들을 보며 나의 코드에 의문을 가질 때도 있고, 확신을 가질 때도 있다. 다른 사람들에게 리뷰를 남기는 과정에서 혹시나 틀린 정보를 전달할까봐 한번 더 검색해보고, 생각하는 과정에서 더욱 성장하는 기분이 들어 코드 리뷰를 좋아하는 것 같다.

이 부분에 관해서 고민을 하시는 분이 있길래 나의 생각을 말해보기도 했는데 다행히 동의하는 분이 많아서 그런가 따봉도 꽤 찍혔다!!!

2주차 미션 종료 직후 다른 사람들과의 코드 리뷰를 통해 다시 돌아보며 많이 배운 것 같다

꽤 많다고 생각하지만 Conversation은 결국 숫자일 뿐 내가 그 안에서 무엇을 배우고 얻었는지가 더욱 중요한 것 같다. 물론 그 숫자가 늘어나면 늘어날수록 그렇게 얻는것도 많아지긴 하지만...

아무튼 많은 의견을 들을 수 있었고 그 의견들을 바탕으로 개선해야할 점을 정리해봤다!

- 매직넘버 상수화
- 테스트 클래스 분리
- 의미없는 멤버 변수 삭제
- 변수면 자료형 명시 X
- 메서드명 의미 확실하게
- 어떤 테스트인지 확실하게 명시
- 의미있는 개행
- 스트림 사용해보기
- 동일 클래스 내 일급 객체 패키지로 관리
- scanner `close` 해주기
- 출력할 문자열 모아서 한번에 출력
- 멤버 변수 순서 생각
- 전략패턴 final 고려 - 러닝타임에 변경될 수 있다고 가정
- 일급 컬렉션 사용해보기

이 정도의 피드백을 생각해봤는데 로또 미션을 진행하면서 반영해보기 위해서 노력했다.

로또 미션

우아한테크코스 3번째 미션은 로또다!
이번 미션 역시 TDD 방식으로 구현해본 것 같다!

제일 고민을 많이 한 부분은 역시 재입력 부분인 것 같다
컨트롤러에 로직을 넣어도 될까 하는 많은 고민을 했는데... 결국 방법을 생각해내지 못해서 입력 부분에 대해서 try-catch문으로 감싸는 방식을 택하게 되었다...

코드에 관한 부분은 뒤에서 설명합니당

진짜 로또 사기

미션을 보고 저녁을 먹으러 갔는데 마침 근처에 로또를 판매하는 곳이 있어서 기념으로 한번 사봤다!

이거 1등 당첨되면 어떡하지 라는 생각을 좀 해봤지만... 결과는 아래에!

설계

이번 미션에서는 전체 흐름을 한 번 그려보고 시작했다!
그리고 대략적인 클래스도 써봤다. 물론 이 정도로는 설계가 전부 되지는 않아서 좀 더 추가한 부분이 많다.

초기 구현에서는 재입력에 대한 생각을 하지 않고 일단 돌아가는 기능을 만드는것에 집중했다.

그렇게 여러개의 클래스가 나왔다!

- 로또(Lotto) 클래스
    - [ ] List<Integer>를 가진 일급 컬렉션
    - [ ] 개수 검증
    - [ ] 범위 검증
    - [ ] 중복 숫자 검증


- 구입 금액(Money) 클래스(String money)
    - [ ] 숫자 검증
    - [ ] 양수 검증
    - [ ] 1000 배수 검증


- 당첨 번호(WinningNumber) 클래스
    - [ ] `List<Integer>` 를 가짐
    - [ ] `BonusNumber` 를 가짐
        - [ ] 중복 검증
    - [ ] 숫자 검증
    - [ ] 정수형 검증
    - [ ] 양수 검증
    - [ ] 범위 검증


- 보너스 번호(BonusNumber) 클래스
    - [ ] 숫자 검증
    - [ ] 정수형 검증
    - [ ] 양수 검증
    - [ ] 범위 검증
    - [ ] 중복 검증


- 당첨 금액(WinningMoney) 클래스
    - [ ] 당첨 내역을 받아온다
    - [ ] 당첨금을 계산한다


- 당첨(Rank) 열거형
    - [ ] 순위, 금액, 보너스 번호 여부를 가진다


- 시도 횟수(Trial) 클래스(Money money)
    - [ ] `Money`를 바탕으로 시도횟수를 계산한다
        - [ ] `Money` / 1000


- 수익률(ReturnRate) 클래스(구입 금액, 당첨 금액)
    - [ ] 수익률을 계산한다
        - [ ] 당첨 금액 / 구입 금액
        - [ ] 소수점 둘째 자리 반올림


- 랜덤 숫자 생성(RandomNumberGenerator) 클래스
    - [ ] 숫자 생성 인터페이스의 구현체
    - [ ] 1~45 사이의 랜덤한 숫자를 6개 생성한다
    - [ ] 오름차순으로 정렬


- 당첨 여부 판단 클래스(List<Lotto>, WinningNumber)
    - [ ] List<Lotto>와 WinningNumber 를 멤버 변수로 가진다
    - List<`Lotto`> 를 순회하며 당첨 여부 확인
        - [ ] `Lotto` 클래스와 `WinningNumber` 클래스를 비교 후 당첨 여부 결정


- 입력 클래스
    - [ ] 구입 금액을 입력 받는다
    - [ ] 당첨 번호를 입력 받는다
    - [ ] 보너스 번호를 입력 받는다


- 출력 클래스
    - [ ] 발행한 로또 수량 및 번호를 출력한다
    - [ ] 당첨 내역을 출력한다
        - [ ] 등수별로 출력
    - [ ] 수익률을 출력한다

이 정도의 클래스가 나온 것 같은데 중간중간에 추가된 부분이 조금씩은 있는 것 같다!

이번에는 저번 미션에서 알게 된 일급 객체일급 컬렉션 이라는 개념에 대해서 집중해 하나의 자료형을 관리해주는 클래스를 만들어 검증을 해보는 방식을 선택했다.

이렇게 하면 검증의 책임이 해당 클래스에 있으니 관리가 좀 더 용이해져서 좋다고 생각했다.

그리고 Rank 라는 enum을 만들어서 상금이나 등수, 몇개의 숫자가 맞는지, 보너스 번호인지에 대한 관리를 해주기로 했다.
따로 클래스를 만드는 것보다 enum을 통해 하나로 관리를 해주니 나중에 좀 더 용이해서 좋은 선택이었다.

public enum Rank {
    PLACE_1ST(6, false, 200000000, 1),
    PLACE_2ST(5, true, 30000000, 2),
    PLACE_3ST(5, false, 1500000, 3),
    PLACE_4ST(4, false, 50000, 4),
    PLACE_5ST(3, false, 5000, 5),
    PLACE_NO(0, false, 0, 6);
    
    ...
    
}

이런느낌..

테스트

그리고 구현 전에 이번에도 테스트를 만들어줬다
위의 단계에서의 설계를 바탕으로 몇개의 클래스를 대상으로 테스트를 만들어줬다
아무 클래스도 만들지 않고 일단 테스트부터 만드니까 클래스 부분에서 빨간줄이 잔뜩 떴다!!

이건 해당 클래스가 존재하지 않아서 뜨는 에러.... 근데 이런식으로 진행을 하게 되면 테스트를 통과시킬때마다 퀘스트를 하나씩 클리어하는 기분이라 좀 더 개발할 맛이 나는 것 같다

그리고 테스트를 잘 구성해서 그런지? 나중에 로직의 변경이 있을때도 테스트 코드는 변경하지 않았다. :)

구현

구현 부분에서는 설계한 부분을 바탕으로 진행을 했다.

Input에 대한 출력문은 어디에서?

보통 View 부분부터 구현하는 편인데, InputView클래스와 OutputView 클래스를 구현을 해두면 나중에 매개변수를 어떤식으로 전달할 지 고민을 덜하게 돼서 선호하는 방식이다.

InputOutput 에 관한 내용이 나와서 프리코스 커뮤니티에서 본 입력을 받을 경우의 출력문은 어디서 판별해야 하는가에 대한 의논이 생각났다.

다른 사람들의 코드에서Input 부분에 아예 Console.readLine() 이라는 메서드 하나만 두고 관리하는 부분을 본 적 있다.

이 하나의 메서드로 모든 입력을 관리하게 되면 결국 입력 부분에서 Output 부분을 호출해야하는데 이 또한 비용이라는 생각이 들어서 선호하지 않는 편이다!

이 부분에 대해서도 토론을 하는 글이 있어서 의견을 남겨봤다!

그런데 이 부분은 취향 차이라고 생각한다
나는 출력문 하나로 인해서 controller 부분에서 한 줄이 더 늘어난다고 생각하니 좋지 않다는 생각이 들었다!

일급 객체, 일급 컬렉션

다시 구현으로 돌아와서..

그리고 위에서 말한 것처럼 하나의 자료형을 분리해주려고 노력을 했다!

BonusNumber, Lotto, Money, Trial 클래스에 대해서 저 개념들을 적용해봤다

이번 미션에서 가장 중요한 Lotto 클래스를 보자면 List<Integer> numbers 를 위한 클래스인 것을 볼 수 있다.

저번 미션에서 private static final String 가 붙은 상수들이 있으면 "일급 객체일급 컬렉션이 아니게 되지 않을까?" 라는 고민을 했었는데 상수static을 통해서 전역적으로 관리하니 멤버 변수에 속하지 않는다는 결론을 내리고 사용해도 괜찮다는 판단을 했다!

그리고 생성자를 통해 검증을 진행해주는데 다른 사람들 중에는 생성자에 로직이 들어가는 것을 선호하진 않는 사람들도 있는데, 나는 생성자를 통해서 객체가 생성될 때 검증을 해주는 것까지 생성자의 역할이라고 생각하기 때문에 해당 방식으로 하게 되었다. sortNumbers()는 뺄 수 있으면 좋았을 것 같긴하다...ㅠㅠ

상수 클래스 분리?

2주차 미션을 진행할 때 다양한 상수가 나왔는데 이때는 따로 상수 클래스를 만들어주지 않고, 해당 상수를 구현하는 클래스 내에 static final로 구성하는 방식으로 해줬다

하지만 이번 미션에서는 로또 범위나, 사이즈 등 동일한 숫자에 관한 검증이 많다보니 자주 사용하는 상수들을 따로 빼줄 수 밖에 없었다!

그렇게 나온 Constant 클래스..

public final class Constant {

    private Constant() {}

    public static final int MIN_LOTTO_NUM = 1;

    public static final int MAX_LOTTO_NUM = 45;

    public static final int LOTTO_NUM_COUNT = 6;

    public static final int LOTTO_PRICE = 1000;
}

정말 여러번 사용하는 상수만 관리하고 한번만 사용하는 상수는 계속 해당 클래스에서 선언하도록 냅뒀다!

초기에는 public static final int ZERO = 0 이라는 상수도 만들어놨는데 찾아보니까 Integer.valueOf(0)라는게 이미 있어서 없애고 저걸 사용해주게 되었다
저렇게 바꾸다가 든 의문은 왜 그냥 0 을 사용하지 않고 Integer.valueOf(0)로 감싸줄까? 였다

궁금해서 검색을 해 본 결과... 새로운 객체를 생성해주는 것과 cache 된 객체를 사용하는 차이라고 한다!

0을 많이 사용하면 사용할수록 동일한 객체 생성이 반복되는 것.

재입력에 대한 고민...

이번 미션에서 가장 많은 고민을 한 부분이다...
과연 재입력을 어떻게 받아야 하는가?

처음에는 그냥 run() 메서드 내부를 try-catch로 감싸줬다

public void run() {
        while (true) {
            try {
                Money money = new Money(InputView.inputMoney());
                Trial trial = new Trial(money.getMoney());
                Lottos lottos = new Lottos(numberGenerator, trial);
                OutputView.printLotties(lottos.getLottoNums());

                WinningNumber winningNumber = new WinningNumber(InputView.inputWinningNumber(),
                    InputView.inputBonusNumber());
                LottoChecker lottoChecker = new LottoChecker(lottos, winningNumber);
                WinningMoney winningMoney = new WinningMoney(lottoChecker.checkLottos());
                ReturnRate returnRate = new ReturnRate(winningMoney, money);

                OutputView.printWinningDetails(lottoChecker.getResultMap());
                OutputView.printReturnRate(returnRate.getReturnRate());
                break;

            } catch (IllegalArgumentException | IllegalStateException e) {
                System.out.println(e.getMessage());
            }
        }
    }

이렇게 되니 당연히!!!! 예외가 발생하면 처음부터 다시 시작하게 됐다.

하지만 요구사항은

사용자가 잘못된 값을 입력할 경우 IllegalArgumentException을 발생시키고, "[ERROR]"로 시작하는 에러 메시지를 출력 후 그 부분부터 입력을 다시 받는다.

그 부분부터 입력을 다시 받는다 이 부분이 가장 중요했다!!!

그래서 많은 고민을 해봤다...
하지만 다른 마땅한 방법이 떠오르지 않았고 결국 입력 부분에 대해서 try-catch 문으로 감싸주는 방법을 선택하게 되었다.

public void run() {
        Money money = inputMoneyWithRetry();
        Trial trial = new Trial(money.getMoney());
        Lottos lottos = new Lottos(numberGenerator, trial);
        OutputView.printLottos(lottos.getLottoNums());

        WinningNumber winningNumber = inputWinningNumberWithRetry();
        LottoChecker lottoChecker = new LottoChecker(lottos, winningNumber);
        WinningMoney winningMoney = new WinningMoney(lottoChecker.checkLottos());
        ReturnRate returnRate = new ReturnRate(winningMoney, money);

        OutputView.printWinningDetails(lottoChecker.getResultMap());
        OutputView.printReturnRate(returnRate.getReturnRate());
    }

    private Money inputMoneyWithRetry() {
        while (true) {
            try {
                return new Money(InputView.inputMoney());
            } catch (IllegalArgumentException e) {
                OutputView.printErrorMessage(e.getMessage());
            }
        }
    }

    private WinningNumber inputWinningNumberWithRetry() {
        while (true) {
            try {
                return new WinningNumber(InputView.inputWinningNumber(), InputView.inputBonusNumber());
            } catch (IllegalArgumentException e) {
                OutputView.printErrorMessage(e.getMessage());
            }
        }
    }

당연히 이 방법이 최선의 방법은 아니라고 생각한다.
3주차 미션에 대한 코드리뷰를 진행하면서 다른 사람들은 어떤식으로 이 문제를 해결했는지 보고 배워보고 싶다!!

전략 패턴

마지막으로 전략 패턴 이다!
2주차 자동차 미션에서도 사용했던 방법이다. 그 때는 Car 클래스에 NumberGenerator를 넘겨주는 방식을 사용했는데 코드리뷰를 진행하면서 이는 불필요하다고 느꼈다.

그래서 이번 미션에서 바꾼 방법이 Lottos 라는 Lotto들을 관리하는 클래스가 NumberGenerator를 가지고 있고 Lotto 객체를 만들때 생성자로 List를 넘겨주는 방식이다.

이렇게 하면 Lotto 클래스는 List<Integer> 하나의 멤버 변수만 가진 일급 컬렉션으로 유지될 수 있다!

Lottos 클래스에서 Lotto를 만드는 방법!

public Lottos(NumberGenerator numberGenerator, Trial trial) {
        this.numberGenerator = numberGenerator;
        this.trial = trial;
        makeLottos();
    }

    public void makeLottos() {
        for (int i = 0; i < trial.getTrial(); i++) {
            lottos.add(new Lotto(numberGenerator.generateNumber()));
        }
    }

Lotto 클래스 생성자

 public Lotto(List<Integer> numbers){}

이렇게 하니 Lotto 클래스는 NumberGenerator를 가지지 않아도 되고 List에 대한 검증만 진행하면 돼서 역할이 확실하게 분리됐다!

전략 패턴을 이용한 테스트

그리고 전략패턴을 통해서 Lotto 클래스에 대한 테스트도 진행해줬다

@Test
    void 로또_정상_동작_테스트() {
        assertSimpleTest(() -> {
            Lotto lotto = new Lotto(new CorrectNumberGenerator().generateNumber());
            assertThat(lotto.getNumbers()).isEqualTo(List.of(1, 2, 3, 4, 5, 6));
        });
    }

    @Test
    void 로또_번호의_개수가_6개가_넘어가면_예외가_발생한다() {
        assertThatThrownBy(() -> new Lotto(new WrongSizeNumberGenerator().generateNumber()))
            .isInstanceOf(IllegalArgumentException.class);
    }

위의 테스트들에 있는 CorrectNumberGenerator 클래스와 WrongSizeNumberGenerator 클래스를 따로 테스트 패키지에 만들어 테스트를 진행할 때 사용해줬다!

public class CorrectNumberGenerator implements NumberGenerator {

    @Override
    public List<Integer> generateNumber() {
        return List.of(1, 2, 3, 4, 5, 6);
    }
}

public class WrongSizeNumberGenerator implements NumberGenerator {

    @Override
    public List<Integer> generateNumber() {
        return List.of(1, 2, 3, 4, 5, 6, 7);
    }
}

이런식으로 진행을 해주어 전략 패턴을 사용한 테스트도 진행해볼 수 있었다!

디버깅

미션을 진행하면서 로또가 생성되지 않는 상황이 있었다!!!
어떨 때는 로또를 제대로 생성하고 어떨 때는 제대로 생성하지 못하는....
같은 1,000원이나 다른 같은 금액을 입력했을 때, 로또가 생성될 때가 있고 생성되지 않을 때가 있었다!

이 때 System.out.println()을 통해서 찍어가면서 오류가 나는 부분을 찾아봤는데 나오지 않았다...
그러다가 디버깅을 해보게 됐다!

로또 생성 부분에 중단점을 찍고 디버깅을 해보니 그 동안 찾았던 시간이 아까울 정도로 간단하게 해결이 돼서 허무했다..! 하지만 돌아보면 디버깅의 중요함을 배우게 되어 다행이라는 생각이 든다.

학부 1학년 때 디버깅을 왜 하는지 의문이 있었는데 의문이 해소됐다!
지금은 요구사항이 작지만, 나중에 요구사항이 변경되거나 커지면 System.out.println()를 통해서 오류를 발견하는 방법은 일일이 출력해봐야 하기 때문에 한계가 존재한다. 하지만 디버깅을 이용한다면 코드 한줄한줄이 진행되면서의 변경사항을 볼 수 있기 때문에 시간 절약이 되고 흐름을 파악하는데 더욱 도움이 될 것이다!

앞으로도 디버깅애용하자

수익률 계산

대망의 로또 당첨 결과....

과연.....!

5천원!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

5천원을 투자해서 5천원이라는 결과가 나왔다!!
수익률 100%. ㅠㅠ

하지만 이게 굉장히 잘 나온 결과라는 사실.....

궁금해서 내가 짠 코드로 평균 수익률이 얼마정도 나오는지 계산을 해봤다

100만원

13%

23.5%

16.5%

3번 평균 17.666666666...%
100만원은 너무 운에 좌지우지 돼서 의미가 없었다!
심지어 그 아래는 진짜 운의 영역..

그래서 1억과 10억을 검사해봤다

1억

23.5%

23.8%

21.4%

53.5%!!!!!!!!

25.6%

1억 정도 되니까 뭔가 유의미한 결과가 나왔다!
1억 이면 10만개의 로또...

5번 평균 29.56%의 수익률.... 돈 잃는 방법이라고 봐도 될 것 같다
운 좋게 2등 한 번 당첨돼서 이정도이다..

그래도 아직 1등이 당첨이 안됐으니까 마지막으로 10억을 해보기로 했다!

10억

24.6%

22.1%

26.3%

23.0%

22.0%

.......

5백만개의 로또를 샀지만 1등은 단 한번도 당첨되지 않았다!!
로또 1등 당첨은 하늘의 별따기....
그래도 수가 크니까 대략 20초반의 수익률이 나오는 것을 볼 수 있다

내가 5천원 투자해서 5천원 번 100%의 수익률이 얼마나 힘든건지 알 수 있게 되는 시뮬레이션이었다.

1등되는 상상만..

후기

이번 미션을 통해서도 역시 많은 고민을 하면서 코드를 짜 볼 수 있었다!
코드리뷰를 통해 받은 피드백도 그렇고 내가 코드를 짜는 것에 대한 근거를 찾는 과정이 재밌는것 같다

그리고 내가 짠 코드로 직접 수익률 계산까지 해보니까 코드를 짠 의미가 더욱 있어지고 재밌었던 것 같다!

다음 미션도 나를 성장시키기 위해 힘내보자!!!

작고 소중한 PR

https://github.com/woowacourse-precourse/java-lotto-7/pull/148

로또는 사지말자

profile
나는 허준기
post-custom-banner

0개의 댓글