📌 미션 - 로또

🔍 진행 방식

  • 미션은 기능 요구 사항, 프로그래밍 요구 사항, 과제 진행 요구 사항 세 가지로 구성되어 있다.
  • 세 개의 요구 사항을 만족하기 위해 노력한다. 특히 기능을 구현하기 전에 기능 목록을 만들고, 기능 단위로 커밋 하는 방식으로 진행한다.
  • 기능 요구 사항에 기재되지 않은 내용은 스스로 판단하여 구현한다.

🚀 기능 요구 사항

로또 게임 기능을 구현해야 한다. 로또 게임은 아래와 같은 규칙으로 진행된다.

1
2
3
4
- 로또 번호의 숫자 범위는 1~45까지이다.
- 1개의 로또를 발행할 때 중복되지 않는 6개의 숫자를 뽑는다.
- 당첨 번호 추첨 시 중복되지 않는 숫자 6개와 보너스 번호 1개를 뽑는다.
- 당첨은 1등부터 5등까지 있다. 당첨 기준과 금액은 아래와 같다. - 1등: 6개 번호 일치 / 2,000,000,000원 - 2등: 5개 번호 + 보너스 번호 일치 / 30,000,000원 - 3등: 5개 번호 일치 / 1,500,000원 - 4등: 4개 번호 일치 / 50,000원 - 5등: 3개 번호 일치 / 5,000원
  • 로또 구입 금액을 입력하면 구입 금액에 해당하는 만큼 로또를 발행해야 한다.
  • 로또 1장의 가격은 1,000원이다.
  • 당첨 번호와 보너스 번호를 입력받는다.
  • 사용자가 구매한 로또 번호와 당첨 번호를 비교하여 당첨 내역 및 수익률을 출력하고 로또 게임을 종료한다.
  • 사용자가 잘못된 값을 입력할 경우 throw문을 사용해 예외를 발생시키고, “[ERROR]”로 시작하는 에러 메시지를 출력 후 종료한다.

입출력 요구 사항

입력

  • 로또 구입 금액을 입력 받는다. 구입 금액은 1,000원 단위로 입력 받으며 1,000원으로 나누어 떨어지지 않는 경우 예외 처리한다.
1
14000
  • 당첨 번호를 입력 받는다. 번호는 쉼표(,)를 기준으로 구분한다.
1
1,2,3,4,5,6
  • 보너스 번호를 입력 받는다.
1
7

출력

  • 발행한 로또 수량 및 번호를 출력한다. 로또 번호는 오름차순으로 정렬하여 보여준다.
1
2
3
4
5
6
7
8
9
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
3개 일치 (5,000원) - 1개
4개 일치 (50,000원) - 0개
5개 일치 (1,500,000원) - 0개
5개 일치, 보너스 볼 일치 (30,000,000원) - 0개
6개 일치 (2,000,000,000원) - 0개
  • 수익률은 소수점 둘째 자리에서 반올림한다. (ex. 100.0%, 51.5%, 1,000,000.0%)
1
총 수익률은 62.5%입니다.
  • 예외 상황 시 에러 문구를 출력해야 한다. 단, 에러 문구는 “[ERROR]”로 시작해야 한다.
1
[ERROR] 로또 번호는 1부터 45 사이의 숫자여야 합니다.

실행 결과 예시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
구입금액을 입력해 주세요.
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%입니다.

🎯 프로그래밍 요구 사항

  • Node.js 14 버전에서 실행 가능해야 한다. Node.js 14에서 정상적으로 동작하지 않을 경우 0점 처리한다.
  • 프로그램 실행의 시작점은 App.js의 play 메서드이다. 아래와 같이 프로그램을 실행시킬 수 있어야 한다.

예시

