우테코 프리코스 미션 #03 | Lotto Game
2022년 11월 17일 01:51
개요
이번 프로젝트는 JS만으로 콘솔에서 아래와 같이 로또게임을 구현하는 것이었다.
구입금액을 입력해 주세요.
8000
8개를 구매했습니다.
[8, 21, 23, 41, 42, 43]
...
[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%입니다.접근 방식
🔍 도메인 설계
최종적으로 클라이언트 개발자가 내가 만든 객체들에게 보내는 메시지가 무엇일까? 를 시점으로 시작했고, 그 메시지는 결국 계산해줘일 것이라고 생각했다.
이 calculate()라는 메시지를 처리해줄 객체는 누군지 고민해보면 발급한 번호들을 모두 알고 있고, 당첨 번호도 알고 있는 객체일 것이다. 나는 이 객체에 Calculator라는 이름을 붙였다.
Calculator는 발급 번호들과 당첨 번호를 알고 있다고 했다. 각각을 Ticket, WinningLotto이라 하자. Ticket의 개수가 n이라 했을 때 Calculator가 Result를 반환하기 위해서 해야하는 연산으로 두가지 경우가 있다.
- WinningLotto에게 번호를 1번 물어봐서 Ticket들에게 일치여부를 n번 묻기
- Ticket들에게 번호를 n번 물어봐서 WinningLotto에게 일치여부를 n번 묻기
나는 당연히 1번의 경우를 택했고, 해당 메시지를 처리하도록 설계 했다.
🔍 객체의 합성
지난 주차부터 작성한 도메인들에 대해 어떤 방식으로 활용할지에 대해서 고민이 많았다. 예를 들어서 바나나를 먹으면 원숭이의 totalCalorie가 올라가는 코드를 작성할 때
class Bannana {
private calorie: number
constructor(calorie: number) {
this.calorie = calorie;
}
getCalorie() {
return calorie;
}
}
class Monkey {
private totalCalorie: number
constructor() {
this.totalCalorie = 0;
}
eat(calorie: number) {
this.totalCalorie += calorie;
}
}
//App.js
const bannana = new Bannana(20);
const monkey = new Monkey();
monkey.eat(bannana.getCalorie());위와 같이 작성할 수 있다. 하지만 이렇게 작성해야 한다면 “App.js”를 작성하는 입장에서 알아야할 것들이 점점 커지게 된다.
나중에 프로젝트의 규모가 커지게 된다면 어떤 객체가 어떤 의존성을 가지고, 어떤 순서로 메서드를 호출해야하는지에 대해 파악하기 어려워질 것이다.
나는 이렇게 작성하는 것이 필요한 메서드들을 절차적으로 호출하는 것처럼 보였다.
class Monkey {
private totalCalorie: number
constructor() {
this.totalCalorie = 0;
}
eat(bannana: Bannana) {
this.totalCalorie += bannana.getCalorie();
}
}
//App.js
const monkey = new Monkey();
monkey.eat(new Bannana(20));최종적으로 내린 결론은 생성자나 메서드의 매개변수를 통해 의존관계를 명시해 주는 것이 추후에 이들을 활용해 코드를 작성할 때, 합성의 방식을 통해서 좀 더 유지보수하기 쉬운 코드를 작성할 수 있게 된다고 생각했다.
위의 코드는 아마 Food인터페이스를 통해 좀 더 확장성있게 작성할 수 있을 것이다.
🔍 책임에 대한 생각
작성한 코드들 중 Buyer를 중점적으로 내 생각을 적어 보겠다.
‘Buyer에게 buy라는 메시지를 보낸다.‘에 초점을 맞춰서 해당 객체를 작성해 봤다.
class Buyer {
static #validate(money) {
//...
}
static #createRandomNumbers() {
return Random
.pickUniqueNumbersInRange(1, 45, 6);
}
static buy(money) {
Buyer.#validate(money);
return Array
.from({ length: parseInt(money) / Ticket.price()})
.map(() => new Ticket(Buyer.#createRandomNumbers()));
}
}Buyer객체의 책임은 가격에 알맞은 개수의 티켓을 랜덤으로 구매하는 것이다.
2개의 책임이 아닐까? 하고 합리적 의심을 할 수 있지만 현재 판단으로는 ‘외부에서는 buy라는 메서드만 호출할 수 있기 때문에 하나의 책임으로 볼 수 있지 않을까?’ 생각한다. (물론 이 책임을 분리한다면 다른 정책을 대체할 수도 있겠다)
Refactoring
♻️ Builder패턴 적용
기능 목록에도 나와있듯이 로또에는 보너스 번호가 포함되어 있다.
그래서 당연히 bonus 필드를 추가하려 했지만, 프로그래밍 요구사항에 “Lotto에 필드를 추가할 수 없다.”라는 문장을 보고 고민에 빠졌다.
처음에는 아래와 같이 bonus 필드를 가진 Lottery 클래스가 Lotto 클래스를 상속받게끔 구현을 했다.
class Lottery extends Lotto {
#bonus;
//...
}하지만 코드 재사용을 줄여준다는 장점 외에는 가독성도 좋지 않아서 Lotto 클래스와 별도로 Bonus 클래스를 작성하고, 이 둘을 필드로 가진 Lottery 클래스를 작성했다.
class Lottery {
#lotto;
#bonus;
//...
}
new Lottery.Builder()
.setLotto(new Lotto([1,2,3,4,5,6]))
.setBonus(new Bonus(7));최종적으로 Lotto와 Bonus의 존재를 몰라도 Lottery 객체를 생성할 수 있도록 작성(Builder 패턴)을 했더니 Client 측의 코드가 가벼워진 걸 경험할 수 있었다.
♻️ MissionUtils.readLine 비동기 처리 (진행중)
readLine 메서드에 콜백 함수를 넣다 보니 각 함수가 끝나는 시점에 다음 함수를 호출하는 방식으로 코드를 작성했다.
function purchase() {
//...
chargeAnswer();
}
function chargeAnswer() {
//...
chargeBonus();
}
function chargeBonus() {
//...
Console.close();
}하지만 이 방식은 다음 서브루틴에게 의존적이게 된다는 이유로 좋지 않다고 생각했고, Promise 객체를 생성해서 해당 부분을 아래와 같이 비동기 처리하려고 했다.
return new Promise((resolve) =>
console.readLine(this.prompt, (command) => {
resolve(command);
}));내가 직접 작성한 테스트 함수 코드에서는 정상적으로 동작했지만 왜인지 ApplicationTest의 logSpy가 log들을 잡아내지 못했다.
이러한 부분을 해결하기 위해 시간을 많이 투자했지만 jest의 Mock 관련 부분을 완전히 이해하지 못해서 끝내 해결하지 못했고, 결국 콜백 함수에 다음 함수를 호출하는 방식으로 구현했다.
마치며
TDD는 역시 어렵다. 이제는 코드를 잘 작성하는 것 뿐만 아니라 테스트 코드를 잘 작성하는 것도 생각해야하고, 어떻게 하면 더 잘 작성할 수 있을지 고민하게 된다.
어렵고 번거롭다는 이유로 매번 즐겁게 하지 못했었는데, 운이 좋게도 이번 주차에서 이 테스트 코드의 덕을 크게 봤다.
이번 프로젝트에서 상금을 할당하는 로직을 score테이블에 key를 6, 5.5, 5, 4, 3 와 각각에 해당하는 상금을 value로 두고 key에 해당하는 value를 계산하는 방식으로 작성했었다.
이전에 작성했던 test 코드를 실행시켜보았고, 자꾸 에러가 나서 처음에는 test 코드를 잘못 작성한 줄 알고 한참 동안 test 코드를 분석해봤다.
아무리 봐도 test 코드에는 문제가 없어서 중간 결과를 출력해 보니 4.5(번호를 4개 맞추고 보너스도 맞추고)인 경우 어떠한 상금을 받지 못하게 작성한 것을 알게 되었다.
현재 내가 고수준의 TDD를 하는 건 아니지만 이번 미션을 계기로 TDD의 중요성을 알게 돼서 너무 즐겁다.