로또 게임 기능을 구현해야 한다. 로또 게임은 아래와 같은 규칙으로 진행된다.
- 로또 번호의 숫자 범위는 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]"로 시작하는 에러 메시지를 출력 후 종료한다.14000
1,2,3,4,5,6
7
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]
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%입니다.
[ERROR] 로또 번호는 1부터 45 사이의 숫자여야 합니다.
구입금액을 입력해 주세요.
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 v14.20.1
, Visual Studio Code
로 세팅하였습니다.자바스크립트 코드 컨벤션
은 Airbnb 자바스크립트 스타일 가이드를 참고하였습니다.커밋 메시지 컨벤션
은 커밋 메시지 컨벤션 가이드를 참고하였습니다.함수(메서드)라인에 대한 기준
예외상황에 대해 고민하기: 예외상항을 모두 고려해 프로그래밍 하도록 고민한다.
비즈니스 로직과 UI 로직을 분리하기: 분리하지 않으면 단일 책임의 원칙에도 위배된다.
class Lotto {
#numbers
// 로또 숫자가 포함되어 있는지 확인하는 비즈니스 로직
contains(numbers) {
...
}
// UI 로직
print() {
...
}
}
객체의 상태 접근 제한하기
private
class필드로 구현한다. 객체는 객체스럽게 사용하기
Getter
메서드만을 가지는 객체는 객체스럽지 않다.필드 수를 줄이기 위해 노력하기
성공하는 케이스 뿐만 아니라 에외에 대한 케이스도 테스트하기
테스트 코드도 코드다
test.each
등을 사용해 중복을 개선하자테스트를 위한 코드는 구현 코드에서 분리하기
단위 테스트하기 어려운 코드를 단위 테스트하기
3주차 미션의 프로그래밍 요구사항 중 아래와 같은 요구사항이 있었다.
Lotto
에 필드를 추가할 수 없다.
그동안은 자바스크립트 클래스의 프로퍼티, 메서드라는 용어에 익숙해져 있어서 필드는 내가 아는 그 필드(ex. Java에서의 클래스 필드)가 맞는지 의문이 들었다.
모던자바스크립트
책을 찾아보니 (p.439)
클래스 필드(또는 멤버)란? 클래스가 생성할 인스턴스
의 프로퍼티를 가리킨다고 한다.
즉, 자바스크립트의 생성자 함수에서 this
에 추가한 프로퍼티를 클래스 기반 객체지향 언어에서는 클래스 필드라고 부른다.(내가 아는 그 필드를 말하는 게 맞았다.)
클래스 필드 정의는 2가지 방법으로 할 수 있다.(ES2022)
1. constructor
에서 필드(인스턴스 프로퍼티)를 정의하는 방식
2. 클래스 바디
에서 필드(인스턴스 프로퍼티)를 정의하는 방식
👉 외부 초기값으로 필드를 초기화할 필요가 있다면 1번, 없다면 1번과 2번 모두 사용 가능하다.
👉 주의) 클래스 바디에서 정의하는 경우 this
에 클래스 필드를 바인딩해서는 안된다.
🙋🏻 왜❓ this
는 클래스의 constructor
와 메서드
내에서만 유효하기 때문이다.
클래스 필드에 대해 알아보려고 하다가 모던자바스크립트
책을 더 읽어보니 (p.440) 아래와 같은 문구가 있었다.
자바스크립트의 클래스에서 인스턴스 프로퍼티를 선언하고 초기화하려면 반드시 constructor 내부에서 this에 프로퍼티를 추가해야 한다.
자바스크립트의 클래스 몸체에는 메서드만 선언할 수 있다.
그럼 내가 본 클래스 바디에 선언되어있는 Lotto
클래스의 #numbers
필드는 무언인가?
라는 생각이 들었는데 바로 밑에 설명이 나와있었다.
자바스크립트에서도 인스턴스 프로퍼티를 마치 클래스 기반 객체지향 언어의 클래스 필드처럼 정의할 수 있는 새로운 표준 사양인 "Class field declararations"가 2021년 1월, TC39프로세스의 stage3(candidate)에 제안되어 있다.
현재 시점(2022. 11.)에서 다시 찾아보니 Class field declarations는 stage4를 거쳐 ECMAScript 제13판(ES2022라고도 불린다)에 포함되었다고 한다. 이제 ECMAScript의 정식 표준 사양으로 승급된 것이다.
Lotto
클래스의 #numbers
는 private
필드인데 이 private
필드 정의 또한 이 ECMAScript2022에 포함되어 정식 표준 사양으로 승급되었다.
private
필드의 특징은 클래스 내부에서만 참조할 수 있다는 것과 반드시 클래스 바디에 정의해야한다는 것이다. (private
필드를 직접 constructor
에 정의하면 에러가 발생한다.)
2주차 때도 정적 메서드 사용에 관하여 고민을 했었는데 새로 알게된 것이 있어 기록하고자 한다.
프로토타입 객체(또는 프로토타입)란❓
- 자바스크립트에서 객체 간 상속을 구현하기 위해 사용되는 객체로 어떤 객체의 상위(부모) 객체의 역할을 하는 객체이다.
- 다른 객체에 공유 프로퍼티, 공유 메서드를 제공한다.
this
를 사용해야 하므로 이런 경우엔 static
메서드 대신 프로토타입
메서드로 정의해야 한다.프로토타입
메서드는 반드시 인스턴스를 생성한 다음 인스턴스로 호출해야 하므로 this
를 사용하지 않는 메서드는 정적 메서드로 정의하는 것이 좋다. (모던자바스크립트 p.433)로또 미션에서 Lotto
클래스는 로또번호(#numbers
)를 필드로 가지고 있는데, 이것은 숫자배열이었다. 나는 당첨번호 또한 Lotto
클래스 생성자함수의 인자로 받아서 #numbers
에 할당하고자 했다. 하지만 당첨번호는 처음 입력받을 때 '1,2,3,4,5,6'
형식의 string
이었기 때문에 숫자배열로 만들어주기 위해 Lotto
클래스의 생성자함수를 오버로딩하여 Lotto
클래스 내에서 string
을 array
로 변환하는 작업을 거치려고 했었다. 그런데 찾아보니 자바스크립트는 오버로딩을 지원하지 않는다는 사실을 알게 되었다..
어찌되었든 최종적으로 당첨번호는 굳이 객체로 만들지 않아도 될 것 같다는 생각을 했기 때문에 당첨번호는 App
클래스에서 숫자배열로 바꿔서 사용하였다.
전역에 네임스페이스 역할을 담당할 객체를 생성하고 전역 변수처럼 사용하고 싶은 변수를 프로퍼티로 추가하는 방법이다. (
모던자바스크립트
, p.205)
네임스페이스를 분리해서 식별자 충돌을 방지하는 효과는 있으나 네임스페이스 객체 자체가 전역 변수에 할당되므로 그다지 유용해 보이지는 않는다.
전역변수는 전역객체의 프로퍼티이다.
웹브라우저
에서 실행한다면 전역객체는window
객체Node.js
에서 실행한다면 전역객체는global
객체자바스크립트는 파일이 분리되어 있어도 전역 스코프를 공유한다는 문제점이 존재한다. 따라서 동일한 이름으로 명명된 변수로 예상치 못한 결과를 가져올 수 있다.
내가 3주차에서 사용한 방법(상수를 모아놓을 객체를 생성하고 메시지를 프로퍼티로 추가하는 방법)도 이런 경우에 해당하나싶어서 한번 찾아보았다.
const MESSAGE = Object.freeze({
START_GAME: '로또 게임을 시작합니다.',
FINISH_GAME: '로또 게임을 종료합니다.',
ENTER_PURCHASE_AMOUNT: '구입금액을 입력해 주세요.\n',
ENTER_WINNING_NUMBERS: '당첨 번호를 입력해 주세요.\n',
ENTER_BONUS_NUMBER: '\n보너스 번호를 입력해 주세요.\n',
WINNING_STATISTICS: '\n당첨 통계\n---',
FIRST_PLACE: '6개 일치 (2,000,000,000원) - ',
SECOND_PLACE: '5개 일치, 보너스 볼 일치 (30,000,000원) - ',
THIRD_PLACE: '5개 일치 (1,500,000원) - ',
FOURTH_PLACE: '4개 일치 (50,000원) - ',
FIFTH_PLACE: '3개 일치 (5,000원) - ',
EA: '개',
PURCHASE_QUANTITY: (quantity) => `\n${quantity}개를 구매했습니다.`,
TOTAL_RATE_OF_RETURN: (rateOfReturn) => `총 수익률은 ${rateOfReturn}%입니다.\n`,
});
아래 두 가지 이유로 내가 사용한 방법은 객체 자체가 전역 변수에 할당되는 문제는 발생하지 않을 것 같다고 생각했다.
1. ES6모듈을 사용하면 더는 전역변수를 사용할 수 없다. ES6 모듈은 파일 자체의 독자적인 모듈 스코프를 제공한다. (
모던자바스크립트
, p.207)
2.let
으로 선언한 전역변수는 전역객체의 프로퍼티가 아니다.
즉window.foo
와 같이 접근할 수 없다.let
전역변수는 보이지 않는 개념적인 블록(전역 렉시컬 환경의 선언적 환경 레코드) 내에 존재하게 된다. (모던자바스크립트
, p.214)
ES6의let
,const
키워드로 선언한 전역변수는 전역객체의 프로퍼티가 되지 않고 개념적인 블록 내에 존재하게 된다.(모던자바스크립트
, p.370)
나는 constant.js
파일을 만들어서 MESSAGE
객체에 프로퍼티를 추가하여 사용하였는데 내 결론은 아래와 같다.
ES6 모듈은 파일 자체의 독자적인 모듈 스코프를 제공하기 때문에 MESSAGE
객체는 전역 변수에 할당되지 않을 것이고,
MESSAGE
객체를 const
로 선언해 주었기 때문에 MESSAGE
객체는 전역객체의 프로퍼티가 아니다!
expect().toThrow()
의 동작에 관하여구매금액 숫자가 아니라면 예외가 발생하도록 하는 것에 대한 테스트 코드를 작성하는데 궁금한 점이 있어서 찾아보았다. 위 코드에서 expect
안에 원래 함수를 화살표 함수로 감싼 후 던져줘야 toThrow
에서 예외를 캐치하는 것이었다. (주석처리한 코드는 제대로 작동 안함) 내가 아래 게시글을 보고 이해한 바로는 expect
에서 이미 에러를 던져줘서(?) toThrow
에서 에러를 캐치하지 못하는 것이므로 화살표 함수로 원래 함수를 감싸서 던져줘야 된다고 한다. toThrow
를 사용할 때 주의해야겠다!
참고 포스팅
3주차 미션이 나오고 가장 먼저 한 일은 이번 주 목표에 대해 의미를 되새기는 일이었습니다.
처음엔 3주차 목표에 나온 '도메인 로직(비즈니스로직)'이란 무엇인지 마음에 와닿지 않았었는데 '숫자야구 피드백 강의'를 듣고 프리코스 커뮤니티에서 도메인과 관련한 게시글을 보게 되면서 힌트를 얻을 수 있었습니다.
"이 코드가 현실 문제에 대한 의사결정을 하고 있는가?"라는 질문을 해보면 어떤 게 도메인 로직인지 알 수 있다고 합니다. 로또 미션에서의 도메인 로직은 무엇일까 제가 작성한 기능목록을 보면서 생각해보았습니다.
로또 미션의 큰 로직은 “로또구입 ➡️ 로또발행 ➡️ 등수계산 ➡️ 당첨금계산 ➡️ 수익률계산”이라고 보고 그럼 "로또구입, 로또발행, 등수계산, 당첨금계산, 수익률계산"이라는 현실 문제들에 대한 의사결정을 하는 코드는 무엇일까를 생각해봤을 때, 제가 생각한 로또 미션의 도메인로직을 아래와 같이 뽑아낼 수 있었습니다.
로또 구입
1. 구입금액이 유효한 입력인지 확인하는 기능 ➡️ 로또 구입이 가능한지에 대한 의사결정
2. 발행할 로또 수량을 구하는 기능 ➡️ 로또 구입 매수에 대한 의사결정
로또 발행
1. 중복되지 않는 6개의 숫자를 뽑는 기능 ➡️ 로또발행이라는 서비스 수행
2. 뽑은 6개의 숫자가 유효한 로또번호인지 확인하는 기능 ➡️ 로또발행이 가능한지에 대한 의사결정
등수 계산
1. 당첨번호와 일치하는 번호 개수를 구하는 기능 ➡️ 등수계산이라는 서비스 수행
2. 보너스 번호와 일치하는지 확인하는 기능 ➡️ 2등을 확인하기 위한 의사결정
당첨금 계산
1. 로또 당첨 내역을 구하는 기능(등수 별 당첨 개수) ➡️ '등수 별 당첨 개수'에 따라 당첨금 결정
2. 당첨금을 계산하는 기능 ➡️ 당첨금 계산이라는 서비스 수행
수익률 계산
1. 구입금액이 유효한 입력인지 확인하는 기능 ➡️ '구입금액'에 따라 수익률 결정
2. 당첨금을 계산하는 기능 ➡️ '당첨금'에 따라 수익률 결정
3. 수익률을 계산하는 기능 ➡️ 수익률 계산이라는 서비스 수행
이렇게 도메인 로직에 대해 생각해보고 이에 대한 단위테스트 구현을 시작하였습니다.
이번 주에도 테스트에서 문제가 있었는데..
구매한 로또번호 배열을 출력하는 부분에서 테스트 실패가 나왔습니다.
또 이유를 한참 고민하다가 Expect에서 expect.arrayContaining(array)
라는 메서드가 있는 것을 발견하고 나서 다시 ApplicationTest.js
의 테스트 코드를 보니 여기서는 expect.stringContaining()
메서드를 사용하고 있었습니다. 처음 저의 코드에서는 단순히 Console.print(로또번호를 담은 배열)
을 해주었기 때문에 출력결과는 같아도 string
타입이 아니어서 테스트를 통과하지 못 했던 것이었습니다. 이번 주에도 테스트에 대한 새로운 메서드를 알게 되었습니다.
어떻게 클래스(객체)를 분리할까 고민했습니다. Lotto
와 App
클래스는 나뉘어져 있는 상태에서 클래스 전역에서 필요한 유틸메서드들은 Util
클래스로 분리한 후 구현을 시작했습니다.
이번 주 로또 미션에서는 App
클래스에서 메서드체이닝을 사용했는데 App
클래스 내에 유효성체크를 하는 메서드들이 섞여있는 것은 코드 가독성이 떨어져보여서 이 체이닝에서 분리하고 싶다는 생각을 했습니다. 그래서 유효성체크를 하는 기능은 Validation
클래스로 분리했고 에러메시지나 출력메시지들은 canstant.js
에 객체로 분리해주었습니다. 구현 후에 좀더 고쳐야할 게 있을지 찾아보다가 포비님이 쓰신 글을 보게 되었습니다.
상태 데이터를 가진다고 무조건 setter, getter 메소드를 만드는 습관을 버리자. setter, getter 메소드는 정말 필요한 순간까지 뒤로 미루는 습관을 만들면 좋겠다. 아예 추가하지 않는 연습을 하면 더 좋겠다.
위 내용을 보고 또 저의 잘못을 깨달았습니다. Lotto
객체에 로또번호(상태데이터)를 조회하는 getLottoNumbers
라는 메서드를 만들어놨기 때문입니다. 이 메서드를 사용하지 않고 어떻게 로또번호와 당첨번호, 보너스번호를 비교하는 로직을 처리할 수 있을지 생각했는데 해당 게시글에서 힌트를 얻을 수 있었습니다.
상태 데이터를 꺼내 로직을 처리하도록 구현하지 말고 객체에 메시지를 보내 일을 하도록 리팩토링한다.
Lotto
클래스에 대해 다시 살펴보았습니다.
여기서 로또번호를 꺼내 당첨번호 및 보너스번호와 비교하는 로직을 처리하지 않고 Lotto
객체에서 로직을 처리하도록 리팩토링해주었습니다.
Lotto
클래스
위와 같이 구현함으로써 얻는 장점은 무엇일지 생각해보았는데 객체의 의존성을 줄일 수 있고, 이게 바로 캡슐화이고 객체 간의 대화를 구현한게 아닌가하고 생각했습니다. 앞으로도 getter
, setter
를 구현하기보단 객체 안에서 로직을 처리할 수 있는 코드를 짜야겠다고 다짐했습니다.
스스로 고민하고 문제가 있으면 해결하는 과정들을 매주 반복하면서 저에겐 자신감이 생기고 있습니다.
1주차 공통피드백에 링크되어있던 Git특강에서 메이커준님이 하신 말씀이 떠올랐습니다. 미리 많이 맞아야(?) 두려움이 덜하다라는 뉘앙스의 말이었는데 Git에 대한 두려움을 없애자라는 의미였지만 지금 저의 경우에도 적용될 수 있을 것 같습니다. 수많은 에러와 문제상황을 만나다보면 그것에 대한 두려움은 작아질 것이고 어느 방향으로 가야 이 문제를 해결할 수 있을지 차분히 고민할 수 있는 여유를 가지게 될 것이라 생각합니다. (사실 문제는 어려움이 아니라 낯섦이기 때문에 낯선 것을 익숙하게 만들면 된다고 생각합니다.)
일주일 전인 2주차랑 비교해 봤을 때도 3주차에 npm test
실패가 떴을 땐 크게 당황하지 않고 "아 이건 테스트 코드를 다시 보고, jest에 관한 문서를 찾아보면 되겠다."하고 문제 해결의 방향성을 잡을 수 있었습니다.
그리고 이런 경험들을 통해 우테코의 교육 철학을 다시금 느낄 수 있었습니다. 잡아준 물고기를 받는 것이 아닌 물고기를 잡는 방법을 스스로 터득하는 방식으로요. 티칭에 익숙했던 저이지만 확실히 스스로 해결할 힘을 길러주는 것 그리고 학습의 즐거움을 알게 해주는 것은 코칭이라고 느꼈습니다.
객체의 상태데이터를 꺼내지 않고 객체 안에서 로직을 처리하기 위해 로또미션에서
Lotto
객체 안에서 해주었는데 이렇게 하면 안 되는 거였다!getter
를 무조건 사용하지 말라는 말이 아니라 출력을 위한 값 등 순수 값 프로퍼티를 가져오기 위해서라면 어느정도getter
는 허용 되는 것이었다! 나의 의문이 풀렸다.
이것은
코드리뷰
를 하면서 느낀 것인데 외부에서 객체의 상태를 직접 접근한다면 상태데이터가 언제 어떻게 변경이 될지 예측이 잘 되지 않고 코드예측 및 파악이 더 힘들어지는 것 같다. 그리고 이런 부분이 유지보수를 어렵게 하는 이유가 될 것 같다. 따라서 이러한 이유로 객체의 상태를 외부에서 직접 접근하는 방식을 최소화해야 한다고 생각된다.
이 부분을 3주차 공통피드백에서 보고 너무 놀랐다.
위 소감문에서 썼듯이 로또미션 구현 중 객체를 객체스럽게 사용하도록 리팩토링해라.라는 포비님의 글을 보게 되어서getLottoNumbers
를 사용하지 않고Lotto
객체에서 로직을 수행하도록 리팩터링해주었는데 마침 이것과 같은 피드백이 나와서 말이다..! 미션에서 의도한 바를 미리 알고 적용해봤다는 점에서 내가 잘 가고있구나라고 생각했다.
여기에
LottoResult
라는 클래스가 예시로 나오는데 나는 객체라는 것의 개념을 착각하고 있었던 것 같다. 로또라는, 명확하게 파악되는 것들만 객체로 만들려고 했었다.LottoResult
라는 클래스를 생성해낼 생각을 왜 못했을까? 오늘도 반성한다. 그리고.. 반성 끝! 4주차 미션엔 클래스 분리를 똑부러지게 잘 하고싶다.
Be the best version of you!