1
2
const app = new App();
app.play();
  • package.json을 변경할 수 없고 외부 라이브러리(jQuery, Lodash 등)를 사용하지 않는다. 순수 Vanilla JS로만 구현한다.
  • JavaScript 코드 컨벤션을 지키면서 프로그래밍 한다
  • 프로그램 종료 시 process.exit()를 호출하지 않는다.
  • 프로그램 구현이 완료되면 ApplicationTest의 모든 테스트가 성공해야 한다. 테스트가 실패할 경우 0점 처리한다.
  • 프로그래밍 요구 사항에서 달리 명시하지 않는 한 파일, 패키지 이름을 수정하거나 이동하지 않는다.
  • indent(인덴트, 들여쓰기) depth를 3이 넘지 않도록 구현한다. 2까지만 허용한다.
    • 예를 들어 while문 안에 if문이 있으면 들여쓰기는 2이다.
    • 힌트: indent(인덴트, 들여쓰기) depth를 줄이는 좋은 방법은 함수(또는 메서드)를 분리하면 된다.
  • 함수(또는 메서드)가 한 가지 일만 하도록 최대한 작게 만들어라.
  • Jest를 이용하여 본인이 정리한 기능 목록이 정상 동작함을 테스트 코드로 확인한다.

추가된 요구 사항

  • 함수(또는 메서드)의 길이가 15라인을 넘어가지 않도록 구현한다.
    • 함수(또는 메서드)가 한 가지 일만 잘 하도록 구현한다.
  • else를 지양한다.
    • 힌트: if 조건절에서 값을 return하는 방식으로 구현하면 else를 사용하지 않아도 된다.
    • 때로는 if/else, switch문을 사용하는 것이 더 깔끔해 보일 수 있다. 어느 경우에 쓰는 것이 적절할지 스스로 고민해 본다.
  • 도메인 로직에 단위 테스트를 구현해야 한다. 단, UI(Console.readLine, Console.print) 로직에 대한 단위 테스트는 제외한다.
    • 핵심 로직을 구현하는 코드와 UI를 담당하는 로직을 구분한다.
    • 단위 테스트 작성이 익숙하지 않다면 tests/LottoTest.js를 참고하여 학습한 후 테스트를 구현한다.

라이브러리

  • MissionUtils 라이브러리에서 제공하는 Random 및 Console API를 사용하여 구현해야 한다.
    • Random 값 추출은 MissionUtils 라이브러리의 Random.pickUniqueNumbersInRange()를 활용한다.
    • 사용자의 값을 입력 받고 출력하기 위해서는 MissionUtils 라이브러리에서 제공하는 Console.readLine, Console.print를 활용한다.

사용 예시

1
const numbers = MissionUtils.Random.pickUniqueNumbersInRange(1, 45, 6);

Lotto 클래스

  • 제공된 Lotto 클래스를 활용해 구현해야 한다.
  • numbers의 # prefix를 변경할 수 없다.
  • Lotto에 필드를 추가할 수 없다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Lotto {
#numbers;

constructor(numbers) {
this.validate(numbers);
this.#numbers = numbers;
}

validate(numbers) {
if (numbers.length !== 6) {
throw new Error();
}
}

// TODO: 추가 기능 구현
}

회고

🐥 당첨 번호 및 로또 번호 예외 처리

  • 당첨번호 에러 처리할 때 중복검사 뿐만 아니라 범위도 확인해줘야 한다. 게다가 보너스 번호는 당첨번호와 중복 검사도 추가로 해줘야 한다.

🦉 typeof NaN === ‘number’

  • “4r”이라는 문자열을 숫자로 형변환 해주면 NaN 값이 나오게된다. 에러처리를 할 때 type이 ‘number’인지를 확인해주도록 하였는데, NaN은 number 타입을 가지고 있으므로 이에 대한 처리를 추가해줘야했다.

🦆 하드코딩 vs 소프트코딩

  • score 객체에 3등 4등 5등 의 키 값은 숫자로만 주었는데, bonus 등수는 문자열로 주어서 출력시 5등 4등 3등 2등 1등 Bonus등수가 출력되어서 이를 해결하기 위해 score의 key 값을 문자열로 변경하였다.

  • lotto 배열의 요소도 [1, 2, 3, 4, 5, 6] 이런 형태의 배열인데, print 결과물이 “[1, 2, 3, 4, 5, 6]” 이렇게 나와야 하므로 이에 대한 처리를 해주었다.

  1. 백틱만 사용
1
this.Lotto.forEach((lotto) => MissionUtils.Console.print(`${lotto}`));

