[코드스쿼드] Max 2주차 - 로또 미션

Jinny·2023년 3월 16일
1

미션

목록 보기
1/2
post-thumbnail

작년 우아한테크코스 프리코스에 참여하며 로또 미션을 구현한 적이 있다.

당시에는 Java를 공부한지 한달정도 되었을 때라 OOP니, TDD니
다양한 것을 고려하며 코드를 짜지는 못했고
어디서 주어들은 MVC 패턴을 흉내내며 프로그램이 돌아가게끔 하는데 집중한 것 같다.

사실 그것 조차 힘에 부치기도 했다.
모르는 문법도 많아 그때그때 검색하며 구현하기도 했다.

그로부터 4개월정도 지난 지금 코드스쿼드에서 다시 로또 미션을 진행하게 되었다.
그때랑 지금이랑 뭐가 달라졌을까 문득 궁금해져 코드를 비교해보았다.

로또 미션의 내용은 우테코 로또 미션 GitHub 링크에 자세하게 나와있다.


💸 우테코 로또 미션

당시에는 최선이라고 생각했는데, 4개월 지난 지금 코드를 다시 보니 💩 x 10000 이다.
(물론 지금 코드가 💩이 아니라는 소리는 아니다.)

물론 Java 문법을 모르다보니 생긴 문제도 있었다.

예를 들면 여러장의 로또를 구매한 경우 List<List<Lotto>> 로 데이터를 저장했는데,
당첨 번호와 각 로또의 일치하는 숫자를 계산하기 위해 <List<Lotto>를 2중 for문으로 하나씩 꺼내왔다던지

contains() 메서드를 몰라서 일치하는 숫자 개수를 이런식으로 구현했었다.
보기만 해도 머리가 아프다...(혼란하다 혼란해)

    private int getMatchedCount(List<Integer> lottoNumbers, List<Integer> playerLottoNumbers) {
        List<Integer> intersection = new ArrayList<>(playerLottoNumbers);
        intersection.retainAll(lottoNumbers);
        if (intersection.isEmpty()) {
            return 0;
        }
        return intersection.size();
    }

참고로 지금은 이렇게 구현했다.

    public int countMatches(Lotto compare) {
        return (int) compare.lottoNumbers.stream()
                .filter(lottoNumbers::contains)
                .count();
    }

🙁 문제점

이런 문법을 몰라서 생긴 문제점은 차치하고 문제점을 좀 추려보면 다음과 같다.

  1. 한 개의 메서드가 여러가지 일을 하고 있다.
  2. 객체에 필드 변수가 많다.
  3. 메서드에 파라미터 개수가 많다.
  4. 도메인 로직과 UI 로직이 분리가 안됐다.
  5. 객체 안에 로직이 없고 Service/Controller에서 로직을 구현했다.
    => 객체가 수동적이다.
    => 객체를 객체스럽게 사용하지 못했다.

비슷한 미션을 진행하고 리팩토링을 해보면서 1~4번 문제는 점차 줄어들었다.
하지만 5번 문제를 어떻게 개선하면 좋을지 감이 잡히지 않았다.


🤔 어떻게 해야 객체를 객체답게 구현할 수 있을까?

힌트는 getter 를 줄이고 객체에 메시지를 던져 객체가 책임져야할 로직을 구현하는 데에서 얻었다.

당시에 참고했던 좋은 블로그 글을 공유한다.

getter를 사용하는 대신 객체에 메시지를 보내자


🧐 코드 비교

4개월 전에 작성한 코드와 지금 코드를 비교를 해보자.
물론 지금 코드도 완벽한 건 아니지만 확실하게 차이가 보인다.


🚫 getter를 지양하기

이전 코드

로또 구입 금액을 의미하는 객체가 있다.
유효성 검증을 할 뿐 getter 외에 별다른 메서드가 없다.

public class Money {

    private static final int LOTTO_PRICE = 1000;

    private final int money;

    public Money(int money) {
        validatePrice(money);
        this.money = money;
    }
	
