로또 게임 기능을 구현해야 한다. 로또 게임은 아래와 같은 규칙으로 진행된다.
로또 번호의 숫자 범위는 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원
throw
문을 사용해 예외를 발생시키고, "[ERROR]"로 시작하는 에러 메시지를 출력 후 종료한다.__tests__/LottoTest.js
를 참고하여 학습한 후 테스트를 구현한다.1. 클래스(객체)를 분리하는 연습
2. 도메인 로직에 대한 단위 테스트를 작성하는 연습
이번 주차에는 도메인 로직에 대한 단위 테스트를 연습하는 것이 주요 목표 중 하나이다.
처음 이 목표를 봤을 때 가장 먼저 든 생각은 도메인 로직이 뭐지? 라는 생각이었다.
도메인 로직이라는 말이 잘 와닿지 않아서 용어를 구글링 해본 결과, 프로그래밍으로 해결하고자 하는 주제에 대한 영역
이라고 한다.
그러나 이 말도 조금 추상적이라서 나에게 확 와닿지 않았고, 조금 더 찾아본 결과 현실과 가까운 영역
, 소프트웨어의 성능 보다는 하나의 행위에 대한 로직
을 의미한다고 이해했다.
UI(입출력), DB, 통신과 같은 로직은 제외하고 순수한 비즈니스 영역에 대한 솔루션 로직을 의미한다고 한다.
예를 들어, 쇼핑몰을 개발 한다고 할 때, 상품 주문, 장바구니 상품 금액 계산과 같은 로직이 쇼핑몰의 도메인 로직이 되고,
모바일 송금의 경우 계좌 잔액 확인, 수수료 계산 등이 도메인 로직이 될 수 있다.
도메인에 대해 조금은 알 것 같다.
그렇다면, 이번 주차에서 구현해야 하는 로또 애플리케이션의 도메인 로직은 어떻게 될까?
- 입력받은 금액에 따른 로또 생성
- 입력받은 당첨 번호와 보너스 번호로 당첨 로또 생성
- 당첨 로또와 플레이어가 구매한 로또를 비교하여 등수 계산
- 구매 금액과 총 상금을 비교하여 수익률 계산
내가 생각한 로또 앱의 큰 도메인 로직은 위와 같다.
크게 4개의 큰 로직을 따라 애플리케이션이 실행된다고 생각했다.
아직 도메인 로직에 대해 많이 어설픈 상태이기 때문에 위 목록처럼 나누는게 맞는지는 모르겠다.
혹시 다른 의견이 있다면 댓글로 남겨주시면 좋을 것 같다.
각 도메인 별로 테스트 파일을 만들었다.
로또 생성에 대한 단위 테스트이다.
로또를 생성할 때 비정상적인 번호가 입력 될 경우의 에러 처리들을 테스트한다.
입력받은 금액에 따른 로또 생성 도메인에 대한 테스트이다.
비정상적인 금액이 입력될 경우 에러 처리를 테스트하고,
정상적인 금액이 입력될 경우 금액에 맞는 개수의 로또가 생성 되는지 테스트한다.
입력 받은 당첨 번호와 보너스 번호로 당첨 로또 생성 도메인에 대한 테스트이다.
당첨 번호 혹은 보너스 번호가 비정상적일 경우 에러 처리를 테스트한다.
당첨 로또와 플레이어의 로또들을 비교해서 등수 계산을 하는 도메인에 대한 테스트이다.
로또 배열과 당첨 로또 객체를 비교해서 올바른 등수 횟수들을 담은 배열을 반환하는지 테스트한다.
구매 금액과 총상금을 비교해서 수익률 계산 도메인에 대한 테스트이다.
구매 금액과 등수를 담은 배열을 입력 후 수익률이 정상적으로 나오는지 테스트한다.
지난 주차에서 따로 학습했던 클래스가 이번 주차의 주요 목표로 제시되었다.
지난 주차에는 자바스크립트 클래스의 이론을 주로 공부했는데, 이번 주차는 클래스를 직접 구현해가며 학습 할 수 있다는 점이 기대가 되었었다.
단순히 잘게 쪼개기만 했던 지난 주차의 함수 분리와는 달리 클래스 분리는 하나의 객체를 만드는 것이므로, 객체를 나누는 기준을 세워야 했다.
뿐만 아니라 클래스 명이나 클래스 변수들을 객체의 의도에 맞도록 잘 설계를 해야했다.
모니터 앞에서 가만히 고민하는 시간이 함수 분리를 할 때 보다 훨씬 길었다.
클래스명이나 클래스의 구조를 계속해서 수정했고, 수정했던 구조를 다시 예전 구조로 롤백하기도 했다.
또한, 하나의 클래스를 계속 나누기도 하고 병합하기도 했다.
그러다보니 커밋 수가 이전 주차보다 엄청 많아졌고, 코드를 객체 지향적으로 잘 짜는 것은 정말 많은 고민이 필요하다는 것을 이번 주차에서 많이 느꼈다.
도메인 단위 별로 하나의 클래스를 만들고자 했다.
사실 위에 작성된 도메인은 최신 도메인이고, 초기의 도메인과는 약간의 차이가 있다.
초기 도메인은 다음과 같았다.
- 로또 구매 및 생성
- 당첨 로또 생성
- 당첨 통계 계산
위와 같이 세개의 도메인을 중심으로 클래스들을 구성했다.
- LottoMachine class
- WinningLotto class
- WinningCalculator class
그러나 위 클래스 목록은 각 클래스마다 덩치가 크다고 느껴졌다.
로또 구매 및 생성 도메인의 경우 ‘금액 입력’과 ‘로또 생성’ 기능이 같이 있었고,
당첨 통계는 ‘등수 계산’, ‘상금 계산’ 기능이 같이 있었다.
나는 이 클래스을 더 세분화하고 싶었다.
세분화한 도메인은 다음과 같았다.
- 구매 금액 저장
- 금액에 따른 로또 생성
- 당첨 로또 생성
- 당첨 로또와 플레이어가 구매한 로또를 비교하여 등수 계산
- 등수를 통해 총 상금 계산
최대한 도메인 당 하나의 기능을 하도록 했고 총 5개의 도메인이 분리되었다.
다섯개의 도메인은 다섯개의 클래스로 표현되었다.
- LottoSeller class
- LottoGenerator class
- WinningLotto class
- RankCaculator class
- PrizeCalculator class
하지만 이 클래스 분리도 아쉬운 점이 존재했다.
LottoSeller 클래스가 독립된 하나의 클래스로 존재하기에는 기능(구매 금액 저장)이 너무 부실해보였다.
이 점에 대해 계속해서 고민을 했고, 결국 구매 금액 저장과 로또 생성은 하나의 연결된 흐름이므로 하나의 클래스에 저장되는 것이 맞다고 판단했다.
또한, PrizeCalculator(상금 계산기) 클래스의 클래스명도 조금 이상하다고 생각했다.
최종적으로 구해야 하는 데이터는 수익률인데, 수익률 계산기가 아닌 상금 계산기라고 하는 점을 바꾸고 싶었다.
고민 끝에 결정된 최종 도메인은 다음과 같다.(1번 목차의 도메인과 같음)
- 입력받은 금액에 따른 로또 생성
- 입력받은 당첨 번호와 보너스 번호로 당첨 로또 생성
- 당첨 로또와 플레이어가 구매한 로또를 비교하여 등수 계산
- 구매 금액과 총 상금을 비교하여 수익률 계산
도메인에 따른 최종 클래스 분리는 다음과 같다.
- LottoMahcine class
- WinningLotto class
- RankCaculator class
- YieldCalculator class
LottoSeller 클래스와 LottoGenerator 클래스를 합쳐서 다시 LottoMachine 클래스로 롤백했다.
그리고 PrizeCalculator 클래스명도 클래스의 의도에 맞게 YieldCalculator(수익률 계산기)로 바꿨다.
이전 주차의 공통 피드백을 모두 지키려고 노력했다.
그 중에서 특히 더 신경 쓴 피드백들은 다음과 같다.
이전 주차까지는 docs 폴더의 README에 기능목록만 작성했었다.
그러나 저번 주 공통 피드백에서 저장소의 README를 상세히 해보라는 피드백이 있었다.
Velog 글 작성을 위해 기존에 학습했던 마크다운 문법을 이번에 열심히 적용해봤다.
또한 마크 다운으로 표현 불가능한 스타일(가운데 정렬 등)은 html 태그를 사용해서 작성했다.
README를 작성하면서 이 미션을 참여하지 않는 사람도 쉽게 이해할 수 있도록 게임 방법을 상세히 작성하려고 노력했고,
각 클래스 별 생성자와 메소드의 사용법 또한 작성했다.
작성한 README는 여기서 볼 수 있다.
사실, 값들의 상수 처리는 이전 주차에 적용 했었지만, 제출 전 예기치 못한 오류로 인해 다 지워버렸다 ㅠ
그렇기 때문에 저번 주차에 지워버렸던 아쉬움을 만회하기 위해 이번 주차에 더 신경써서 적용하려고 했다.
LOTTO_LENGTH: 6,
MAX_NUMBER: 45,
MIN_NUMBER: 1,
MATCH_COUNT: {
FIRST_RANK: 6,
SECOND_RANK: 5,
THIRD_RANK: 5,
FORTH_RANK: 4,
FIFTH_RANK: 3,
},
PRIZE_AMOUNT: {
FIRST_RANK: 2000000000,
SECOND_RANK: 30000000,
THIRD_RANK: 1500000,
FORTH_RANK: 50000,
FIFTH_RANK: 5000,
},
MATCH_COUNT와 PRIZE_AMOUNT 처럼 여러 값이 필요한 상수는 객체를 사용해 여러 값을 저장했다.
일반적인 문자열의 경우 상관이 없었지만, 문자열 중간에 동적인 값을 넣고 싶을 때 어떻게 상수로 저장할지 고민에 빠졌다.
고민 끝에 다음과 같은 방법으로 상수를 저장했다.
PURCHASE_LOTTO(number) {
return `${number}개를 구매했습니다.`;
},
RATE_OF_RETURN(rateOfReturn) {
return `총 수익률은 ${rateOfReturn.toLocaleString()}%입니다.`;
},
위와 같이 함수를 사용해서 상수들을 저장했다.
함수를 사용하니 중간에 값을 넣은 문자열을 가져올 수 있었다.