본문 바로가기
Memo/우테코 4기

[우테코] 로또 미션 1단계 학습 로그

by 연로그 2022. 2. 28.
반응형

목차

1. GitHub 저장소

2. 구현 기능 목록

3. 새로운 도전

4. 생각하기

5. 앞으로 할 일


 

 


1. GitHub 저장소 😺

 

Repository: https://github.com/yeon-06/java-lotto/tree/step1

Pull Request: https://github.com/woowacourse/java-lotto/pull/391

 

 


2. 구현 기능 목록 📚

페어인 칙촉이랑 만난 기념으로 로또 한장^^

 

  1. 로또 구입 금액
    • 로또 가격은 1000원
    • (e) 1000원 단위 외의 금액 불가
    • (e) 문자열 입력
    • 구매 로또 개수 구하기
  2. 구매한 로또 개수 출력
  3. 로또 목록
    • 로또 생성
      • 숫자는 1 ~ 45
      • (e) 중복된 숫자 불가
      • Collections.shuffle() 활용
    • 목록 출력
    • 로또 숫자 정렬
  4. 당첨 번호 입력
    • 6개 숫자 입력 (ex: 1, 2, 3)
    • 숫자는 1 ~ 45
    • 입력 구분자는 ,
    • (e) 중복된 숫자 불가
  5. 보너스 번호 입력
    • 숫자는 1 ~ 45
    • (e) 당첨 번호와 중복 불가
  6. 추첨
    • 숫자랑 위치 일치하는지 확인
    • 보너스볼 확인
  7. 추첨 결과 출력
    • 수익률 출력 (수익률 = 당첨 금액 /구입 금액)

 

 


3. 새로운 도전 🚀

TDD를 시작하는 방법에 대해서 조금은 감을 잡았으니 자세한 구현 과정은 생략한다.

이번 미션을 통해서 어떤 도전을 하였는가에 대해 정리해보았다.

 

3-1. 정적 팩토리 메소드 도입

이번 미션에서 숫자 하나를 저장하는 LottoNumber를 생성했다.

항상 1 ~ 45 사이의 숫자만을 저장하고 저장 후에는 값이 변경되지 않으며 객체 간 동등성을 보장해야한다고 판단,

정적 팩토리 메소드를 도입해 인스턴스를 재사용하도록 만들어 보았다.

 

public class LottoNumber {
    // ...
    private static final LottoNumber[] cacheLottoNumber = new LottoNumber[MAX + 1];

    private final int number;

    static {
        IntStream.range(MIN, MAX + 1)
                .forEach(i -> cacheLottoNumber[i] = new LottoNumber(i));
    }

    private LottoNumber(int number) {
        this.number = number;
    }

    public static LottoNumber of(int number) {
        validateNumber(number);
        return cacheLottoNumber[number];
    }

    // ...
}

 

이미 존재하는 인스턴스를 가져오기만 하는데 동등성 보장을 해줘야하는가?

👉 추후에 애플리케이션의 변경이 일어나거나 새로운 인스턴스를 반환하는 케이스가 생기는 등 LottoNumber의 동등성이 침해받는 케이스가 생길 수 있다

 

3-2. EnumMap의 사용

Ranking이라는 Enum을 key값으로 LinkedHashMap을 생성했다.

더 좋은 방법을 찾다가 EnumMap에 대해 알게 되었고 적용시켰다.

이 부분에 대해서는 별도로 포스팅한 것이 있으니 링크로 대체한다.

👉 https://yeonyeon.tistory.com/195

 

[Java] EnumMap이란?

😏 개요 먼저 Enum에 대해 잘 모르면 아래 글을 먼저 읽어보길 바란다. https://yeonyeon.tistory.com/171 [Java] Enum에 대해 목차 1. Enum이란? 2. 사용 방법 3. 주요 메소드 4. Singleton과 Enum 1. Enum이란?..

