[우아한테크코스 2022] 프리코스 3주차 풀어보기

재오·2023년 8월 15일
8
post-thumbnail

3주차

3주차 과제 미션은 로또 게임 구현이다. 이번 주차에서는 조금 더 다양한 방법을 이용해서 문제를 해결해볼 생각이다. 아래에 나오는 목차는 개인적인 생각으로 순차적으로 진행된다면 조금 더 쉽게 해결된다는 필자의 판단으로 적은 것이니 참고하면 좋겠다.

📑 기능구현 목록 작성

2주차 프리코스 숫자야구에서는 기능구현을 작성할 때 UI 로직을 포함하여 입력값, 출력값 등을 구체적으로 적었었다. 하지만 그렇게 작성하는 것보다는 해당 미션에서 필요한 기능들과 필요한 에러처리 정도만 리스트로 나열하고 해당 기능을 구현했으면 체크하는 방식으로 간단하게 작성하였다.


여기서 중요한 점은 기능구현 목록을 작성할 때, 단순하게 기능목록만 작성하는 것이 아니라 Util 클래스와 MVC 패턴을 이용하는 것이다. Util 클래스 적용MVC 패턴에 대해서는 아래에서 자세히 설명할 예정이다.

이전에도 말한 바 있지만 기능 구현 목록을 작성하는 것에 있어 시간을 많이 쓰는 것을 아까워하면 안될 것 같다. UI 로직을 짜고 코드를 작성하려고 할 때 여기서 꼬여버리면 뒤에서 시간 낭비가 꽤 심하기 때문이다.

만약 최종 테스트에 간다면...여기서는 구현 목록 짜는데 많은 시간을 쓰면 안되겠지만...연습을 많이 해야겠다.

🧱 Static 상수화

2차 미션에서도 언급했듯이 상수화는 유지 및 보수하는데 있어서 상당히 유용하다. 하드코딩으로 파일마다 넣다보면 관리하는데 있어서도 어렵고 가독성 측면에서도 좋지 않다.

가장먼저 고정적인 상수와 에러의 종류가 무엇인지를 꼼꼼하게 파악한다.

그리고 해당 조건에 맞는 상수화 작업이 이루어지면 된다. 이 작업은 그렇게 오래 걸리지 않아야 한다.

📁 Util 클래스 적용

이번 미션에서 처음으로 Util 클래스를 적용해 보았다.

Util 클래스란 문자열 관련, 랜덤값 생성, 날짜 혹은 시간 등 프로젝트 전역에서 사용되는 특정 로직이나 독립적인 기능을 구현한 클래스를 의미한다.

예를 들어 랜덤값을 만드는 경우 RandomUtil을 만들 수 있다. 그 안에는 랜덤값을 구하는 여러 개의 메서드들이 들어 있을 수 있다. 이런 기능들은 어떤 상태를 가지고 있기 않기 때문에 static으로 선언되어 사용된다.

정말 주의해야 할 점은 Util 클래스를 만들 때에는 해당 기능이 어디서든 사용 가능해야 하기 때문에 이에 대한 기준이 명확해야 한다.

위에 기능목록에서 Util 클래스를 적용해야 하는 부분은 어디일까?

흔히 우리가 입력을 받고 출력을 하는 부분은 실제 게임이 진행되는 다른 클래스에서 적용하면 된다. Util 클래스에 포함되는 부분은 앞에서 설명한 것과 마찬가지로 랜덤값을 적용하거나, 순위를 반환하기 위해 번호를 카운트하고 같은 번호가 있는지 판단하는 기능과 같이 이후 다른 클래스에서도(전역적으로) 쓰일 수 있는 함수를 넣어두면 된다.

이번 과제에서 쓰인 Util 클래스 파일 몇개를 살펴보자.

📩 InputValidator

앞에 설명에서 말했던 것처럼 예외처리에 관한 Util 클래스를 하나 생성하는 것이 좋다. 어떤 상황일 때 예외처리를 해야하는지를 상황에 따라 함수별로 구체적으로 나눠놓는 것이 좋다.

이번 과제에서는 구매금액에 따른 예외처리, 당첨 번호에 따른 예외처리, 보너스 번호에 따른 예외처리 이렇게 세가지로 나눠서 코드를 작성하였다.