    public int getMoney() { // getter
        return this.money;
    }
    
    private void validatePrice(int money) { // 유효성 검증
        if (money % LOTTO_PRICE != 0) {
            View.printNotLottoPrice(LOTTO_PRICE);
            throw new IllegalArgumentException();
        }
    }
}

하지만 우리는 돈을 받아서 로또를 몇장 발행해야 할지 알아야 하는데 그 정보가 없다.
해당 정보는 Controller에 있었다.

심지어 Money 객체를 만들어 놓고 getMoney()로 int 값을 리턴했다;; 뿌직💩

로또 발행 개수를 확인하는 일을 굳이 Controller에서 굳이 getter로 받아야 할까?
그리고 ControllerMoney의 금액을 직접적으로 알 필요가 있을까?

public class Controller {

    private int getMoney() {
        View.printInputMoney();
        Money money = new Money(PlayerInput.getInteger());  // Money 객체 만들고
        return money.getMoney(); // int을 리턴????????
    }

    private int getLottoAmount(int money) { // 금액에 따른 로또 발행 개수 계산
        return money / Money.getLottoPrice();
    }
}

리팩토링

Money에 상태값이 있으니 Money에서 확인하면 되지 않을까?
getter 없이도 이렇게 간단하게 확인이 가능하다.


public class Money {

    public static final int LOTTO_PRICE_UNIT = 1000;

    private final int money;

    public Money(int money) {
        ...
        this.money = money;
    }

    public int getLottoCount() {
        return money / LOTTO_PRICE_UNIT;
    }
}

⭕️ 객체에 메시지 던지기

이전 코드

4개월 전 Lotto 클래스와 Service 클래스의 코드이다.
Lotto 클래스도 마찬가지로 유효성 검증 외에 별다른 메서드가 없다.

public class Lotto {

    private final List<Integer> numbers;

    public Lotto(List<Integer> numbers) {
        validateNumber(numbers);
        ...
        this.numbers = numbers;
    }

    public static void validateNumber(int number) {
        //로직
    }
}

객체에서 확인할 수 없는 로직들이 많다보니 Service는 정보 확인을 위해
필드 변수를 많이 받았다.

public class Service {
	
    private final int lottoAmount;
    private final List<List<Integer>> lottoNumbers;
    private final List<Integer> winningLottoNumbers;
    private final int winningBonusNumber;
	
    // Service에서 객체 로직을 확인해야 하니 주입받아야 하는 필드가 늘어난다.
    public Service(int lottoAmount, List<List<Integer>> lottoNumbers, List<Integer> winningLottoNumbers,
                   int playerBonusNumber) {
        this.lottoAmount = lottoAmount;
        ...
    }

그리고 4가지 로직을 구현했다.

  1. 구매한 로또 중 1~5등에 당첨된 로또 개수를 순서대로 반환함
  2. 각 로또의 당첨 등수를 반환함
  3. 2등일 경우 보너스 번호 일치 여부를 반환함
  4. 당첨 번호와 로또 번호를 비교해 일치하는 개수를 반환함
    public int[] getLottoResult() {
        // 구매한 로또 중 1~5등에 당첨된 로또 개수를 순서대로 반환함
    }

    private int checkRank(List<Integer> lottoNumbers, List<Integer> playerLottoNumbers, int playerBonusNumber) {
        // 각 로또의 당첨 등수를 반환함
    }

    private boolean winBonusNumber(List<Integer> lottoNumbers, List<Integer> playerLottoNumbers,
                                   int playerBonusNumber) {
        // 2등일 경우 보너스 번호 일치 여부를 반환함
    }

    private int getMatchedCount(List<Integer> lottoNumbers, List<Integer> playerLottoNumbers) {
        // 당첨 번호와 로또 번호를 비교해 일치하는 개수를 반환함
    }
}

코드를 보면 이런 의문이 든다.
4개의 메서드를 꼭 Service에서 구현해야 할까?
3번 4번 메서드의 경우 당첨 번호라는 객체에 메시지를 던질 수 있진 않을까?

