로또 게임 미션 회고

손효재·2023년 3월 21일
0

구현한 미션을 리뷰하며 어떤 것을 배웠고 어떤 점이 부족했는지 정리합니다.
로또 게임 미션 코드 Github

✅ 도메인 로직 테스트 커버리지

🚀 미션 개요 - 기능 요구 사항

로또(자동)

  • 로또 구입 금액을 입력하면 구입 금액에 해당하는 로또를 발급해야 한다.
  • 로또 1장의 가격은 1000원이다.

로또(2등)

  • 2등을 위해 추가 번호를 하나 더 추첨한다.
  • 당첨 통계에 2등도 추가해야 한다.

로또(수동)

  • 현재 로또 생성기는 자동 생성 기능만 제공한다. 사용자가 수동으로 추첨 번호를 입력할 수 있도록 해야 한다.
  • 입력한 금액, 자동 생성 숫자, 수동 생성 번호를 입력하도록 해야 한다.

💡 미션을 통해 배운 내용

정적 팩터리 메서드 사용으로 캐싱

1~45의 로또 번호(LottoNumber) 객체는 생성이 자주 일어나기 때문에 한 번만 생성하고 재사용할 수 있도록 정적 팩터리 메서드를 활용했다.
정적 팩터리 메서드에서 매개변수를 하나 받아서 인스턴스를 반환하는 일반적인 명명 방식인 from을 사용했다.

private static final int MIN_LOTTO_NUMBER = 1;
private static final int MAX_LOTTO_NUMBER = 45; 
private static final List<LottoNumber> LOTTO_NUMBERS = IntStream.rangeClosed(MIN_LOTTO_NUMBER, MAX_LOTTO_NUMBER)
            .mapToObj(LottoNumber::new)
            .collect(Collectors.toList());

    private final int lottoNumber;

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

    public static LottoNumber from(int lottoNumber) {
        if (isOutOfBound(lottoNumber)) {
            throw new IllegalArgumentException("올바르지 않은 숫자입니다");
        }

        return LOTTO_NUMBERS.get(lottoNumber - MIN_LOTTO_NUMBER);
    }
}

EnumMap 활용

로또 등수(매칭 개수, 당첨금)를 Enum 클래스로 관리하면서, 로또 결과를 저장하기 위해 HashMap을 선택했고, 로또가 매칭된 개수를 key, 당첨 인원수를 value로 저장했다. 하지만, EnumMap을 알게 되면서 열거 타입을 key로 가지는 Map 구현체를 사용할 수 있었다.

Enum 에 정의된 상수의 순서를 따라가면서, 내부에서 배열을 사용하는 내부 구현 방식으로, key 값으로 null을 사용할 수 없으며 성능도 더 좋다. Map의 타입 안전성과 배열의 성능을 모두 얻을 수 있었다.

또한, ordinal() 메서드로 어느 위치의 값이 저장되었는지 인덱스를 사용하면서 생기는 문제가 없어진다.
이와 관련해서 “이펙티브 자바 아이템 37. ordinal 인덱싱 대신 EnumMap을 사용하라”를 참고했다.

특별한 일이 없다면 Map 인터페이스로 선언하고 구현체만 EnumMap으로 사용하도록 하자!

상속보다는 컴포지션을 고려하자

당첨 로또를 의미하는 WinLotto 클래스는 Lotto를 상속해서 만들었다.
하지만, 로또 번호의 자료구조인 Set이 int 배열로 바뀌거나 로또 클래스의 메서드나 매개변수가 변경되면, 강한 의존을 가지는 하위 클래스는 전부 깨지게 된다. 이를 해결하기 위해 상속한 하위 클래스를 모두 수정해야 한다.

이와 관련해서 “이펙티브 자바 아이템18. 상속보다는 컴포지션을 사용하라”을 참고했다.

Lotto 객체에서 변경이 일어나면 이를 상속한 클래스에서도 수정 요소가 생기기 때문에 상속을 사용하기보다는, 상속하려던 인스턴스(Lotto)를 클래스의 private 필드로 가지도록 한다.
그로 인해 새로운 클래스(WinLotto)는 기존 클래스(Lotto)의 내부 구현 방식의 영향에서 벗어나고, 기존 클래스에서 새로운 메서드가 추가되어도 영향을 받지 않게 된다.

📚 부족했던 점

객체의 역할에 맞는 책임을 부여하자

Money 객체는 단순히 돈을 계산하는 메서드여야 하는데, Lotto의 가격을 알고 있거나 로또를 몇 개 살 수 있는지를 의미하는 메서드명으로 Money 객체의 역할에 맞지 않는 책임을 가지고 있었다.

해당 객체에 맞는 책임만 가지도록 객체를 설계하고, 반환값을 표현할 수 있는 객체가 있다면 활용하자!
계산한 금액을 반환하는 메서드는 int를 사용하지 않고, Money 클래스로 반환할 수 있겠다.

중복되는 테스트도 모두 작성해야 할까?

말이 이상한데, 당연히 아니라고 답할 질문이다. 왜 이런 고민을 적었는지 돌아보니, 일급 객체와 일급 컬렉션을 불필요하게 사용하다 보니 테스트 코드를 작성하면서 테스트가 중복됨을 느꼈다.
로또(Lotto)에서 하나의 로또번호(LottoNumber)가 포함되어 있는지 확인하는 메서드는, 필드로 사용하던 로또번호들을 가지고 있는 LottoNumbers 에서 다시 LottoNumber가 포함되는지 확인하는 로직으로 중복되어 사용하고 있었다.

결국 로또(Lotto)의 모든 책임을 로또번호들(LottoNumbers)가 가지게 되면서, 단지 한번 감싸는게 전부였다. 불필요한 작업이라고 생각되어 제거하고, 중복되는 테스트 없이 깔끔한 구조로 바꿀 수 있었다.

도메인 모델의 관리 포인트를 늘리지 말자

값을 입력받는 과정에서 view에서 도메인 모델을 생성하고 controller로 넘겨줬다.
이로 인해 도메인 모델의 관리 포인트가 view까지 늘어나게 된 것이다. 불필요한 과정이었고 view에서는 원시값만 받고 controller에서 도메인 모델을 사용하도록 하자.

✍🏻 회고

이펙티브 자바의 내용을 참고하는게 많은 도움이 되었다. 정적 팩터리 메서드와 EnumMap의 사용, 상속이 항상 최선이 아님을 알고 상속과 컴포지션을 사용하는 등 근거 있게 사용할 수 있게 되었다. 아는 만큼 보인다는 말처럼 기술 서적을 가까이하며 성장해야겠음을 느꼈다.

객체를 꺼내쓰지 않고, 메시지를 보내려고 신경 쓰면서 객체 지향 프로그래밍이 처음에 비해 많이 발전했음을 느낀다. 로또 미션을 하면서 새로운 내용을 많이 배워서 좋았다.

💻 실행결과

0개의 댓글