우테코 7기 프리코스 3주차 회고
🎬 Intro
우테코 프리코스 3주차 회고를 적어봅니다~
1️⃣ 요구사항 파악하기
3주차에는 1,2주차 요구사항에 다음과 같은 내용이 더 추가되었습니다.
✅ 클린코드를 위한 요구사항
- 메서드의 길이가 15라인을 넘어가지 않도록 구현한다.
- else 예약어를 쓰지 않는다.
✅ Java Enum을 적용하여 프로그램을 구현한다.
코드 리뷰를 하면서 많은 크루원들이 Enum을 사용하여 에러메세지를 정의하는 것을 보았습니다. 그래서 Enum을 사용하면 어떤 장점이 있는지 한번 찾아 보았습니다.
- 타입 안정성 보장
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Enum 사용하지 않을 때
public static final String USER = "USER";
public static final String ADMIN = "ADMIN";
// 오타나 다른 문자열도 들어갈 수 있음
String role = "USEER"; // 오타지만 컴파일 에러 없음
// Enum 사용
public enum Role {
USER, ADMIN
}
Role role = Role.USER; // 컴파일러가 체크해줌
// Role role = "USER"; // 컴파일 에러
- IDE 자동완성
1
2
3
4
5
6
public enum PaymentType {
CARD, CASH, BANK_TRANSFER
}
// IDE가 자동완성 제공
PaymentType type = PaymentType.CARD; // IDE가 가능한 값들을 보여줌
- 추가 데이터와 메서드를 가질 수 있음
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public enum Grade {
A(90, "우수"),
B(80, "좋음"),
C(70, "보통"),
D(60, "미흡");
private final int minScore;
private final String description;
Grade(int minScore, String description) {
this.minScore = minScore;
this.description = description;
}
public static Grade getGradeByScore(int score) {
for (Grade grade : values()) {
if (score >= grade.minScore) {
return grade;
}
}
return D;
}
}
// 사용
Grade grade = Grade.getGradeByScore(85);
이 외에도 데이터를 데이터베이스에 저장할 때나 네트워크로 보낼때 직렬화 과정이 필요 없다는 장점도 있었습니다.
2️⃣ 문제 분석 하기
사용자가 입력한 구입 금액 만큼 로또 번호를 생성한 뒤, 당첨 번호를 입력 받아 당첨 결과를 출력하는 것이었습니다.
✅ 구입 금액을 int 타입으로 제한
int의 최대값은 10e9, 로또 1장의 가격은 1,000원 이므로 최대 10e6의 시간 복잡도가 넘지 않게끔 구입금액을 int 타입으로 제한 하였습니다. 그 외 로또 번호의 숫자 범위, 중복 여부 등은 요구사항에 나와 있는 사항을 따랐습니다.
3️⃣ 기능 구현 목록 작성
2주차와 마찬가지로 업데이트 내용을 두어 해당 문서가 변경될 수 있음을 표현 하였습니다.
✨ 업데이트 내용
- 숫자가 2,147,483,647(Integer.MAX_VALUE)를 넘을 경우 예외 처리
- 총 수익률 계산을 위한 LottoProfit 객체 구현
- 유효성 검증 클래스 이름에
Lotto
키워드 제거 - ListValidator, NumberValidator 클래스에 제네릭 적용
- 단일 숫자일 경우 InvalidNumberException, 복수 숫자일 경우 InvalidNumbersException 으로 예외 처리 분리
- LottoGenerator 추상화 및 QuickLottoGenerator(자동 로또) 구현
✅ 진행과정
1. 로또 구입 금액을 입력 받는다.
- 구입 금액은 1,000원 단위만 가능
- 구입 금액의 최대는 Integer.MAX_VALUE로 제한
2. 당첨 번호를 입력 받는다.
- 번호는
,
(쉼표) 기준으로 구분 - 당첨 번호를 중복 불가
- 당첨 번호는 1 부터 45사이 자연수만 가능
3. 보너스 번호를 입력 받는다.
- 보너스 번호는 당첨번호와 중복 불가
- 보너스 번호는 1 부터 45사이 자연수만 가능
4. 발행한 로또 수량 번호를 출력한다.
- 로또 번호는
오름차순
으로 정렬
5. 당첨 내역을 출력한다.
- 당첨 내역은 5등 부터 1등까지 차례대로 출력
- 등수마다 당첨된 갯수 포함
6. 수익률을 출력한다.
- 수익률은 소수점 둘째 자리에서 반올림 (ex. 100.0%, 51.5%, 1,000,000.0%)
7. 예외 상황 시 에러 문구를 출력해야 한다.
- 에러 문구는
[ERROR]
로 시작
✅ 실행 결과 예시
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
구입금액을 입력해 주세요.
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%입니다.
✅ 클래스 및 기능
1. domain
Lotto
- 로또 번호의 정보를 갖고 있음
- 특정 숫자가 로또 번호에 있는지 판단
예외 처리
- 번호가 중복된 경우
- 번호가 1~45를 벗어난 경우
Money
- 구입 금액의 정보를 갖고 있음
- 1,000으로 나눈 몫을 반환 (로또 티켓 수)
예외 처리
- 1,000 단위가 아닌 경우
QuickLottoGenerator
- 자동 로또를 발행
- LottoGenerator의 구현체
LottoStore
- 로또를 구입금액 만큼 발행
WinningNumbers
- 당첨 번호와 보너스 번호의 정보를 갖고 있음
- 당첨 번호에 특정 숫자가 있는지 판단
BonusNumber
- 보너스 번호의 정보를 갖고 있음
- 특정 숫자가 보너스 번호와 일치하는지 판단
예외 처리
- 보너스 번호가 1~45 사이의 자연수가 아닌 경우
- 보너스 번호가 당첨 번호와 중복 되는 경우
WinningResult
- 로또 당첨 결과를 반환
LottoProfit
- 총 수익률을 계산하여 반환
LottoRank
- 로또 당첨 정보를 갖고 있음
2. util
ListValidator
- 리스트 사이즈 검증
- 리스트 중복 검증
- 리스트 요소 범위 검증
NumberValidator
- 숫자 범위 검증
- 숫자 단위 검증
InputConvertor
- 문자열을 숫자 리스트로 변환
- 문자열을 숫자로 변환
3. constant
LottoRule
- 로또 규칙을 정의
4. error
ErrorMessage
- 에러 메세지를 정의
AppException
- IllegalArgumentException의 하위 클래스
InvalidNumberException
- 숫자 관련 예외 클래스
- AppException의 하위 클래스
InvalidNumbersException
- 여러 숫자 관련 예외 클래스
- AppException의 하위 클래스
5. view
ConsoleInputView
- 사용자의 콘솔 입력을 읽음
- InputView의 구현체
ConsoleOutputView
- 사용자에게 결과를 콘솔 출력
- OuputView의 구현체
6. controller
- 애플리케이션 흐름을 제어
4️⃣ 구현하기
이번 미션은 5시간 이내로 구현 하는 것을 목표로 하였지만, 요구한 기능을 완성하는데 까지 총 7시간 정도가 소요되었습니다.
✅ 클래스 다이어그램
1. Main
2. Util
3. Constant
4. Error
5. View
5️⃣ 고민한 점
✅ 추상화와 DI 꼭 해야할까??
항상 설계를 할 때 테스트 용이성을 위해 벨리데이터 로직은 추상화와 DI를 고민하였습니다. 하지만 이번에는 구현 시간을 확보하기 위해 불필요한 추상화를 하지 않으려고 노력하였습니다. 그 과정 속에서 2주차 코드 리뷰로 좋은 리뷰어에게 답변 받았던 내용이 많은 도움이 되었습니다.
위 와 같이 도메인 내부에 벨리데이터 객체를 생성하는것도 응집도, 가독성, 짧은 구현 시간 등 장점이 존재했습니다. 그래서 결과적으로 모든 벨리데이터 객체는 도메인 내부에서 생성하는 것으로 결정하였습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class Lotto {
private final List<Integer> numbers;
public Lotto(List<Integer> numbers) {
validate(numbers);
List<Integer> lottoNumbers = new ArrayList<>(numbers);
lottoNumbers.sort(Comparator.naturalOrder());
this.numbers = lottoNumbers;
}
public boolean isContainsNumber(final int number) {
return numbers.contains(number);
}
public List<Integer> getNumbers() {
return Collections.unmodifiableList(numbers);
}
private void validate(List<Integer> numbers) {
final ListValidator<Integer> listValidator = ListValidator.getInstance();
final NumberValidator<Integer> numberValidator = NumberValidator.getInstance();
listValidator.validateSize(numbers, LottoRule.WINNING_NUMBER_SIZE)
.validateRange(numbers,
number -> numberValidator.validateRange(number, LottoRule.MIN_NUMBER, LottoRule.MAX_NUMBER))
.validateDuplicate(numbers);
}
}
그리고 이런식으로 도메인 내부에 구현하고 보니 다음과 같은 장점도 있었습니다.
- 벨리데이터를 주입받고 사용하기 위한 정적 팩토리 메서드가 필요 없음
- 동시에 정적 팩토리 메서드가 갖는 파라미터 갯수도 줄어듬
그래서 다른 도메인 역시도 위와 같이 도메인 내부에서 벨리데이터 객체를 생성하는것으로 구현하였습니다.
✅ SRP를 위한 기준 세워보기
2주차 부터 학습 목표에 함수를 쪼개고, 관련 함수를 묶어 하나의 객체로 만들라는 내용이 있었습니다. 이는 SRP를 잘 준수하여 객체의 책임 분리를 명확하게 하라는 것으로 이해를 했습니다. 하지만 객체의 책임을 나눌 때 뚜렷한 기준이 없어 리팩토링 하는데 많은 시간이 소요되었습니다. 그래서 고민 끝에 저 나름대로 객체 분리를 고려해야 하는 기준에 대해 정의를 해보았습니다.
- 클래스 내부에 필드를 사용하지 않는 메서드가 있을때
- 하나의 클래스에서 데이터 변환이 2번 이상 일어날때
- 필드 변수의 개수가 5개 이상일 때
위 3가지 기준으로 객체의 책임을 분리하였고, 최종적으로 도메인은 아래와 같이 9개로 분리 하였습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
├── generator
│ ├── LottoGenerator.java
│ └── QuickLottoGenerator.java
├── number
│ ├── BonusNumber.java
│ ├── Lotto.java
│ └── WinningNumber.java
├── purchase
│ ├── LottoStore.java
│ └── Money.java
└── winning
├── LottoProfit.java
├── LottoRank.java
└── WinningResult.java
이렇게 나름 기준을 세운 덕분에 객체 분리가 전보다 수월했지만, 이 방법이 정답이라고 생각하지는 않아 개발 역량을 더 쌓아서 더욱 명확하게 기준을 세워 볼예정입니다.
✨마무리 하며
1주차부터 함께하기 채널에 공유하고 싶은 정보들을 포스핑하고 있습니다. 정말 소소한 팁들이지만, 도움이 됐다는 크루원들의 피드백을 받을 때마다 뿌듯함을 느꼈습니다. 비록 지금은 개발 실력이 부족해서 인텔리제이 사용팁 정도 밖에 공유 할 수 없지만, 나중에 개발 역량이 더 쌓이게 되면 기술적인 부분도 함께 나눠보고자 합니다. 그럼 그날이 빨리 오기를 기대하며 4주차도 열심히 몰입 해보겠습니다.