🎯 주제
3주차 미션은 로또 문제였다. 구매 금액에 따른 로또 번호를 생성하고, 당첨 번호와 보너스 번호를 사용해 당첨 결과와 수익률을 출력하는 것이 목표였다. 또한, 이번 미션의 주요 학습 목표는 클래스(객체) 분리와 도메인 로직에 대한 단위 테스트 작성이었다. Jest를 활용한 테스트 코드 작성에 아직 익숙하지 않지만, 이번 미션을 통해 단위 테스트의 중요성과 장점을 체계적으로 이해하고자 한다.
👨🏻💻 공들인 부분
📄 기능 목록 작성하기
# 📄 기능 목록
- 입력 기능
- [ ] 구입 금액 입력받는 기능
- [ ] 당첨 번호를 입력받는 기능
- [ ] 보너스 번호를 입력받는 기능
- 출력 기능
- [ ] 1,000원 단위로 로또 수량 및 번호를 오름차순으로 출력하는 기능
- [ ] 당첨 내역을 출력하는 기능
- [ ] 총 수익률을 출력하는 기능
- 핵심기능
- [ ] 1개의 로또마다 1~45까지의 숫자를 랜덤으로 6개를 랜덤으로 오름차순으로 뽑는 기능
- [ ] 당첨 통계를 계산하는 기능
- [ ] 총 수익률을 계산하는 기능
# 🎯 예외 상황
- 구입 금액 입력
- [ ] 1,000으로 나누어 떨어지지 않는 경우
- [ ] 숫자가 아닌 값을 입력한 경우
- [ ] 값을 입력하지 않은 경우
- 당첨 번호 입력
- [ ] 쉼표 구분을 잘못 기입했을 경우
- [ ] 당첨번호가 6개가 아닌 경우
- [ ] 1~45의 값이 아닌 경우
- [ ] 숫자가 아닌 값을 입력한 경우
- [ ] 값을 입력하지 않은 경우
- 보너스 번호 입력
- [ ] 1~45의 값이 아닌 경우
- [ ] 숫자가 아닌 값을 입력한 경우
- [ ] 값을 입력하지 않은 경우
이번 미션에도 구현 전에 기능 목록을 세부적으로 작성하였다. 하지만, 클래스와 함수의 설계 및 구현 등과 같은 세부 사항은 과도하게 작성하지 않았다. 기능 목록은 구현 과정에서 언제든지 수정될 수 있음을 인지하고 있기 때문에, 이를 통해 개발의 효율성을 높이는 데 초점을 맞추었다. 구현 과정을 통해 수정된 최종 기능 목록은 다음과 같다.
# 📄 기능 목록
- 입력 기능
- [x] 구입 금액 입력받는 기능
- [x] 당첨 번호를 입력받는 기능
- [x] 보너스 번호를 입력받는 기능
- 출력 기능
- [x] 로또 수량 및 번호를 출력하는 기능
- [x] 당첨 내역을 출력하는 기능
- [x] 총 수익률을 출력하는 기능
- 핵심기능
- [x] 1~45까지의 숫자를 랜덤으로 6개를 랜덤으로 오름차순으로 뽑는 기능
- [x] 로또 번호를 생성하는 기능
- [x] 당첨 통계를 계산하는 기능
- [x] 일치하는 당첨번호의 개수를 계산하는 기능
- [x] 당첨번호 5개와 보너스 넘버가 일치하는 여부를 계산하는 기능
- [x] 메시지 결과를 스왑하는 기능
- [x] 당첨 통계 결과 메시지를 추가하는 기능
- [x] 총 수익률을 계산하는 기능
- [x] 당첨금을 배열에 추가하여 반환하는 기능
# 🎯 예외 상황
- 구입 금액 입력
- [x] 1,000으로 나누어 떨어지지 않는 경우
- [x] 숫자가 아닌 값을 입력한 경우
- [x] 값을 입력하지 않은 경우
- [x] 음수 값을 입력했을 경우
- 당첨 번호 입력
- [x] 쉼표 구분을 잘못 기입했을 경우
- [x] 당첨 번호가 6개가 아닌 경우
- [x] 1~45의 값이 아닌 경우
- [x] 숫자가 아닌 값을 입력한 경우
- 보너스 번호 입력
- [x] 1~45의 값이 아닌 경우
- [x] 숫자가 아닌 값을 입력한 경우
- [x] 값을 입력하지 않은 경우
- 로또 클래스
- [x] 로또 번호가 6개가 아닌 경우
- [x] 중복된 숫자가 있을 경우
큰 차이점으로는 핵심 기능을 추가하고, 그에 필요한 세부 기능들을 보강하였다. 또한, 테스트 과정에서 발견된 예외 상황들도 목록에 추가하였다. 이는 자연스럽게 우테코에서 강조하고 있는 ‘죽은 문서’가 아닌 '살아있는 문서'로 완성되어졌다.
기능 목록을 작성하는 것이 개발의 효율성을 극대화시키며, 타인이 내 코드를 쉽게 이해하고 설명할 수 있게 도와준다는 점을 항상 느낀다. 이러한 장점을 경험한 이후, 다른 개발을 시작할 때도 먼저 키보드에 손이 먼저 올라가는 것이 아닌 노트를 펼치고 목록부터 작성하게 되었다. 이는 매우 긍정적인 발전이라고 생각한다.
🏄♂️ 플로우 리스트 작성하기
이전에는 프로그램의 순서를 플로우 차트로 그렸다. 하지만, 이 과정이 생각보다 많은 시간을 차지했다. 그래서 플로우 차트를 대체할 다른 효율적인 방법을 찾았고, 그것이 바로 플로우 리스트 작성이었다. 작성한 플로우 리스트는 다음과 같다.
- 사용자로부터 구매 금액을 입력받는다.
- 사용자로부터 입력받은 구매금액을 통해 로또 번호를 생성한다.
- 생성한 로또 번호를 출력한다.
- 사용자로부터 당첨 번호를 입력받는다
- 사용자로부터 보너스 번호를 입력받는다.
- 사용자로부터 입력받은 당첨 번호와 보너스 번호를 통해 당첨금 결과를 생성한다.
- 생성한 당첨금 결과를 출력한다.
- 사용자로부터 입력받은 구매 금액과 당첨금 결과를 통해 총 수익률을 생성한다.
- 총 수익률을 출력한다.
이전에는 유효성 검사를 추가하였지만, 이미 view에서 유효성 검사를 진행하므로 세부적인 기능들은 생략하였다. 프로그램의 순서를 파악하기 위해 입력과 출력, 중간에 필요한 핵심 기능들을 기반으로 플로우를 리스트화하였다. 이런 방식은 프로그램의 순서를 쉽게 파악하고, 각 플로우 별로 함수를 구성하면서 controller를 작성하는 데 큰 도움이 되었다.
🚀 피드백 반영하기
🖌️ Airbnb 자바스크립트 스타일 가이드 준수하기
💡 Don’t use iterators. Prefer JavaScript’s higher-order functions instead of loops like for-in or for-of
Airbnb의 자바스크립트 스타일 가이드에 따르면, for문의 사용을 지양하고 고차함수를 활용하도록 권장하고 있다.
GitHub - airbnb/javascript: JavaScript Style Guide
JavaScript Style Guide. Contribute to airbnb/javascript development by creating an account on GitHub.
github.com
const numbers = [1, 2, 3, 4, 5];
// bad
let sum = 0;
for (let num of numbers) {
sum += num;
}
sum === 15;
// best (use the functional force)
const sum = numbers.reduce((total, num) => total + num, 0);
sum === 15;
이전 미션에서는 출력 함수에서 for문을 사용했었는데, 이번 미션에서는 이를 고차함수로 대체하려 노력하였다.
printResulString: (cars, count) => {
for (let i = 0; i < count; i += 1) {
Console.print(cars.map(car => driveAndStopCars(car)).join('\\n'));
Console.print('\\n');
}
}
예를 들어, 로또 번호를 랜덤으로 생성하는 함수를 특정 횟수만큼 반복 호출하는 코드에서 Array.from()과 같은 고차함수를 사용하여 반복 로직을 개선하였다.
// before
for(let i = 0; i < count; i++) {
this.#lottos.push(this.#generateLotto())
}
#generateLottos(count) {
Array.from({ length: count }, () => this.#lottos.push(this.#generateLotto()));
}
기존에는 카운트 변수 i를 0으로 초기화하고, 1씩 증가시키며 count 값이 될 때까지 반복문을 사용했을 것이다. 하지만 Array.from()과 같은 고차함수를 활용하면, length 속성의 값만큼 콜백함수 내부 로직을 순회하게 함으로써 이를 개선할 수 있었다.
🗂️ 클래스(객체) 분리하기
MVC 패턴을 통해 클래스와 객체들을 역할에 따라 분리했다. 상수들을 관리하는 constants, 프로그램 실행 로직을 관리하는 controller, 각 도메인 로직과 클래스를 관리하는 domain, I/O를 관리하는 view, 유틸 함수들을 관리하는 utils로 크게 분리했다. 특히, 클래스 내부 로직을 숨기고 외부 인터페이스를 통해서만 상호작용하도록 캡슐화에 신경을 썼다.
Constants
매직넘버를 제거하기 위한 여러 상수들과 에러 메시지, 입력 메시지들을 정의해 놓았다.
View
입/출력과 관련된 객체들을 구성하였다. 사용자와의 인터페이스를 처리하고 입력을 받아서 Controller 영역으로 전달하는 역할을 담당하였다.
Controller
로또 게임의 실행 로직을 작성하였다. 입력을 받아 도메인 영역의 클래스들을 활용하여 게임을 진행하고 결과를 반환하는 역할을 수행하였다.
Domain
로또 번호 생성 및 당첨 결과들과 총 수익률과 관련한 여러 도메인 클래스들을 생성하였다. 각자의 역할에 맞게 핵심 로직들을 구현하여 역할을 수행하도록 하였다.
Utils
유효성 검사와 관련된 객체를 구성하였다. 사용자의 입력에 해당하는 구매 금액과 당첨 번호, 보너스 번호의 유효성 검사 함수들을 정의하고 입력에 따른 유효성 검사를 진행하는 함수를 정의하여 InputView에서 호출하여 유효성 검사를 시행하도록 수행하였다.
1️⃣ Lotto
이번 미션에서는 기본으로 제공된 Lotto 클래스에 다른 필드를 추가하지 않는 요구사항이 있었다. 따라서, 이 클래스에서는 번호들을 받아 유효성 검사를 하고, 외부로 로또 번호를 반환하는 역할을 수행하는 클래스를 설계했다.
import Validator from '../utils/Validator.js';
class Lotto {
#numbers;
constructor(numbers) {
this.#validate(numbers);
this.#numbers = numbers;
}
#validate(numbers) {
Validator.validateLotto(numbers);
}
getNumbers() {
return this.#numbers;
}
}
export default Lotto;
2️⃣ Lottos
Lottos 클래스는 구매 금액에 따라 로또 번호를 생성하는 역할을 한다. 내부적으로 6개의 랜덤 숫자를 생성하여 Lotto 클래스의 인스턴스를 만들고, 인자로 받은 로또 번호 생성 횟수만큼 순회하여 lottos에 추가한다. 이후 외부에서는 lottos를 반환 받도록 함수를 작성했다.
import { Random } from '@woowacourse/mission-utils';
import CONSTANTS from '../constants/constants.js';
import Lotto from './Lotto.js';
class Lottos {
#lottos;
constructor(count) {
this.#lottos = [];
this.#generateLottos(count);
}
getLottos() {
return this.#lottos;
}
#generateLottos(count) {
Array.from({ length: count }, () => this.#lottos.push(this.#generateLotto()));
}
#generateRandomNumber(min, max, count) {
return Random.pickUniqueNumbersInRange(min, max, count).sort((a, b) => a - b);
}
#generateLotto() {
const randomNumber = this.#generateRandomNumber(
CONSTANTS.number.min,
CONSTANTS.number.max,
CONSTANTS.number.count,
);
return new Lotto(randomNumber).getNumbers();
}
}
export default Lottos;
3️⃣ WinningStatistics
WinningStatistics 클래스는 당첨 통계를 계산하고 메시지 결과를 생성하는 역할을 한다. 내부적으로 매치된 당첨 번호의 개수를 구하고 보너스 번호를 체크한 통계를 생성한다. 그리고 이 통계를 기반으로 상수화된 메시지들을 순회하여 메시지 통계 결과를 생성한다. 최종적으로 외부에서는 당첨 통계 또는 메시지 결과를 반환 받도록 메소드를 작성했다.
import CONSTANTS from '../constants/constants.js';
import MESSAGE from '../constants/message.js';
class WinningStatistics {
#winningStatistics;
#winningStatisticsString;
#lottos;
constructor(lottos, winningNumbers, bonusNumber) {
this.#lottos = lottos;
this.#winningStatistics = this.#generateWinningStatistics(
this.#lottos,
winningNumbers,
bonusNumber,
);
this.#winningStatisticsString = this.#generateWinningStatisticsString(this.#winningStatistics);
}
getWinningStatistics() {
return this.#winningStatistics;
}
getWinningStaticsString() {
return this.#winningStatisticsString;
}
#generateWinningStatistics(lottos, winningNumbers, bonusNumber) {
const matchCounts = this.#getMatchingNumbersCounts(lottos, winningNumbers);
return this.#findFiveMatchWithBonus(lottos, matchCounts, bonusNumber);
}
#generateWinningStatisticsString(matchCounts) {
const winningStatisticsString = Object.keys(MESSAGE.winningStatistics).map(key => {
const count = matchCounts.filter(winningstatistic => winningstatistic === Number(key)).length;
return `${MESSAGE.winningStatistics[key]}${count}개`;
});
this.#swapWinningStatisticsString(winningStatisticsString);
return winningStatisticsString;
}
#swapWinningStatisticsString(winningStatisticsString) {
const threeMatchValue = winningStatisticsString[CONSTANTS.match.threeMatch];
winningStatisticsString[CONSTANTS.match.threeMatch] =
winningStatisticsString[CONSTANTS.match.fourMatch];
winningStatisticsString[CONSTANTS.match.fourMatch] = threeMatchValue;
}
#getMatchingNumbersCounts(lottos, winningNumbers) {
return lottos.map(lotto => lotto.filter(item => winningNumbers.includes(item)).length);
}
#findFiveMatchWithBonus(lottos, matchCounts, bonusNumber) {
const matchWithBonusCounts = [];
const newMatchCounts = [...matchCounts];
newMatchCounts.forEach((element, index) => {
if (element === CONSTANTS.match.fiveMatch) {
matchWithBonusCounts.push(index);
}
});
matchWithBonusCounts.forEach(indice => {
if (lottos[indice].includes(bonusNumber))
newMatchCounts[indice] = CONSTANTS.match.fiveMatchWithBonus;
});
return newMatchCounts;
}
}
export default WinningStatistics;
4️⃣ RateOfReturn
RateOfReturn 클래스는 총 수익률을 계산하는 역할을 한다. 내부적으로 당첨 통계를 통해 당첨 금액을 반환하는 함수를 작성했다. 이 당첨 금액을 기반으로 구매 금액으로 총 수익률을 문자열로 반환하는 기능을 수행하도록 작성했다. 최종적으로 외부에서는 총 수익률을 반환 받도록 메소드를 작성했다.
import CONSTANTS from '../constants/constants.js';
class RateOfReturn {
#rateOfReturn;
constructor(winningStatistic, purchaseAmount) {
const returns = this.#genarateReturns(winningStatistic);
this.#rateOfReturn = this.#generateRateOfReturn(purchaseAmount, returns);
}
getRateOfReturn() {
return this.#rateOfReturn;
}
#generateRateOfReturn(purchaseAmount, returns) {
if (returns.length === CONSTANTS.number.zero) return '0%';
const finalValue =
Number(purchaseAmount) + returns.reduce((acc, currentReturn) => acc + currentReturn);
const rateOfReturn = (
((finalValue - purchaseAmount) / purchaseAmount) *
CONSTANTS.number.percentage
).toFixed(1);
return `${rateOfReturn}%`;
}
#genarateReturns(winningStatistics) {
return winningStatistics
.map(matchCount => CONSTANTS.prize[matchCount])
.filter(item => item !== undefined);
}
}
export default RateOfReturn;
😈 하드 코딩하지 않기(상수화 시키기)
문자열이나 숫자 등의 값을 하드 코딩하지 않고, 상수로 만들어 constants 폴더에 관리했다. 특히, 각 상수의 역할을 잘 드러내는 변수명을 사용하고, Airbnb 자바스크립트 스타일 가이드를 따랐다. 다음은 숫자 야구 미션 때 작성한 코드이다.
export const InputString = Object.freeze({
INPUT_USER_NUMBER: '숫자를 입력해주세요 : ',
INPUT_RESTART_NUMBER: `게임을 새로 시작하려면 1, 종료하려면 2를 입력하세요.\\n`,
});
export const OutputString = Object.freeze({
OUTPUT_GAME_START: '숫자 야구 게임을 시작합니다.',
OUTPUT_GAME_END: '3개의 숫자를 모두 맞히셨습니다! 게임 종료',
});
객체의 키를 대문자의 스네이크 케이스로 작성했던 예전과 달리, 이번에는 Airbnb에서 제안하는 대로 키를 무의미하게 대문자로 쓸 필요가 없다고 판단했다.
GitHub - airbnb/javascript: JavaScript Style Guide
JavaScript Style Guide. Contribute to airbnb/javascript development by creating an account on GitHub.
github.com
// bad - unnecessarily uppercases key while adding no semantic value
export const MAPPING = {
KEY: 'value'
};
// good
export const MAPPING = {
key: 'value',
};
따라서, 이번 미션에서는 상수들을 의미에 맞게 묶은 객체를 생성하고, Object.freeze()를 사용하여 값이 변경되지 않도록 관리했다.
const number = Object.freeze({
zero: 0,
count: 6,
min: 1,
max: 45,
percentage: 100,
});
const CONSTANTS = Object.freeze({
purchaseAmount,
number,
match,
prize,
});
export default CONSTANTS;
🤡 Jest를 통한 단위 테스트에 친숙해지기
이번 미션에서는 '도메인 로직'과 '단위 테스트'에 대한 정확한 이해가 부족했다. 따라서 이 두 가지 개념에 대해 자세히 알아보고, 어떻게 테스트를 작성해야 하는지 찾아보았다.
도메인 로직이란?
'도메인'은 '소프트웨어로 해결하고자 하는 문제'를 의미하며, 이를 해결하기 위한 코드 로직을 '도메인 로직'이라고 부른다. 이 외에 다른 외부 로직들은 '애플리케이션 서비스 로직'이라고 부른다. 따라서, 이번 미션에서 요구하는 핵심 기능이 무엇인지 찾아 도메인 로직을 구성하는 것이 중요하다고 판단하였다.
단위 테스트란?
단위 테스트(Unit test)'는 프로그래밍에서 소스 코드의 특정 모듈(메서드)이 의도된 대로 정확히 작동하는지 검증하는 절차이다. 즉, 작성한 모든 메서드들에 대한 테스트 케이스를 작성하는 것을 의미한다.
테스트 케이스 작성
즉, 이번 미션에서는 해결하고자 하는 궁극적인 문제에 해당하는 도메인을 찾고, 해당 로직(메서드)에 대한 테스트 케이스를 작성해야 한다는 결론을 내렸다.
먼저, 이번 미션에서의 도메인은 무엇인지 생각해보았다.
- 구매 금액 만큼의 로또 번호 생성
- 당첨 통계 계산
- 총 수익률 계산
이 세 가지를 해당 미션에서의 도메인이라 판단하고, 각각에 대한 메서드 구현과 테스트 케이스를 작성하였다. 먼저, 로또 번호를 랜덤으로 생성하는 시나리오를 구상하기 위해 mockRandoms 메서드를 생성했다.
const mockRandoms = numbers => {
Random.pickUniqueNumbersInRange = jest.fn();
numbers.map(number => Random.pickUniqueNumbersInRange.mockReturnValueOnce(number));
};
1️⃣ 구매 금액 만큼의 로또 번호 생성
구매 금액에 따른 로또 번호를 생성하는 Lottos 클래스를 테스트하기 위해, 구매 금액에 따른 생성 횟수를 2라고 가정했다. 그 다음에는 mockRandoms에 random 값을 전달하여 로또 번호 시나리오를 설정하였고, 이를 Lottos에 count로 전달하여 2개의 로또 번호가 생성되도록 했다. 생성된 로또 번호는 getLottos() 메서드를 호출하여 받아온 값을 recievedValue에 할당했다. 마지막으로, 생성된 로또 번호가 expectedValue라고 가정하고 recievedValue와 함께 테스트를 진행했다.
test('구매금액 만큼 로또 번호를 생성한다.', () => {
const expectedValue = [
[1, 2, 3, 4, 5, 6],
[2, 3, 4, 5, 6, 7],
];
const count = 2;
mockRandoms(expectedValue);
const lottos = new Lottos(count);
const recievedValue = lottos.getLottos();
expect(recievedValue).toStrictEqual(expectedValue);
});
2️⃣ 당첨 통계 계산
로또 번호, 당첨 번호, 보너스 번호를 통해 당첨 통계를 계산하는 WinningStatistics 클래스를 테스트하기 위해, 먼저 구매 금액만큼의 로또 번호를 생성했다. mockRandoms에 random 값을 전달하여 로또 번호 시나리오를 설정하고, 당첨 번호와 보너스 번호를 임의로 할당하여 WinningStatistics 클래스에 인자로 전달했다. 당첨 통계의 메시지가 담긴 배열을 가져오기 위해 getWinningStaticsString()를 호출하여 winningStatistics 변수에 할당했다. 최종적으로, 생성된 당첨 통계인 winningStatistics를 expectedValue와 함께 테스트를 진행했다.
test('당첨 통계를 계산하는 기능', () => {
const expectedValue = [
'3개 일치 (5,000원) - 0개',
'4개 일치 (50,000원) - 0개',
'5개 일치 (1,500,000원) - 0개',
'5개 일치, 보너스 볼 일치 (30,000,000원) - 1개',
'6개 일치 (2,000,000,000원) - 0개',
];
const winningNumbers = [10, 34, 2, 42, 33, 22];
const bonusNumber = 21;
const count = 3;
const random = [
[1, 2, 3, 4, 5, 6],
[3, 4, 5, 6, 2, 8],
[10, 34, 2, 42, 33, 21],
];
mockRandoms(random);
const lottos = new Lottos(count).getLottos();
const winningStatistics = new WinningStatistics(lottos, winningNumbers, bonusNumber).getWinningStaticsString();
expect(winningStatistics).toStrictEqual(expectedValue);
});
3️⃣ 총 수익률 계산
당첨 통계와 구매 금액을 통해 총 수익률을 계산하는 RateOfReturn 클래스를 테스트하기 위해, 구매 금액과 당첨 통계를 가정했다. 이후 구매 금액과 당첨 통계를 RateOfReturn에 인자로 전달하여 총 수익률을 반환받아 rateOfReturn에 할당했다. 마지막으로, rateOfReturn를 기대값인 expectedValue와 함께 테스트를 진행했다.
test('총 수익률을 계산하는 기능', () => {
const purchaseAmount = 8000;
const winningStatistics = [0, 0, 3];
const expectedValue = '62.5%';
const rateOfReturn = new RateOfReturn(winningStatistics, purchaseAmount).getRateOfReturn();
expect(rateOfReturn).toBe(expectedValue);
});
테스트 케이스 작성을 위해 matcher 관련 메서드를 찾아보는 과정이 있었지만, mocking을 이용한 테스트 코드 작성에는 이전에 진행했던 미션에서 보다 익숙해진 것 같다. 하지만, 테스트 코드를 먼저 작성하지 않고 도메인 로직을 먼저 구현하다 보니, 이후에 발생하는 버그를 다시 수정해야 하는 문제가 있었다. 이로 인해 시간이 꽤 걸렸는데, 이를 통해 '테스트 코드를 먼저 작성하고 개발하는 TDD(Test-Driven Development) 방법론'에 대해 고민하게 되었다.
TDD란?
TDD는 '테스트 주도 개발'이라고 하며, 작은 단위의 테스트 케이스를 작성하고, 이를 통과하는 코드를 추가하는 단계를 반복하여 구현한다. 중요한 점은 실패하는 테스트 코드를 작성할 때까지 실제 코드를 작성하지 않는 것과, 실패하는 테스트를 통과할 정도의 최소한의 실제 코드를 작성해야 한다는 것이다.
TDD를 통해 실제 코드에 대한 기대치를 명확하게 정의함으로써 불필요한 설계를 피하고, 정확한 요구 사항에 집중할 수 있다는 것을 깨달았다. 따라서, 다음 미션에서는 TDD 방법론을 적용해보려고 한다.
🤯 시행착오
⚠️ 에러 핸들링
ApplicationTest에서 계속 실패가 발생하는 runException('1000j') 부분을 살펴보기로 했다. 이 부분에서 왜 실패하는지 이해가 가지 않아, runException() 메서드를 상세히 살펴보기로 했다.
const runException = async input => {
// given
const logSpy = getLogSpy();
const RANDOM_NUMBERS_TO_END = [1, 2, 3, 4, 5, 6];
const INPUT_NUMBERS_TO_END = ['1000', '1,2,3,4,5,6', '7'];
mockRandoms([RANDOM_NUMBERS_TO_END]);
mockQuestions([input, ...INPUT_NUMBERS_TO_END]);
// when
const app = new App();
await app.play();
// then
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('[ERROR]'));
};
이전 미션에서는 throw에 [ERROR]가 포함되어 있는지 확인했었는데, 이번에는 throw를 확인하는 것이 아니라 Console.print()를 spy로 감지하여 출력에 [ERROR]가 포함되어 있는지 확인하고 있었다. 출력에서 왜 에러 문구를 확인하는지 이해하지 못해, 요구사항을 다시 살펴보았다.
요구사항을 다시 보니, throw문으로 예외를 발생시키고 해당 예외의 메시지를 출력하며, 사용자로부터 다시 입력을 받도록 되어 있었다. 그래서 입력 부분에서 try 문을 사용해 입력에 대한 유효성 검사를 통해 에러를 throw하고, 동시에 catch 문을 통해 error의 메시지를 출력하도록 수정해야 했다.
static remainderNotZero(purchaseAmount) {
if (purchaseAmount % CONSTANTS.purchaseAmount.amountDivisor !== CONSTANTS.number.zero)
throw new Error(ERROR.message.remainderNotZero);
}
static invalidNumber(number) {
if (!Number(number)) throw new Error(ERROR.message.invalidNumber);
}
이미 유효성 검사를 담당하는 Validator 객체에서 에러를 throw하고 있었기 때문에, 어떻게 하면 그 에러 객체에서 메시지를 가져올 수 있을지 고민했다. 이에 대한 의문을 해결하기 위해, 에러 핸들링에 관한 내용을 더 자세히 찾아보기로 했다.
Error 객체에 message라는 속성이 있다는 것을 확인했다. 이를 통해 Error.message를 구조 분해하여 상수로 정의해놓은 에러 메시지를 콘솔에 출력하도록 수정했다. 또한, 무한 반복문을 사용하여 사용자가 올바른 입력을 할 때까지 입력을 계속 받도록 로직을 수정했다. 수정한 코드는 다음과 같다.
readPurchaseAmount: async () => {
while (true) {
try {
const purchaseAmount = await Console.readLineAsync(MESSAGE.read.purchaseAmount);
Validator.validatePurchaseAmount(purchaseAmount);
return purchaseAmount;
} catch ({ message }) {
Console.print(message);
}
}
},
💡 회고
이번 미션에서는 클래스와 함수를 어떻게 분리할 수 있는지에 대해 많은 시간을 투자했다. 특히, 클래스를 분리하고 리팩토링하면서, 자연스럽게 클래스의 안정성을 고려하게 되어 캡슐화를 진행하는 과정이 신기했다.
이번 미션을 통해 다음과 같은 것들을 배웠다.
단위 테스트의 중요성을 알게 되었다. 도메인 로직에 따른 테스트를 진행하면서, 오류가 적은 안전한 유닛을 작성할 수 있음을 깨달았다. 아쉽게도, 도메인 로직에 해당하는 기능을 먼저 개발하고 테스트 코드를 작성하게 되면서, 여러 버그를 수정하는 데 시간이 오래 걸렸다. 그러나 이를 통해 TDD(Test-Driven Development)라는 방법론을 알게 되었고, 다음 미션에서는 이 방법론이 효율적인 테스트 코드 작성에 큰 도움이 될 것이라 생각한다.
다만, 제출 후에는 몇 가지 후회와 아쉬움도 있었다.
특히, 클래스에서 정적 메소드와 프로퍼티를 더 고려했으면 하는 생각이 들었다. 대부분의 클래스들이 복제가 필요하지 않는 데이터를 다루고 있었기 때문에, 인스턴스를 생성하지 않고 클래스에서 메소드를 호출할 수 있는 방법이 있었다.
또한, 유효성 검사를 수행하는 기능들을 Validator라는 하나의 클래스로 관리한 점이 아쉬웠다. 각 목적에 맞는 유효성 검사들을 다수의 클래스로 분리하여 관리하는 것이 더 좋지 않았을까 하는 의문이 들었다.
벌써 다음 미션이 4주차라는 사실이 믿기지 않는다. 3주 동안 이렇게 깊이 있는 내용들을 빠르게 학습할 수 있었다는 것이 놀라웠다. 우테코에서 추구하는 교육 방식이 나에게 매우 적합하다는 것을 느꼈다. 이를 매주 느끼면서 이번 우아한테크코스 6기에 꼭 들어가고 싶다는 마음은 더욱 굳건해졌다. 배운 내용들을 모두 정리하고, 다시 한 번 미션을 수정하면서 복습하는 시간을 갖어야겠다. 다음 4주차 미션에서는 모든 것을 쏟아붓어야겠다. 앞으로 남은 한 주도 의미 있는 시간이 되도록 노력할 것이다.
⛳️ 다음 목표
- TDD 방법론 적용하기
- 정적 메소드와 프로퍼티 고려하기
- 테스트코드 분리하기
🏃♂️ 구현 코드 보러가기
[로또] 강병현 미션 제출합니다. by llbllhllk · Pull Request #202 · woowacourse-precourse/javascript-lotto-6
github.com
'🚀 우아한테크코스 6기 프리코스' 카테고리의 다른 글
[우아한테크코스 6기] 최종 코딩 테스트 - 온콜 리팩토링 회고록 (0) | 2023.12.21 |
---|---|
[우아한테크코스 6기] 1차 합격 및 최종 코딩 테스트 후기 (4) | 2023.12.17 |
[우아한테크코스 6기] 프리코스 4주차 - 크리스마스 프로모션 회고록 (0) | 2023.11.20 |
[우아한테크코스 6기] 프리코스 2주차 - 자동차 경주 회고록 (0) | 2023.11.02 |
[우아한테크코스 6기] 프리코스 1주차 - 숫자 야구 회고록 (0) | 2023.10.26 |