이번에 만들 것은 로또(우테코 2주차 깃허브 링크)였다. 간단히 말하면 로또를 구매하고, 당첨 번호와 보너스 번호를 입력하여 로또에 당첨된 결과를 알려주는 것이었다.
추가된 요구 사항은 크게 정리하자면
적고 보니 추가된 요구 사항을 제대로 지키지 않은 것 같다. 함수가 한 가지 일만 하도록 하는 것에 집중해서 길이가 15라인을 넘는지 아닌지는 체크하지 않았다. 다음에는 15라인을 확인해가면서 해야겠다.
else를 지양하는 것도 다소 논란이 되는 부분인 것 같긴 한데, else를 사용하면 함수의 depth가 깊어져서 가독성이 떨어지는 부분 때문에 early return을 권장한다고 생각한다. 물론 else구문이나 switch 구문을 사용하는 것이 나은 경우도 있겠지만, 이번 경우에는 if구문을 사용하는게 나아 보였다.
도메인 로직이라는 말은 잘 몰라서 검색을 해 봤다. 간단히 말하면 코드에는 크게 도메인 로직과 서비스 로직이라는 게 있는데, 도메인 로직은 실제 비즈니스에 상관 있는 코드, 서비스 로직은 그 비즈니스를 하기 위해서 도와주는 로직이다. 이론상으로는 간단한데 분리하는게 쉽지는 않았다. 관련된 자료를 찾아보던 중 도메인과 서비스를 분리하기 어렵다면 기능 단계의 재정의가 필요하다는 자료를 읽었고, 반복되는 재정의로 도메인과 서비스를 분리했다. 마지막까지 계속 도메인 로직과 서비스 로직 정의를 반복할 정도로 헷갈리는 부분이었다.
가이드라인을 확인한 후
1. 기능 목록 정리하기
2. 테스트 코드 작성(단위 테스트)
3. 기능 구현
4. pull request 보내기
5. node로 구현 확인하기
순으로 진행하였다.
이 글의 마지막에는 피드백과 소감을 적어 보았다.
깃허브 레포지토리는 여기이니 코드 리뷰 남겨 주시면 감사하겠습니다. 😄
고심 끝에 정리한 최종 기능 목록은 다음과 같다. 처음부터 이렇게 작성한 건 아니고 계속 수정을 거듭했다. 이번 기능 목록은 앞부분을 체크박스로 만들었다. 커밋에서 어떤 부분을 작업했는지 명확하게 알려 주기 위해서다. 그리고 옆에 이 기능을 어떤 함수로 만들어 줬는지 적어 주었다.
# 로또
## 💼 기능 목록
### 📍 도메인
- [x] 유효한 로또 구입 금액을 입력받는다 - Purchase#validate()
- [x] e) 로또 금액이 숫자가 아닌 경우
- [x] e) 로또 금액이 0보다 작은 경우
- [x] e) 로또 금액 금액이 1000원으로 나누어 떨어지지 않는 경우
- [x] 숫자를 랜덤하게 뽑아 오름차순 정렬한다. - MyLotto#generateRandom()
- [x] 숫자는 중복되지 않는다.
- [x] 숫자는 1 ~ 45 사이이다.
- [x] 숫자는 6개이다.
- [x] 유효한 숫자 6개를 입력받는다. - Lotto#validate()
- [x] e) 숫자 범위가 1 ~ 45가 아닌 경우
- [x] e) 숫자가 6개가 아닌 경우
- [x] e) 중복이 존재하는 경우
- [x] 유효한 보너스 번호를 입력받는다. - Bonus#validate()
- [x] e) 숫자 범위가 1 ~ 45가 아닌 경우
- [x] 로또 번호와 당첨 번호를 비교한다. - Result#compare()
- [x] 3개 일치 : 5,000원
- [x] 4개 일치 : 50,000원
- [x] 5개 일치 : 1,500,000원
- [x] 5개 일치 + 보너스 번호 일치 : 30,000,000원
- [x] 6개 일치 : 2,000,000,000원
### 📍 서비스
- [x] 로또 구입 금액/1000개를 구매했다는 메시지를 띄운다. -
Service#printLottoCount()
- [x] 중복되지 않은 6개의 숫자를 로또 구매 개수만큼 출력한다. -
Service#printLottoNumbers()
- [x] 당첨 번호 입력 메시지를 띄운다. - Service#printGetWinningNumber();
- [x] 보너스 번호 입력 메시지를 띄운다. - Service#printGetBonusNumber();
- [x] 당첨 통계를 출력한다. - Service#printResult()
테스트 코드 자체는 수월하게 작성했다. 다만 이번 테스트에서는 로또 번호의 유효성 검사를 할 때 Lotto 내부의 validate()함수를 부르지 않고 그냥 new Lotto()로 불러줬다. 그 이유는 Lotto 클래스 내부의 constructor에서 자동으로 validate()를 불러주도록 코드가 작성되었기 때문이다. 이 과정을 보면서 constructor 내부에 작성된 코드는 클래스를 부를 때 자동으로 실행되는 것을 알 수 있었다.
describe('로또 클래스 테스트', () => {
test('로또 번호의 개수가 6개가 넘어가면 예외가 발생한다.', () => {
expect(() => {
new Lotto([1, 2, 3, 4, 5, 6, 7]);
}).toThrow('[ERROR]');
});
기능을 구현할 때는 이러한 조건도 있었다.
class Lotto {
#numbers;
constructor(numbers) {
this.validate(numbers);
this.#numbers = numbers;
}
validate(numbers) {
if (numbers.length !== 6) {
throw new Error();
}
}
// TODO: 추가 기능 구현
}
일단 Lotto에 필드를 추가할 수 없다는 조건은 이 클래스에서 로또 당첨 번호, 내 로또 번호, 보너스 번호를 한 번에 받아오지 말라는 제약 조건을 만든다고 생각했다. 따라서 클래스를 분리해 각 번호를 받아오는 클래스를 만들기로 했다.
# prefix라는 개념은 몰라서 알아보았다. 변수 앞에 #을 붙이면 private 필드가 되며, 이 변수는 클래스 내부에서만 접근 가능한 필드가 된다. 개념 자체는 이해했는데 #을 왜 붙여야 하는 지는 이해가 잘 되지 않았다. 어차피 이 값을 lotto.#numbers로 받아오지 않아도 다른 경로로 받아올 수 있는데 굳이 붙여야 하나 하는 생각이 들었다. 안전성 때문이라고는 하는데, 큰 오류를 발생시킬 수 있는 구체적인 예제를 아직 못 찾아서 아직까지는 잘 모르겠다. 자바 빈 설계 규약에 따르면 무조건 getter과 setter을 만들어야 한다는데, setter을 사용하지 않으면 굳이 만들어야 하나 싶기도 했다.
처음에 클래스를 분리하고 import해도 클래스를 다른 파일에서 사용할 수 없어서 당황스러웠다. 알고보니 module.exports를 하지 않아서였다. 이전까지는 module.exports가 뭔지 몰랐지만 기능을 사용하는데 문제가 없어서 무시하고 넘어갔는데, 클래스 분리를 하면 반드시 export를 해 줘야 import를 할 수 있다는 것을 알게 되었다.
또한 고민이 되었던 지점은 상수 분리였다. 하드코딩을 피하기 위해서 상수를 생성하고 상수를 한 폴더 constant에 모아 두었는데 메시지들을 통일성 없게 export 했다는 생각이 들었다. 메시지의 종류를 그룹화하면서도 통일성 있게 export 하는 방법의 고민이 필요해 보인다.
이번에는 pull request를 보내고 나서도 부족한 부분이 계속 보여 수정을 거듭했다. 덕분에라고 하긴 뭐하지만 pull request를 보내고 나서도 내 레파지토리에 push하면 pull request에 반영된다는 것을 확인할 수 있었다.
저번 주차에는 node를 돌려 보라는 말이 npm test로 테스트 케이스를 돌려 보라는 이야기인줄 알았다. 그런데 숫자 야구 피드백 강의에서도 프로그램을 실행하는 걸 보니 아무래도 이상해서 찾아 보았다. 알고보니 터미널에 node src/App.js 를 입력하면 프로그램을 실행할 수 있었다.
터미널로 실행해 보니 내 프로그램은 입력을 전혀 받지 못하고 Console.print 부분만 실행되고 있었다. 저번 주차에도 예감하고 있었지만 아무래도 Console.readLine 부분에서 문제가 생긴 것 같았다. 깃허브 discussion을 뒤져본 결과 Console.readㅣine은 비동기 코드지만 async/await 구문을 사용하면 테스트 케이스에서 오류가 생기므로 동기적으로 처리해줘야 한다는 것을 알 수 있었다. 그래서 콜백 함수를 콜백 함수에 담아 주었다.
getLottoCount() {
Console.readLine(ServiceMessage.PURCHASE_INPUT, (amount) => {
this.printLottoCount(amount);
});
}
이해를 돕기 위해 살짝 생략해 주었다. 이런 식으로 Console.readLine 함수 값이 있어야 다음 printLottoCount()가 호출이 되도록 연결해 주면 비동기 함수를 동기적으로 처리할 수 있다.
다만 이 과정을 거치면서 저번 주는 0점일수도 있겠다.. 라는 생각이 들었다. node에서 실행해 보니 제대로 작동하지 않았기 때문이다. ㅎㅎ 하.. 그래도 이제라도 node로 실행시키는 방법을 알아서 다행이라고 생각한다.
함수(메서드) 라인에 대한 기준
프로그래밍 요구사항을 보면 함수 15라인으로 제안하는 요구사항이 있다. 이때 공백 라인도 한 라인에 해당한다. 15라인이 넘어간다면 함수 분리를 위한 고민을 한다.
발생할 수 있는 예외 상황에 대해 고민한다
비즈니스 로직과 UI 로직을 분리한다
객체의 상태 접근을 제한한다
필드는 private class 필드로 구현한다. 객체의 상태를 외부에서 직접 접근하는 방식을 최소화 하는 이유에 대해서는 스스로 찾아본다.
객체는 객체스럽게 사용한다
필드의 수를 줄이기 위해 노력한다
성공하는 케이스 뿐만 아니라 예외에 대한 케이스도 테스트한다
테스트 코드도 코드다
테스트를 위한 코드는 구현 코드에서 분리되어야 한다
단위 테스트하기 어려운 코드를 단위 테스트하기
이번에는 빨리 진행해서 다른 사람의 코드도 살펴보고 피드백도 받고 싶었는데 나에겐 빡빡한 과제였다.. 한 주가 끝나면 자신감이 생겨서 이 정도도 할수 있구나! 하고 뿌듯해지는데 다음주 과제를 시작하면 모르는 게 너무 많아서 다시 쪼그라든다. 코드를 작성하는 시간보다 모르는 개념을 찾아보고 이 개념을 왜 써야 하는지 이해하는 시간이 더 많은 것 같다. 모르는 게 많은 건 좀 슬프지만 그래도 공부하는 방법을 점점 더 알게 되고 있다. 아직 3주차지만 혼자 공부한 것보다 많은 것을 배우고 있다.
다른 사람의 피드백은 역시 중요하다는 생각이 든다. 내가 혼자 짰을 때는 꽤 괜찮은 코드인 기분이었는데 피드백을 받고 여러가지 기준을 적용해보니 부족한 점이 많이 느껴진다. 부족한 점을 지적받아서 오히려 기분이 좋다. 다른 사람에게 피드백 받아 더 나은 코드를 짤 수 있는 귀한 기회이기 때문이다!!! 다음 주면 끝난다는 사실이 많이 아쉽다.