예를 들면 이런 것이다.

"당첨 번호야 내가 로또 번호를 줄테니 너랑 몇 개가 일치하는지 알려줄래?"
"당첨 번호야 내가 보너스 번호를 줄테니 일치하는지 알려줄래?"

코드로 살펴보자.


리팩토링

일단 당첨 번호에게 물어보기로 했으니 당첨 번호 객체를 만들었다.

/**
 * 보너스 번호를 포함한 로또 당첨 번호를 의마하는 객체
 */
public class WinningLotto {

    private final Lotto lotto;
    private final LottoNumber bonusNumber;

    public WinningLotto(Lotto lotto, LottoNumber bonusNumber) {
        validateBonusNumber(lotto, bonusNumber);
        this.lotto = lotto;
        this.bonusNumber = bonusNumber;
    }

    private void validateBonusNumber(Lotto lotto, LottoNumber bonusNumber) {
        if (lotto.contains(bonusNumber)) {
            throw new IllegalArgumentException();
        }
    }

그리고 객체에게 물어볼 메시지도 구현했다.

    /**
     * 구매한 로또 번호와 비교해 몇개의 번호가 일치하는지 확인하는 메서드
     * @param compare 구매한 로또 번호
     * @return 일치하는 개수
     */
    public int countMatches(Lotto compare) {
        return lotto.countMatches(compare);
    }
	
    /**
     * 구매한 로또 번호의 보너스 번호가 일치하는지 확인하는 메서드
     * @param compare 구매한 로또 번호
     * @return 일치 여부
     */
    public boolean matchesBonusNumber(Lotto compare) {
        return countMatches(compare) == Ranking.RANK2.winningCondition
                && lotto.contains(bonusNumber);
    }
}

이렇게 객체에게 물어보면 굳이 Service에서 getter로 Lotto 값을 받아온다던지
생성자에서 Lotto를 주입받아 사용할 필요가 없다.

그리고 자연스럽게 getter를 사용하는 문제필드 변수가 많은 문제도 해결이 되었다.


🔄 해결된 고민과 추가된 고민

공부를 하면서, 또 다양한 경험을 하면서 이전에 했던 고민이 해결된 반면
또 추가된 고민이 있다.

이번에 로또 미션을 하면서 생긴 추가적인 고민은 다음과 같다.

  • 당첨 결과를 저장하는 Result 클래스를 만들었는데, 각 로또의 등수는 Lotto가 상태 정보로 갖고 있어야 되나...?
    • 그렇게 되면 1급 컬렉션의 조건이 파괴 되는데...
  • 객체를 그냥 감싸기만 하는(로직이 없는) 1급 컬렉션이 생겼는데, 굳이 필요있을까?
    • 아직 DTO에 대해 모르지만 DTO 역할을 하는 것처럼 보인다.
  • 값을 래핑하면서 안의 값을 확인하기 위해서는 계속 메서드를 타고 들어가야 해서 여러 클래스에 로직만 전달하는 메서드가 있다. 이게 맞는 것일까...?
  • View에서 원시 값을 전달해줘야 하나? 아님 객체로 만들어서 리턴해주는 것이 좋을까?
  • 수익률을 계산하는 메서드는 Result 클래스가 책임지는 것이 좋을까 아니면 Service에서?
    • 어디서 책임지든 Money에서 getter로 값을 가져와야 한다.
    • 그러면 수익률은 Money에서 책임져야 하는 것일까?

앞으로 또 열심히 공부해서 새로 생긴 고민을 해결해야겠다.

profile
공부는 마라톤이다. 한꺼번에 많은 것을 하다 지치지 말고 조금씩, 꾸준히, 자주하자.

3개의 댓글

comment-user-thumbnail
2023년 3월 23일

고민하신 부분 인상 깊게 잘 봤습니다!!

1개의 답글
comment-user-thumbnail
2023년 3월 27일

퍼가요~

답글 달기