yeonyeon.tistory.com

 

3-3. @CsvSource에서 Enum 받기

구매한 로또 번호가 당첨 되었다면 Ranking(Enum 클래스)이 반환되는 메소드가 있다.

원래는 모든 등수에 대해 테스트 코드를 하나하나 반환했다.

이를 불편하다고 느꼈고 Enum을 어떻게 받아낼까 고민을 하다가 @CsvSource가 Enum도 받아낼 수 있다는 것을 알게 되었다.

FIRST, SECOND, ... 는 Enum에 존재하는 상수들의 이름이다.

@ParameterizedTest
@CsvSource(value = {"1,2,3,4,5,6:FIRST", "1,2,3,4,5,7:SECOND", "1,2,3,4,5,8:THIRD"
        , "1,2,3,4,8,9:FOURTH", "1,2,3,8,9,10:FIFTH"}, delimiter = ':')
@DisplayName("[n]등 당첨 결과 반환")
void prize(String input, Ranking expect) {
    // given
    LottoNumbers lottoNumbers = new LottoNumbers("1,2,3,4,5,6");
    LottoNumber bonusNumber = LottoNumber.of(7);
    winningNumbers = new WinningNumbers(lottoNumbers, bonusNumber);

    // when
    LottoNumbers myLotto = new LottoNumbers(input);

    // then
    assertThat(winningNumbers.calculateRanking(myLotto)).isEqualTo(expect);
}

 

3-4. 불변 컬렉션 반환하기

객체의 설계상 데이터가 변경되면 안되는 경우가 많았다.

List 형태의 데이터들도 예외가 아니었고 이를 확실하게 변경이 불가능하다고 못박아두고 싶었다.

// ex 1
return new ArrayList<>(candidateLottoNumbers.subList(0, LOTTO_COUNT));	// AS-IS
return List.copyOf(candidateLottoNumbers.subList(0, LOTTO_COUNT));	// TO-DO
// ex 2
private final List<LottoNumbers> lottoTickets = new ArrayList<>();

return lottoTickets;					// AS-IS
return Collections.unmodifiableList(lottoTickets);	// TO-DO

 

 


4. 생각하기 🏃‍♀️

내가 궁금했던 점이나 코드 리뷰를 받은 부분에 대해 정리한다.

🙋‍♀️: 연로그(본인), 🙍‍♂️: 닉(리뷰어)

 

4-1. 모든 validation이 도메인의 역할일까?

🙍‍♂️ 저는 View라는 계층이 별도로 있다는 점에서, View에서 최소한의 사용자 입력 유효성은 보장해주어야 하지 않을까? 라고 생각해요. 우리가 로또 숫자 1개를 입력 받을 때 고려해야 할 validation은 다음과 같은 것들이 있죠.

  1. 해당 입력이 숫자인가?
  2. 해당 입력이 1과 45 사이의 숫자인가?

분명 1번 validation과 2번 validation은 성격이 다릅니다. 1번은 우리가 정한 도메인 요구사항과 관련 없이, 숫자를 원한다면 숫자를 받아야 하는 최소한의 값 정합성인 것이고, 2번은 우리가 정한 도메인 요구사항의 규칙에 맞는지 검사한 것이죠. 어떻게 수정하면 좋을지 한번 고민해보시고, 여기 뿐만 아니라 전체적으로 리팩토링 해보셔요 :)

 

👉 1의 책임은 view, 2의 책임은 domain이 가도록 수정

 

4-2. 상수화 하는가?

 

🙋‍♀️ InputView에서의 문자열도 하드 코딩일까요? 단 한 곳에서만 사용하는데 이 문자열도 상수화를 해야할지 고민이 됩니다.

 

🙍‍♂️ 상수화를 왜 하는가?에 대한 자신만의 답을 내려보길 바라요 ㅎㅎ 왜 할까요? :) 이유가 정립된다면 위 문자열을 상수로 만들지 아닐지에 대한 기준이 나올 것 같아요 ㅎㅎ

 