validateWinningNumbers(input) {
    const inputNumbers = input.split(",");

    if (inputNumbers.length !== StaticNumber.LOTTO_NUMBER_COUNT)
      throw new Error(ErrorString.WINNING_NUMBER_COUNT_ERROR);
    if (input.replace(/\d|\,/g, "").length > 0)
      throw new Error(ErrorString.WINNING_NUMBER_NOT_NUMBER_ERROR);
    if (inputNumbers.filter(InputValidator.rangeFilterFunction).length > 0)
      throw new Error(ErrorString.WINNING_NUMBER_OUT_OF_RANGE_ERROR);
    if (inputNumbers.length !== new Set(inputNumbers).size)
      throw new Error(ErrorString.WINNING_NUMBER_DUPLICATE_ERROR);
  }

💣 LottoNumberGenerator

랜덤 값을 생성 하는 것 역시 Util 클래스에 넣어두는 것이 좋다고 했었다. 이번 과제 역시 금액에 따라 여러 개의 숫자 배열로 이루어진 객체를 생성해야하므로 이 과정에서 랜덤 함수를 작성해주는 것이 좋다.

const LottoNumberGenerator = {
  generate() {
    return Random.pickUniqueNumbersInRange(
      StaticNumber.LOTTO_NUMBER_RANGE_START,
      StaticNumber.LOTTO_NUMBER_RANGE_END,
      StaticNumber.LOTTO_NUMBER_COUNT
    );
  },
};

🗃️ MVC 패턴 적용

여기까지가 기본적으로 이루어져야 하는 작업이다. 이 이후부터는 드디어 MVC 패턴에 대해서 적용해볼 것이다.

이후에 MVC 패턴에 관한 공부를 한 이후에 관련 포스트를 작성하고자 한다.
MVC 패턴에 좀더 구체적으로 적어놨으니 확인을 해보기를 바란다.

📕 Model

우선 Model에 포함될 내용에 대해서 생각해보기 전에 이번 미션에서 사용되는 에 대해 나열을 해봐야 한다.

  • 구매 금액
  • 구매금액으로 구매한 로또의 번호
  • 당첨 번호
  • 보너스 번호
  • 당첨 결과
  • 수익률

이렇게 크게는 6가지가 미션에 쓰일 예정이다. 하지만 해당 목록에 대해서 좀더 깊게 생각해보자.

  • 구매 금액 ➡️ [로또 개수와 정확히 비례하기 때문에 굳이 저장할 필요 X]
  • 구매금액으로 구매한 로또의 번호 ➡️ [로또 번호를 관리하기 위해서 저장 필요 O]
  • 당첨 번호 ➡️ [당첨된 번호를 관리하기 위해서 저장 필요 O]
  • 보너스 번호 ➡️ [보너스 번호를 관리하기 위해서 저장 필요 O]
  • 당첨 결과 ➡️ [로또 번호, 당첨 번호, 보너스 번호로 계산가능하기 때문에 굳이 저장할 필요 X]
  • 수익률 ➡️ [당첨 결과와 구매 금액으로 산출할 수 있기 때문에 저장할 필요 X]

따라서 로또 번호, 당첨 번호, 보너스 번호를 Model에 저장할 것이다. 주로 이 파일들을 domain이라는 폴더에 저장한다.

📗 View

구입금액을 입력해 주세요.
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%입니다.

위 내용은 과제 설명에 나온 예시의 일부이다. 이것을 바탕으로 UI로직을 정리해볼 수 있다.

  • 구입 금액을 입력받는 기능
  • 구입한 로또 개수를 출력하는 기능
  • 구입한 로또 번호를 출력하는 기능
  • 당첨 번호를 입력받는 기능
  • 보너스 번호를 입력받는 기능
  • 당첨 통계를 출력하는 기능

📥 InputView

const InputView = {
  readPurchasePrice(callback) {
    this.getUserInput(GuideString.INPUT_PURCHASE_PRICE, callback);
  },

  readWinningNumber(callback) {
    this.getUserInput(GuideString.INPUT_WINNING_NUMBER, callback);
  },

  readBonusNumber(callback) {
    this.getUserInput(GuideString.INPUT_BONUS_NUMBER, callback);
  },

  getUserInput(guide, callback) {
    Console.readLine(guide, (input) => {
      callback(input);
    });
  },
};

