3주차에 들어가기에 앞서서
이번주는 어떤 목표를 가지고 풀어나가야할지 고민하는 시간을 가졌다.
클래스(객체)를 분리하는 연습
숫자야구 과제를 구현할때에는 모듈화를 하였지만 객체 분리를 하지는 않았었다. 이번 기회를 통해 객체지향 프로그래밍을 조금이나마 다뤄볼 수 있을 것 같다.
그리고 MVC 패턴을 적용해 볼 생각이다. 이전에 MVC 패턴에 대해 궁금해서 검색했던 기억이 나는데 그 당시에 잘 이해가 가지않아서 다음으로 미뤘었다. 이번에는 적용해보면서 한번 더 공부를 해봐야겠다.
도메인 로직에 대한 단위 테스트를 작성하는 연습
Jest 라는 테스팅 프레임워크를 배웠고, 바로 적용을 할 수 있었다. 저번주의 테스트는 단위 테스트 보다는 통합 테스트에 가깝게 구현을 했었다. 이번에는 요구사항대로 클래스로 나누어진 모델, 컨트롤러의 메소드를 좀 더 단위테스트에 가깝게 적용해봐야겠다.
구현하면서 배웠던 점들
MVC 로직 설계
마침 MVC 패턴에 대해 공부하려고 했는데 블로그 이웃분께서 그와 관련된 글을 포스팅 해주셔서 정보를 얻을 수 있었다. 참고자료
내가 생각한 MVC 는 다음과 같았다.
- Model: 객체 덩어리, User정보가 될 수도 있고 이번과제에서는 로또 정보가 Model 이 되지 않을까 싶다. 또한 만약에 커뮤니티를 구현 한다고 하면 게시글 정보 또한 Model 이 될 수 있다고 생각했다.
- View: 브라우저 화면 , 데이터를 입력받고 가공된 데이터를 출력하는 영역이다.
- Controller: Model 들의 데이터를 이용해 새로운 Model 에 데이터를 넣을 수도 있을것이고 Model - View 의 데이터를 사용할 수 있게 연결시켜주는 영역이라고 생각했다.
그렇다면 로또 과제에서는 어떻게 로직을 구분해야할까. 고민을 많이했다.
View 영역을 크게 3가지 영역으로 구분하였다.
- 로또를 구입할 금액을 입력
- 당첨번호와 보너스 번호를 입력
- 로또 당첨 결과를 출력
당첨 번호와 보너스 번호 입력도 쪼갤까 했는데 너무 쪼개어지면 오히려 코드읽을때 어려워질 것 같아서 쪼개지 않았다.
View는 생각했던대로 입출력 UI 기능을 기준으로 나누었다.
Controller 같은 경우에는 로또 게임과 관련된 하나만 있으면 될 것이라는 판단을 했다. 또한 컨트롤러를 여러개 설계를 해본 경험이 없기도 했다.
Model은 로또게임의 주인공인 Lotto 로 설계를 했다. 로또번호를 저장하고, 당첨번호와 자신의 정보를 비교해서 몇개 맞았는지 리턴하는 메소드를 만들면 될 것이라고 생각했다.
참고로 기존에는 utils 함수에 모든 모듈화된 함수를 넣었는데 그렇게 하면 안된다라는 것을 알았다. 그 이유는 특정 데이터에서만 동작하는 함수 같은 경우 다른 로직에서 재사용하기 힘들뿐만아니라 유지보수할때 모듈화된 함수를 찾기가 어려워지는 점 때문이다.
common.js 라는 파일을 만들어 공통의 함수를 관리할 수 있게 하였다. validate.js 또한 숫자인지 판별하는 함수, 0이 포함되어있는지 판별함수와 같이 재사용이 가능한 유효성 체크 함수로 구분했다.
만약 특정 데이터에서만 사용을 해야할 경우 Model 의 메소드로 만들어 둘 생각이었다.
MVC 패턴 적용
이번 과제가 Console로 입출력 기능이 구현되다보니 readLine 콜백을 사용해야했다. 동기적인 흐름에 따라 코드가 실행되도록 콜백내부에 다음 실행될 함수를 호출해주어야했는데 Controller 에 구현되어있는 메소드를 bind 하여 View 로 넘겨주어 호출하는 방법을 선택했다. this 로 바인딩한 이유는 Controller 에 있는 필드를 사용하기 위함이다.
start() {
this.view.inputMoneyView.inputMoney(this.buyLotto.bind(this));
}
Lotto 모델은 다음과 같이 구현을 했다. checkHowManyCorrect 라는 메소드를 사용하여 로또가 몇개나 맞았는지 값을 리턴한다.
이처럼 Model 의 데이터를 가공하여 만들 수 있는 후처리 데이터는 Model method 를 사용하여 리턴을 해주려고 했다.
만약 A model 과 B model 의 데이터를 섞어 C 를 만들어야했다면 Controller 에서 처리를 해주었을 것이다.
const { Console } = require('@woowacourse/mission-utils');
const { RULES, ERROR_MESSAGE } = require('./constants/index.js');
class Lotto {
#numbers;
constructor(numbers) {
this.validate(numbers);
this.#numbers = numbers;
}
validate(numbers) {
if (numbers.length !== RULES.LOTTO_NUMS) {
Console.close();
throw new Error(ERROR_MESSAGE.INVALID_LOTTO_NUMBER);
}
if (new Set(numbers).size !== RULES.LOTTO_NUMS) {
Console.close();
throw new Error(ERROR_MESSAGE.DUPLICATE_NUMBER);
}
}
// TODO: 추가 기능 구현
getLottoNumber() {
return this.#numbers;
}
checkHowManyCorrect(winningNumber, bonusNumber) {
const { length: matchCount } = this.#numbers.filter((number) => winningNumber.includes(number));
if (matchCount === 5 && this.#numbers.includes(Number(bonusNumber))) {
return '5+bonus';
}
return matchCount;
}
}
module.exports = Lotto;
도메인 로직의 단위테스트
메소드를 하나 만들때마다 그 메소드에 대한 테스트 코드를 작성했다. 가장 의미가 있었던 테스트는 예외처리 테스트였는데, 이 과정을 통해 고려하지 못했던 예외사항에 대해 처리를 할 수 있었다.
예를 하나 들자면 보너스 번호를 입력받을때 그 번호가 이미 입력받은 당첨번호에 있는지 여부를 체크하지 않았는데 그 부분에 대해 예외를 파악하고 로직을 수정했다.
테스트를 해보며 느낀 에로사항
예상치 못한 오류또한 만나게 되었는데 2주차 과제와 마찬가지로 Jest 가 종료되지 않는 오류가 발생했다. 저번 주차에서는 입력을 계속 기다렸다면 이번에 오류가 생긴 이유는 throw Error 를 터뜨린 다음 Console.close 를 해주지 않아서 발생한 오류였다.
이후 모든 validation에 close 를 추가해줌으로써 오류를 수정할 수 있었다.
그리고 로직을 조금 수정할때마다 테스트 코드를 계속 수정해주어야해서 불편했다. 작은 단위로 쪼개어서 그렇게 오래걸리지는 않았지만 귀찮게 여겨졌다.
테스트 케이스를 만들때 다음과 같은 생각이 들기도 했다. 구현된 기능을 테스트 코드를 통해 검증할 수 있다는 것은 큰 장점이지만 만약 내가 잘못된 생각을 가지고 기능을 구현했고, 그 잘못된 생각이 테스트에도 반영될 거라는 것이라는 생각이 들었다. 이러한 문제점을 막기위해서는 테스트 코드를 짤때는 여러명의 개발자가 모여 테스트 케이스들를 만들어 실행을 해봐야할 것이라고 판단할 수 있었다.
리팩토링
구현을 하면서 코드 퀄리티를 높이기 위해서 노력했다. 다른 분들의 코드를 참고하기도 했다. 어떤 부분에서 감명을 받았는지 기록해둔다.
우선 각 등수가 몇개나 있는지 체크하기위해 객체를 dictionary 로 사용하였다. 다음과 같이 말이다. 하지만 이 코드에는 문제가 많다. 우선 프로퍼티의 key 값이 하드코딩이 되어있다.
class LottoMachineController {
constructor() {
this.lottoResultMap = {
'3개': 0,
'4개': 0,
'5개': 0,
'5개+보너스': 0,
'6개': 0,
};
...
}
그러므로 위 코드를 다음과 같이 변경하였다. LOTTO_RANKING_REWARD 라는 상수를 선언하여 Object.keys 를 사용해 key 값만 뽑아내어 lottoResult 라는 객체를 만들었다.
const LOTTO_RANKING_REWARD = Object.freeze({
'1등': 2_000_000_000,
'2등': 30_000_000,
'3등': 1_500_000,
'4등': 50_000,
'5등': 5_000,
});
generateLottoResultObject() {
const lottoResult = {};
for (let ranking of Object.keys(LOTTO_RANKING_REWARD)) {
lottoResult[ranking] = 0;
}
return lottoResult;
}
두번째 리팩토링한 코드는 랜덤으로 발급된 로또 번호를 당첨번호와 매칭하는 로직이었다.
아래코드는 나쁘지 않다고 생각했는데, 다른분 코드를 보다가 더 좋다고 생각한 로직이 있어서 반영했다.
const checkHowManyCorrect = (lotto, winningNumber, bonusNumber) => {
return lotto.getLottoNumber().reduce(
(prev, curr) => {
if (winningNumber.includes(curr)) return { ...prev, correctCount: prev.correctCount + 1 };
if (curr === bonusNumber) return { ...prev, bonus: prev.bonus + 1 };
return prev;
},
{ correctCount: 0, bonus: 0 },
);
};
아래 로직인데 딱 봐도 간결하고 이해하기도 편했다.
filter 메소드를 사용해서 당첨번호와 같은 번호만 남기는데 이때 { length : matchCount} 와 같이 구조분해 할당으로 배열의 길이를 받아온 코드가 인상적이여서 반영했다.
checkHowManyCorrect(winningNumber, bonusNumber) {
const { length: matchCount } = this.#numbers.filter((number) => winningNumber.includes(number));
if (matchCount === 5 && this.#numbers.includes(bonusNumber)) {
return '5+bonus';
}
return matchCount;
}
아래 코드 또한 굉장히 맘에 들었다.
dictionary 에 없는 key 값을 참조할때 undefined 가 리턴 되는 것을 활용하는 디테일이 돋보이는 코드였다.
this.purchasedLottos.forEach((lotto) => {
const matchCount = lotto.checkHowManyCorrect(winningNumber, bonusNumber);
const ranking = RANKING_ACCORDING_MATCH_COUNT[matchCount];
if (lottoResult[ranking] === undefined) return;
lottoResult[ranking] += 1;
});
마지막으로 개선한 점은 총 상금을 계산하는 로직이였다.
초기 코드이다. 딱 봐도 이건 아닌 것 같다. 금액이 하드 코딩 되어있어 나중에 상금이 수정되면 일일히 내부 로직을 찾아서 수정해주어야하했다. 또 반복문을 사용하지 않았던 점이 원시인 같다.
calculateTotalPrizeMoney() {
this.totalPrizeMoney =
5_000 * this.lottoResultMap['3개'] +
50_000 * this.lottoResultMap['4개'] +
1_500_000 * this.lottoResultMap['5개'] +
30_000_000 * this.lottoResultMap['5개+보너스'] +
2_000_000_000 * this.lottoResultMap['6개'];
}
마찬가지로 Object.keys 를 이용해서 key 값만 빼낸다음 상금 * 로또 갯수 연산을 수행한다. reduce 와 함께 사용하니 코드가 많이 간결해졌다.
calculateTotalPrizeMoney(lottoResult) {
const totalProfit = Object.keys(lottoResult).reduce(
(total, ranking) => (total += lottoResult[ranking] * LOTTO_RANKING_REWARD[ranking]),
0,
);
이번주 느낀점 요약
- MVC 패턴 아직 잘 모르겠다. 하지만 View, Model, Controller 로 나뉘어짐으로써 입력에 문제가 생겼다면 View 를 보면 되고 Lotto 데이터가 이상하다면 Lotto Model 을 보면된다. 고로 유지보수에 유리할 것이라는 생각이 들었다.
- 구현된 기능을 테스트 코드를 통해 검증할 수 있다는 것은 큰 장점이지만 만약 내가 잘못된 생각을 가지고 기능을 구현했고, 그 잘못된 생각이 테스트에도 반영될 거라는 것이라는 생각이 들었다. 이러한 문제점을 막기위해서는 테스트 코드를 짤때는 여러명의 개발자가 모여 테스트 케이스들를 만들어 실행을 해봐야할 것이라고 판단할 수 있었다.
- 리팩토링을 할때에는 다른사람의 코드를 참고하자. 내가 생각하지 못했던 방법이 굉장히 많다. 생각이 고여버려서 안된다.
'공부기록 > 웹 개발' 카테고리의 다른 글
[우아한 테크코스 프리코스] 1차 탈락 (4) | 2022.12.15 |
---|---|
[우아한 테크코스 프리코스] 4주차 (0) | 2022.11.30 |
[우아한 테크코스 프리코스] 2주차 (1) | 2022.11.09 |
html dataset 을 사용한 css switch case (1) | 2022.10.11 |
[타입스크립트] Utility Types (0) | 2022.10.11 |