🙋‍♀️ 재사용 가능 여지가 충분한 경우 재사용을 위해, 또는 유지보수를 쉽게 하기 위함이라고 이해하고 있습니다! 상수는 기본적으로 클래스 내부의 최상단에 위치하니 해당 메소드의 위치를 찾을 필요 없이 좀 더 쉽게 접근할 수 있기 때문에 유지 보수를 쉽게 할 수 있다고 생각을 합니다. 정리해놓고 보니 상수화를 진행해야 겠네요 ㅎㅎ 혹시 닉은 이견 있으시다면 추가로 의견 부탁드립니다!

 

🙍‍♂️ 네네 좋습니다. 사실 지금 단계에서는 View가 단순한 편이라 굳이 상수화를 진행하지 않아도 괜찮다고 생각하긴 해요. 애플리케이션이 커지면 사용자향 메시지 같은 경우는 별도의 파일로 관리할 수도 있을거고요 :)

 

4-3. 에러 메시지를 이용해 테스트 하기

 

🙋‍♀️ 중복 에러, 개수 에러 나는 경우를 따로 테스트하기 위해 상수를 default로 지정했습니다 (테스트 코드에서도 사용하기 위함) 해당 문자열을 테스트 코드에 복붙해갈수도 있지만 에러 문구가 바뀌게 되면 원본 코드, 테스트 코드 둘 다 수정해야한다는 번거로움 때문에 위 방법을 택했는데 어떻게 생각하시나요??

 

🙍‍♂️ 네네 좋은 시도입니다ㅎㅎ 저는 조금 다르게 생각하는 부분이 있는데요. 만약 누군가 실수로 사용자향 에러 메시지를 수정했다고 가정하면, 해당 테스트가 그래도 통과할 것이기 때문에 그대로 배포가 나갈 수도 있다고 생각해요. (일종의 장애인거죠) 문자열 메시지를 수정했을 때 테스트가 깨져야 개발자가 더 인지를 할 수 있고, 체크를 할 수 있게 되어서 테스트에서는 문자열 날것으로 검증하는 것이 좋다고 생각합니다 :)

 

👉 이에 동의하고 접근 제어자 private로 전환, 테스트 코드에서 직접 하드 코딩한 메시지 사용

 

4-4. 배열 반환하기

 

🙍‍♂️ 배열 보다는 리스트를 사용하는 것이 변경/확장에 용이합니다 :)

 

🙋‍♀️ 리스트가 변경, 확장에 용이하기 때문에 오히려 배열을 사용했는데요. (변경이나 확장될 일이 없어야 한다고 판단) 이런 경우에는 보통 배열보다는 불변으로 만드시나요?

 

🙍‍♂️ 네네 그런 의도라면 불변 리스트로 만들어서 반환합니다. 공개 API를 설계할 때, 클라이언트가 어떻게 사용할지는 모르지만 배열의 경우 사용하면서 추가 가공이 필요하기 때문에 (stream을 사용한다던가) 사용성이 떨어지는 편입니다.

 

 


5. 앞으로 할 일

2단계에서 리팩토링 할 부분에 대해 간단히 정리했다.

 

  • LottoNumber → 변수명 value 고려해보기
  • LottoNumber → 정적 팩토리 메소드의 구조 다시 생각해보기 (미리 만들어놓지 말고 필요할 때 생성)
  • LottoNumber → 캐싱한 자료구조 map 고려해보기
  • LottoNumbers → MAX, MIN 상수 값이 LottoNumber 것이 아닌지 생각해보기
  • LottoNumbers → 생성자의 자료형 변경하기
  • Ranking → DEFAULT 대신 NONE 이름 고려하기
  • WinningResultTest → given, when, then 적절하게 사용하기
  • 전체 → 상수화에서 String.format()을 적극 사용하기
반응형