Error1

  1. 문자열에 [ ] 삽입
1
this.Lotto.forEach((lotto) => MissionUtils.Console.print(`[${lotto}]`));

Error2

  1. 문자열 배열처럼 처리
1
2
3
this.Lotto.forEach((lotto) =>
MissionUtils.Console.print(`[${lotto.join(", ")}]`)
);

Error3

🐒 숫자를 천 단위 콤마찍은 문자열로 변환하기

  • Number.prototype.toLocaleString() 메서드를 사용하면 천단위에 ,를 찍은 문자열로 변환한다.

✏️ 피드백 (22.11.17 추가)

비즈니스 로직과 UI로직 구분

  • 한 클래스에서 담당하지 않고 구분하여야 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
class Lotto {
#numbers

// 로또 숫자가 포함되어 있는지 확인하는 비즈니스 로직
contains(numbers) {
...
}

// UI 로직
print() {
...
}
}

객체의 상태 접근 제한

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 내 코드
class App {
constructor() {
this.LottoCount = null;
this.Lotto = [];
this.winNum = [];
this.bonusNum = null;
this.score = { "3개": 0, "4개": 0, "5개": 0, bonus개: 0, "6개": 0 };
this.prize = {
"3개": 5000,
"4개": 50000,
"5개": 1500000,
bonus개: 30000000,
"6개": 2000000000,
};
this.totalMoney = null;
}
}
  • 위 와 같은 경우 객체의 데이터를 외부에서 접근 가능하므로 이는 디버깅과 유지보수를 어렵게하므로 접근 제한자를 설정하고 선택된 메서드만으로 데이터를 참조하거나 변경할 수 있도록 개선한다.

객체는 객체답게 사용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Lotto {
#numbers

constructor(numbers) {
this.#numbers = numbers
}

getNumbers() {
return this.#numbers
}
}

class LottoGame {
play() {
const lotto = new Lotto(...)

// 숫자가 포함되어 있는지 확인한다.
lotto.getNumbers().contains(number)
// 당첨 번호와 몇 개가 일치하는지 확인한다.
lotto.getNumbers().stream()...
}
}
  • 로또 클래스는 로또 숫자를 가져오는 역할만 한다.
  • 로또게임 클래스는 play() 메서드를 호출 하였을 경우 이와 관련된 로직으로 구성한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Lotto {
#numbers

constructor(numbers) {
this.#numbers = numbers
}

contains(number) {
// 숫자가 포함되어 있는지 확인한다.
return ...
}

matchCount(other) {
// 당첨 번호와 몇 개가 일치하는지 확인한다.
return ...
}
}

class LottoGame {
play() {
const lotto = new Lotto(...)

lotto.contains(number)
lotto.matchCount(...)
}
}
  • getter를 사용하여 값을 꺼내 사용하는 대신 다른 객체에 메시지를 건네주자. 객체가 스스로 일하도록 하는 객체 지향 프로그래밍 기법을 사용하자.
  • 출력을 위한 값, 순수 값 프로퍼티를 가져오기 위해서라면 getter를 허용한다.

필드 수를 줄이도록 하자

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 리팩터링 전
class LottoResult {
#result = new Map()
#profitRate
#totalPrize
}

// 리팩터링 후
class LottoResult {
#result = new Map()

calculateProfitRate() { ... }

calculateTotalPrize() { ... }
}
  • 위 객체의 profitRate와 totalPrize는 등수 별 당첨 내역(result)만 있어도 모두 구할 수 있는 값이다. 따라서 위 객체는 다음과 같이 하나의 필드만으로 구현할 수 있다.

🏓 소감

2주차때 클래스구조와 테스트에 대해서 조금은 익숙해져서 3주차 미션을 받았을 때 구조가 비슷하여 조금 마음이 놓였다.

  • jest expect 함수와 matcher 함수를 찾아보면서 단위 테스트 기능에 대해 공부해볼 수 있는 기회여서 좋았다.
  • 로또 클래스와 비즈니스 로직 클래스를 구분하여 테스트를 진행하고 관심사를 분리하면서 개발을 하니 유지보수가 편리하다는 것을 느낄 수 있었다.