각 입력받는 기능을 getUserInput으로 묶어서 한번에 처리할 수 있도록 처리하였다. 그리고 콜백 함수와 연결을 지어줬다.

📤 OutputView

const OutputView = {
  printPurchaseAmount(amount) {
    Console.print(`${amount}개를 구매했습니다.`);
  },

  printLottos(lottos) {
    for (let i = 0; i < lottos.length; i++) {
      Console.print(`[${lottos[i].getSortedLottoNumber().join(", ")}]`);
    }
  },

  printRankStatistic(winningStatistic) {
    const raps = winningStatistic.length;

    for (let i = 0; i < raps; i++) {
      Console.print(
        StatisticString[i] +
          `${winningStatistic[StaticNumber.TOTAL_LOTTO_RANKS - 1 - i]}`
      );
    }
  },

  printRevenueRate(revenueRate) {
    Console.print(`총 수익률은 ${revenueRate}%입니다.`);
    Console.close();
  },
};

출력은 InputView와는 다르게 공통으로 Console.readLine을 쓰는 부분이 없기 때문에 각각 다른 함수로 출력할 수 있게끔 하였다.

총 수익률을 출력하는 부분은 로직의 가장 끝부분이기 때문에 Console.close()를 통해 종료할 수 있게끔 하였다.

📘 Controller

controller에서는 모델에 저장되어 있는 로또 객체 배열, 당첨 번호, 보너스 번호를 이용하여 UI로직에 맞게끔 구현하면 된다.

  • InputView에서 구매 금액 입력 메서드를 호출한다.
  • 입력에 따라 Lotto객체를 생성한다. ➡️ OutputView에서 구입한 로또 출력 메서드를 호출한다.
  • InputView에서 당첨 번호 입력 메서드를 호출한다.
  • InputView에서 보너스 번호 입력 메서드를 호출한다.
  • 당첨 통계 계산 메서드를 호출하고 이를 이용하여 OutputView에서 당첨 통계 출력 메서드를 호출한다.
  • 총 수익률 메서드를 호출하고 이를 이용하여 OutputView에서 총 수익률 출력 메서드를 호출한다.

🔐 클래스 private를 이용하여 보충

자세한 내용은 [모던 자바스크립트] 클래스에 적어두었습니다.

여기서 간략하게만 정리하자면 private 필드의 선두에는 #을 붙여준다. private 필드를 참조할 때에도 반드시 #을 붙여줘야 한다. 이 private 필드는 클래스 내부에서만 참조할 수 있고, 외부에서 직접 접근하는 방법은 존재하지 않는다.

이번 미션에 적용한 클래스 코드를 보여주자면 다음과 같다.

class LottoGameController {
  #lottos;
  #winningNumbers;
  #bonusNumber;
  
  inputWinningNumber() {
    InputView.readWinningNumber((input) => {
      this.#winningNumbers = new WinningNumbers(input);
      this.inputBonusNumber();
    });
  }

  inputBonusNumber() {
    InputView.readBonusNumber((input) => {
      this.#bonusNumber = new BonusNumber(
        input,
        this.#winningNumbers.getWinningNumbers()
      );

      this.displayRankStatistic();
    });
  }
}

🔎 느낀점

이번 미션에서는 클래스, MVC 패턴, props 객체를 이용하여 코드를 좀더 세분화하고 조직적으로 구성해보자 하였다. 아직 배워야 하는 부분도 많고 다듬어야 하는 부분도 많지만 저런 기술(?)들을 한번은 찍먹이라도 해본 것에 대해 의미를 두고싶다.

앞으로는 generate()를 이용해 한번 코드를 다듬어보고 싶고 무엇보다 아직은 props 객체에 대해 다루는 것에서 어색함을 느끼기 때문에 체화하는 것에 남은 시간을 쏟고 싶다. 하나의 또 욕심이라면 조금 더 빠른 시간에 기능 구현 목록과 UI로직을 작성하고 싶다.

profile
블로그 이전했습니다

4개의 댓글

comment-user-thumbnail
2023년 8월 15일

졸부되세요~!

1개의 답글
comment-user-thumbnail
2023년 8월 15일

감사합니다 도움이 많이되었습니다!

1개의 답글