요구 사항
요구사항은 과제 진행 요구 사항, 기능 요구 사항, 프로그래밍 요구 사항 세 가지로 구성되어 있었다.
과제 진행 요구 사항
- 기능을 구현하기 전 README.md에 구현할 기능 목록을 정리해 추가한다.
- Git의 커밋 단위는 앞 단계에서 README.md에 정리한 기능 목록 단위로 추가한다.
- Angular JS Git Commit Message Conventons를 참고해 커밋 메시지를 작성한다.
기능 요구 사항
간단한 로또 발매기를 구현한다.
- 로또 번호의 숫자 범위는 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원
- 로또 구입 금액을 입력하면 구입 금액에 해당하는 만큼 로또를 발행해야 한다.
- 로또 1장의 가격은 1,000원이다.
- 당첨 번호와 보너스 번호를 입력받는다.
- 사용자가 구매한 로또 번호와 당첨 번호를 비교하여 당첨 내역 및 수익률을 출력하고 로또 게임을 종료한다.
- 사용자가 잘못된 값을 입력할 경우 IllegalArgumentException 을 발생시키고, "[ERROR]"로 시작하는 에러 메시지를 출력 후 그 부분부터 입력을 다시 받는다.
- Exception 이 아닌 IllegalArgumentException , IllegalStateException 등과 같은 명확한 유형을 처리한다.
입력
로또 구입 금액
- 예외 처리
- 문자가 입력된 경우
- 빈 문자열이거나 null인 경우
- 양의 정수가 아닌 경우
- +를 붙이는 경우
- 1,000원 단위로 나누어 떨어지지 않는 경우
- `int`로 발행할 로또 수량 반환 (로또 1장의 가격은 1,000원이다.)
당첨 번호 입력
- 예외 처리
- 빈 문자열이거나 null인 경우
- 맨 앞이나 맨 뒤에 ,가 입력된 경우
- 문자가 입력된 경우
- 양의 정수가 아닌 경우
- List<Integer>로 반환
보너스 번호 입력
- 예외 처리
- 빈 문자열이거나 null인 경우
- 숫자가 아닌 경우
- 숫자 범위가 1 ~ 45를 벗어난 경우
- 당첨 번호의 숫자와 겹치는 경우
- 양의 정수가 아닌 경우
- int로 반환
로직 구현
- Lotto 생성시 예외 처리 사항
- 숫자 범위가 1~45 범위를 벗어난 경우
- 숫자의 개수가 6개가 아닌 경우
- 중복된 숫자가 있는 경우
- 로또 수량만큼 중복되지 않는 6개의 숫자 세트 생성
- 각 로또 번호 세트와 당첨 번호 및 보너스 번호의 일치 여부 확인
- 당첨 번호와 일치하는 숫자 개수 확인
- 5개가 일치하는 경우, 보너스 번호와의 일치 여부 추가 확인
- 수익률을 계산하고, 소수점 둘째 자리에서 반올림
출력
발행한 로또 수량 출력
X개를 구매했습니다.
발행한 로또 번호 출력
[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]"로 시작해야 한다.
[ERROR] 로또 번호는 1부터 45 사이의 숫자여야 합니다.
구현하기
이번 3주차의 목표는 서비스 계층 도입, 구조 개선, 예외 처리, 검증, 상수화, 테스트 코드 등을 염두에 두고 코드를 작성했다.
전체 코드
폴더 구조
src
├── main
│ └── java
│ └── lotto
│ ├── Application.java
│ ├── constant
│ │ └── LottoConstants.java
│ ├── controller
│ │ └── LottoController.java
│ ├── domain
│ │ ├── Lotto.java
│ │ ├── LottoRank.java
│ │ ├── Lottos.java
│ │ └── WinningLotto.java
│ ├── exception
│ │ └── ExceptionMessage.java
│ ├── input
│ │ ├── BonusNumberProcessor.java
│ │ ├── PurchaseAmountProcessor.java
│ │ └── WinningNumberProcessor.java
│ ├── service
│ │ ├── LottoGenerator.java
│ │ ├── LottoService.java
│ │ └── ResultCalculator.java
│ ├── util
│ │ ├── InputUtil.java
│ │ └── LottoUtils.java
│ └── view
│ ├── input
│ │ ├── Input.java
│ │ └── InputView.java
│ └── output
│ ├── Output.java
│ └── OutputView.java
└── test
└── java
└── lotto
├── ApplicationTest.java
├── domain
│ ├── LottoRankTest.java
│ ├── LottoTest.java
│ ├── LottosTest.java
│ └── WinningLottoTest.java
├── input
│ ├── BonusNumberProcessorTest.java
│ ├── PurchaseAmountProcessorTest.java
│ └── WinningNumberProcessorTest.java
└── service
└── ResultCalculatorTest.java
이번 미션 또한 입력 → 로직 → 출력 순으로 코드를 구현해나갔다. 자세히 말하자면, 입력 로직에 관한 validation → 입력 관련 domain → InputView → 로직 → 전체 코드 완결 → OuputView 순서이다.
프리소스 커뮤니티를 통해 인텔리제이로 다이어그램을 만들 수 있다는 걸 알게 되어 인텔리제이로 만들어보았다.
고민한 내용
어떻게 하면 입력 값과 관련된 domain이 아닌 로직 관련 부분도 TDD를 잘 수행할 수 있을까?
이건 저번 주차에서도 잘 수행하지 못해 회고를 했었는데 이번 주차도 잘 수행하지 못했던 부분이다. 이번에는 좀 더 차근차근 설계를 해보려고 했다. README 파일도 다시 읽으면서 어떤 테스트 로직을 작성할지 고민했다. 하지만 어떤 클래스로 나눌까, 어떻게 나눌까 등등 계속 복잡하게 생각하려고 했고 시간을 너무 많이 잡아먹어 TDD를 포기하고 그냥 코드를 작성했다...
그렇게 미션을 제출하고 난 후, 제출했던 지원서부터 다시 읽기 시작했다. 그리고 몰입에 관해 적었던 말을 다시 보았다.
몰입을 통해 얻은 교훈은 너무 복잡하게 생각하기보다는 단순한 관점에서부터 시작하는 비움의 미학이었습니다. 처음부터 너무 깊이 생각하면 오히려 더 복잡해져버릴 때가 많았고, 기본적인 원리와 단순한 접근으로 단계적인 문제 해결을 이어나갔을 때 비로소 파훼되곤 했습니다.
좀 더 차근차근하게 설계를 하려고 어떤 클래스로 나눌지 등 여러 가지를 고민했는데 지금 보니 너무 복잡하게 접근했던 것 같다. 마지막 미션에는 우선 로직 관련 테스트 코드는 한 클래스에 모두 작성한 후, 필요한 클래스로 나누어보면 더 수월하게 TDD를 진행할 수 있을 것 같다.
각 클래스가 명확한 역할과 책임을 가질 수 있도록 하려면 어떻게 해야 할까?
1주차 미션에서는 view 관련해서 지적을 받았고, 그 이후로는 `print`와 관련된 모든 부분을 `view` 파일에 넣어두었다. 그런데 이번 미션에서 `service` 계층을 추가하면서 헷갈리기 시작했다. service가 비즈니스 로직을 담당하는 계층이라는데, 그 경계가 어디까지인지 애매하게 느껴졌다.
이전 카테캠 프로젝트에서는 코드를 제출일까지 완성하기에 바빠 `controller`가 사용자의 입력을 받아 service 메서드에 전달하고, 그 메서드에서 반환된 값을 단순히 다시 반환하는 식으로 무작정 외우고 작성했다. 그러다보니 처음에 service 계층을 도입할 때 막막했지만, 관련 내용을 공부하면서 나름의 기준을 세웠다.
그렇게 내가 내린 결론은 모든 로직을 service에 넣고, controller는 입력과 서비스를 연결하는 역할에 집중하게 하는 것이다.
Enum 유용하게 사용하기
이번 미션에서는 enum을 적용하여 프로그램을 구현하라는 말이 있었다. 로또 당첨 결과 부분을 enum으로 작성하라는 말로 생각했다. 그런데 마침 저번 주 코드 리뷰로 에러 메시지도 enum으로 작성하는 게 어떠냐는 말을 들어서 이 부분도 단수 상수인 String에서 enum으로 적용을 했다.
나는 처음에 단순하게 enum을 선택지를 한정해 놓을 수 있는 기능이라고 생각했는데 이번 기회를 통해 enum을 공부하면서 enum에 필드를 넣을 수 있는 등 생각보다 더 다양한 기능을 할 수 있다는 사실을 알았다. 처음에는 예제에서 보았던 것처럼 `Lotto`에 `LottoRank` enum을 추가해서 EnumMap을 이용한 메서드를 만든 후, 이를 이용해 각 Rank의 개수를 구하려고 했다. 하지만 `Lotto`에 `numbers` 이외에 다른 필드를 추가할 수 없다는 제약 조건을 본 후 고민을 했다.
`Lotto`와 `rank`를 합친 클래스를 따로 만들까? - `LottoResult`
그런데 이렇게 되면 `Lotto`의 리스트로 이루어진 `Lottos`도 있어야 하고 `LottoResult`의 리스트로 이루어진 것도 있어야 하면 너무 번거롭지 않을까??
이렇게 생각했었는데 지금 생각해보니 그냥 처음 로또를 구매할 때, `LottoResult`에 바로 넣고 초기화할 때, rank에 `None` 값을 주었으면 될 것 같기도 하다. 근데 이러면 `Lotto`가 있는 클래스가 너무 많아지지 않나...? 객체 지향적으로 생각하려는데 아직 너무 부족한 것 같다...
그래서 이번에는 그냥 `ResultCalculator` 클래스를 만들어 이 안에 `Map<LottoRank, Integer> result`를 필드로 사용하였다. 그리고 `TreeMap`을 통해 키 값을 자동으로 정렬하여 저장되게 해서 결과를 print할 때, 순서대로 나올 수 있게 만들었다. 근데 이것이 enum을 유용하게 쓴 것이 맞을까?라는 생각이 들었다. 좀 더 잘 정리해서 쓸 수도 있지 않을까...
예외 메시지 꼼꼼하게 처리하기
이번에는 예외 처리를 더 꼼꼼하게 했습니다. 예를 들어, 사용자가 +기호를 입력한 경우, "[ERROR] 양의 부호를 포함할 수 없습니다."는 메시지를 반환하도록 했다. 단순히 입력이 잘못되었다는 메시지를 주기보다는 구체적인 조언을 주는 것이 사용자에게 더 도움이 된다고 생각했기 때문이다. 마찬가지로, 소수점을 입력한 경우에도 해당 오류에 맞는 메시지를 반환하여 사용자가 입력 오류를 쉽게 이해할 수 있도록 했다.
그리고 생각은 했지만 미처 적용하지 못했던 부분이 있다. 지금 코드는 잘못된 값을 입력하면 에러 메시지가 뜨고, 그리고 다시 "~을 입력해주세요." 메시지가 뜨는데 그렇게 하기보다는 "~을 입력해주세요" 메시지는 딱 처음 한 번만 출력되고, 에러 메시지 뒤에 "다시 입력해주세요"를 덧붙였으면 더 좋았을 것 같다.
validation 관련 코드는 어디에?
이건 1주차부터 고민했던 문제인 것 같다. 어디에 넣는 게 맞을까?
나는 우선 domain 관련 검증은 다 domain 안에서 관리하는 것이 책임에 맞다고 생각했다. 그래서 예를 들어, 보너스 번호가 1 ~ 45 사이의 값인지나 로또 당첨 번호와 겹치지 않는지는 `BonusNumber` 도메인 안에서 관리하도록 하였다. 그리고 사용자의 입력 값에 대한 기본 적인 검증(ex. null이나 빈 문자열 체크)는 따로 해당 입력값에 대한 processor 클래스를 만들어서 이 곳에서 처리했다.
나와 같은 고민을 하는 사람이 있지 않을까 싶어 프리코스 커뮤니티를 보니 역시나 존재했다. validation 전용 클래스를 따로 만들거나 service 계층에서 처리하는 사람이 많았다. 하지만 나 같은 경우에는 예외 처리를 좀 길게 작성했는데 이럴 경우에는 validation 클래스를 따로 만들면 여러 도메인에 관한 많은 메서드들이 섞여서 다소 복잡해 보일 것 같다는 생각을 했고 `BonusNumberProcessor` 같이 입력 값에 대해 처리를 하는 클래스를 `Input` 패키지를 만들어 넣어주었다.
저번 주에는 입력 처리를 위한 클래스를 `util` 패키지를에 넣었지만 `util`은 주로 반복적으로 사용하는 코드를 모아두는 곳이라 생각되어 이번에는 `Input` 패키지를 별도로 만들었다. 그리고 null이나 빈 문자열 체크처럼 반복해서 사용하는 기능은 `InputUtil` 클래스를 따로 만들어 이 곳에 넣어주었다.
그런데 지금 보니 `Input` 패키지를 service 폴더 아래에 넣는 게 더 적절하지 않을까 하는 생각이 든다.
실수한 부분
이번에는 너무 큰 실수를 했다... 어떻게 하면 좀 더 간단한 코드가 될까? 생각을 하면서 리팩토링을 계속 했는데 그러다가 당첨 번호는 정상적으로 되었는데 보너스 번호에서 오류가 나는 경우, 당첨 번호부터 다시 입력하게 만드는 로직이 되어버렸다......... 처음에 이 부분 만들 때, 신경써서 코드를 짰는데 리팩토링하면서 그냥 날렸다ㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠ 너무 슬프다.....
그리고 리팩토링하면서 더 이상 쓰지 않지만 지우지 않은 코드가 꽤 있었다..... 이것도 꼼꼼히 체크해야겠다............ 미션 제출 전까지 계속 코드 리팩토링을 하다보니 확인을 제대로 못했었다....
학습하고 사용한 것들
[AssertJ] Exception Assertions
코드 리뷰 후기
[우아한테크코스] 프리코스 3주차 코드 리뷰 후기
[우아한테크코스] 프리코스 3주차 회고 코드 리뷰 피드백 요약public void run() { int purchaseCount = setPurchaseCount(); output.printLottoCount(purchaseCount); Lottos lottos = lottoService.generateLottos(purchaseCount); output.printLo
best11gh.tistory.com
'우아한테크코스' 카테고리의 다른 글
[우아한테크코스] 프리코스 4주차 회고 (0) | 2024.11.12 |
---|---|
[우아한테크코스] 프리코스 3주차 코드 리뷰 후기 (0) | 2024.11.07 |
[우아한테크코스] 프리코스 2주차 코드 리뷰 후기 (0) | 2024.10.31 |
[우아한테크코스] 프리코스 2주차 회고 (0) | 2024.10.31 |
[우아한테크코스] 프리코스 1주차 코드 리뷰 후기 (1) | 2024.10.30 |