우아한테크코스 프리코스 3주차 미션입니다.
로또 구매부터 당첨 확인까지 전체 프로세스를 구현한 콘솔 기반 로또 게임입니다.
- 💰 구매 금액 입력 및 로또 자동 발행
- 🎲 1~45 범위의 중복되지 않는 번호 6개 생성
- 🎯 당첨 번호 및 보너스 번호 입력
- 🏆 당첨 통계 및 수익률 계산
- ✅ 입력값 검증 및 예외 처리 (재시도 로직)
- Language: Java 21
- Build Tool: Gradle
- Testing: JUnit 5, AssertJ
- Library: camp.nextstep.edu.missionutils
입력:
구입금액을 입력해 주세요.
8000
당첨 번호를 입력해 주세요.
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%입니다.
레이어별 책임:
- Application: 프로그램 시작점
- Domain: 핵심 비즈니스 로직
- Service: 전체 게임 흐름 조율 및 예외 재시도 처리
- View: 입출력 담당 (
Console.readLine(),System.out.println()) - Parser: 문자열 → 객체 변환
- Validator: 비즈니스 규칙 검증
- Util: 로또 번호 생성 등 유틸리티
src/main/java/lotto/
├── Application.java # 프로그램 진입점
├── domain/ # 핵심 비즈니스 로직
│ ├── Lotto.java # 로또 번호 (6개)
│ ├── LottoTickets.java # 여러 로또 관리
│ ├── WinningLotto.java # 당첨 번호 + 보너스 번호
│ ├── Rank.java # 당첨 등수 (Enum)
│ └── LottoResult.java # 당첨 결과 및 수익률
├── service/ # 게임 진행 조율
│ └── LottoGameService.java
├── view/ # 입출력 처리
│ ├── InputView.java # 사용자 입력 (Console.readLine())
│ └── OutputView.java # 결과 출력
├── parser/ # 문자열 파싱
│ ├── PurchaseAmountParser.java
│ ├── WinningNumbersParser.java
│ └── BonusNumberParser.java
├── validator/ # 입력 검증
│ └── InputValidator.java
└── util/ # 유틸리티
└── LottoGenerator.java # 로또 번호 생성
정상 케이스:
- 일치 개수와 보너스 일치 여부로 등수를 판별한다
- 각 등수별 상금을 반환한다
- 5등: 3개 일치 / 5,000원
- 4등: 4개 일치 / 50,000원
- 3등: 5개 일치 / 1,500,000원
- 2등: 5개 + 보너스 일치 / 30,000,000원
- 1등: 6개 일치 / 2,000,000,000원
- 당첨되지 않으면 NONE 반환
커밋: feat(domain): Rank 열거형 구현
정상 케이스:
- 6개의 로또 번호를 저장한다
- 로또 번호는 1~45 범위의 중복되지 않는 숫자이다
- 로또 번호를 오름차순으로 정렬하여 반환한다
- 주어진 번호 리스트와 일치하는 개수를 반환한다
- 특정 번호 포함 여부를 확인한다
예외 케이스:
- 로또 번호가 null이면
IllegalArgumentException발생[ERROR] 로또 번호를 입력해주세요.
- 로또 번호가 6개가 아니면
IllegalArgumentException발생[ERROR] 로또 번호는 6개여야 합니다.
- 중복된 번호가 있으면
IllegalArgumentException발생[ERROR] 로또 번호는 중복될 수 없습니다.
- 1~45 범위를 벗어나면
IllegalArgumentException발생[ERROR] 로또 번호는 1부터 45 사이의 숫자여야 합니다.
커밋: feat(domain): Lotto 객체 기본 기능 구현
정상 케이스:
- 당첨 번호 6개와 보너스 번호 1개를 저장한다
- 로또와 비교하여 등수를 판별한다
- 일치 개수를 계산한다
- 보너스 번호 일치 여부를 확인한다
예외 케이스:
- 당첨 번호가 null이면
IllegalArgumentException발생[ERROR] 당첨 번호를 입력해주세요.
- 보너스 번호가 당첨 번호와 중복되면
IllegalArgumentException발생[ERROR] 보너스 번호는 당첨 번호와 중복될 수 없습니다.
- 보너스 번호가 1~45 범위를 벗어나면
IllegalArgumentException발생[ERROR] 보너스 번호는 1부터 45 사이의 숫자여야 합니다.
커밋: feat(domain): WinningLotto 객체 구현
정상 케이스:
- 여러 개의 로또를 관리한다
- 구매한 로또 개수를 반환한다
- 모든 로또를 반환한다
- 당첨 번호와 비교하여 각 로또의 등수를 판별한다
예외 케이스:
- 로또 리스트가 null이면
IllegalArgumentException발생[ERROR] 로또 목록이 없습니다.
- 로또 리스트가 비어있으면
IllegalArgumentException발생[ERROR] 최소 1개 이상의 로또를 구매해야 합니다.
커밋: feat(domain): LottoTickets 객체 구현
정상 케이스:
- 등수별 당첨 개수를 저장한다
- 총 수익금을 계산한다
- 수익률을 계산한다 (소수점 둘째 자리 반올림)
- 등수별 당첨 개수를 반환한다
예외 케이스:
- 구매 금액이 0 이하면
IllegalArgumentException발생[ERROR] 구매 금액은 0보다 커야 합니다.
커밋: feat(domain): LottoResult 객체 구현
정상 케이스:
- 문자열을 정수로 변환한다
- 앞뒤 공백을 제거한다
예외 케이스:
- 입력이 null이면
IllegalArgumentException발생[ERROR] 구입 금액을 입력해주세요.
- 입력이 빈 문자열이면
IllegalArgumentException발생[ERROR] 구입 금액을 입력해주세요.
- 숫자가 아닌 문자가 포함되면
NumberFormatException발생[ERROR] 구입 금액은 숫자여야 합니다.
커밋: feat(parser): 구매 금액 파싱 구현
정상 케이스:
- 쉼표 기준으로 문자열을 분리한다
- 각 번호의 앞뒤 공백을 제거한다
- 문자열 리스트를 정수 리스트로 변환한다
예외 케이스:
- 입력이 null이면
IllegalArgumentException발생[ERROR] 당첨 번호를 입력해주세요.
- 입력이 빈 문자열이면
IllegalArgumentException발생[ERROR] 당첨 번호를 입력해주세요.
- 쉼표로 구분되지 않은 경우
IllegalArgumentException발생[ERROR] 당첨 번호는 쉼표(,)로 구분해야 합니다.
- 숫자가 아닌 값이 포함되면
NumberFormatException발생[ERROR] 당첨 번호는 숫자여야 합니다.
- 공백만 있는 번호가 있으면
IllegalArgumentException발생[ERROR] 당첨 번호는 빈 값일 수 없습니다.
커밋: feat(parser): 당첨 번호 파싱 구현
정상 케이스:
- 문자열을 정수로 변환한다
- 앞뒤 공백을 제거한다
예외 케이스:
- 입력이 null이면
IllegalArgumentException발생[ERROR] 보너스 번호를 입력해주세요.
- 입력이 빈 문자열이면
IllegalArgumentException발생[ERROR] 보너스 번호를 입력해주세요.
- 숫자가 아닌 문자가 포함되면
NumberFormatException발생[ERROR] 보너스 번호는 숫자여야 합니다.
커밋: feat(parser): 보너스 번호 파싱 구현
정상 케이스:
- 1,000원 이상의 금액이면 통과
- 1,000원 단위로 나누어떨어지면 통과
예외 케이스:
- 음수를 입력하면
IllegalArgumentException발생[ERROR] 구입 금액은 양수여야 합니다.
- 0을 입력하면
IllegalArgumentException발생[ERROR] 구입 금액은 0보다 커야 합니다.
- 1,000원 미만이면
IllegalArgumentException발생[ERROR] 구입 금액은 1,000원 이상이어야 합니다.
- 1,000원으로 나누어떨어지지 않으면
IllegalArgumentException발생[ERROR] 구입 금액은 1,000원 단위여야 합니다.
커밋: feat(validator): 구매 금액 검증 구현
정상 케이스:
- 6개의 번호면 통과
- 각 번호가 1~45 범위 내에 있으면 통과
- 중복되지 않으면 통과
예외 케이스:
- 번호 리스트가 null이면
IllegalArgumentException발생[ERROR] 당첨 번호를 입력해주세요.
- 번호가 6개가 아니면
IllegalArgumentException발생[ERROR] 당첨 번호는 6개여야 합니다.
- 1~45 범위를 벗어나면
IllegalArgumentException발생[ERROR] 로또 번호는 1부터 45 사이의 숫자여야 합니다.
- 중복된 번호가 있으면
IllegalArgumentException발생[ERROR] 당첨 번호는 중복될 수 없습니다.
커밋: feat(validator): 당첨 번호 검증 구현
정상 케이스:
- 1~45 범위의 숫자면 통과
- 당첨 번호와 중복되지 않으면 통과
예외 케이스:
- 1~45 범위를 벗어나면
IllegalArgumentException발생[ERROR] 보너스 번호는 1부터 45 사이의 숫자여야 합니다.
- 당첨 번호와 중복되면
IllegalArgumentException발생[ERROR] 보너스 번호는 당첨 번호와 중복될 수 없습니다.
커밋: feat(validator): 보너스 번호 검증 구현
정상 케이스:
- 1~45 범위에서 중복되지 않는 6개의 숫자를 생성한다
-
Randoms.pickUniqueNumbersInRange()사용 - 구매 금액에 맞는 개수만큼 로또를 생성한다
예외 케이스:
- 생성할 로또 개수가 0 이하면
IllegalArgumentException발생[ERROR] 로또 개수는 1개 이상이어야 합니다.
커밋: feat(util): 로또 번호 자동 생성 구현
- 입력 안내 메시지 출력: "구입금액을 입력해 주세요."
-
Console.readLine()으로 입력 받기 - 입력받은 문자열을 그대로 반환
커밋: feat(view): 구매 금액 입력 기능 구현
- 빈 줄 출력
- 입력 안내 메시지 출력: "당첨 번호를 입력해 주세요."
-
Console.readLine()으로 입력 받기 - 입력받은 문자열을 그대로 반환
커밋: feat(view): 당첨 번호 입력 기능 구현
- 빈 줄 출력
- 입력 안내 메시지 출력: "보너스 번호를 입력해 주세요."
-
Console.readLine()으로 입력 받기 - 입력받은 문자열을 그대로 반환
커밋: feat(view): 보너스 번호 입력 기능 구현
- 빈 줄 출력
- "n개를 구매했습니다." 출력
- 각 로또를 오름차순 정렬하여 출력
- 형식:
[번호1, 번호2, 번호3, 번호4, 번호5, 번호6] - 각 로또마다 줄바꿈
커밋: feat(view): 구매 로또 출력 기능 구현
- 빈 줄 출력
- "당첨 통계" 헤더 출력
- 구분선 "---" 출력
- 5등부터 1등까지 순서대로 출력
- 형식: "n개 일치 (금액원) - m개"
- 2등: "5개 일치, 보너스 볼 일치 (금액원) - m개"
- 금액은 천 단위 쉼표 포함
커밋: feat(view): 당첨 통계 출력 기능 구현
- "총 수익률은 n%입니다." 형식으로 출력
- 소수점 둘째 자리에서 반올림
- 천 단위 쉼표 포함
커밋: feat(view): 수익률 출력 기능 구현
- "[ERROR]"로 시작하는 에러 메시지 출력
-
System.out.println()사용
커밋: feat(view): 에러 메시지 출력 기능 구현
- 구매 금액 입력 및 검증 (재시도 로직)
- 로또 자동 발행
- 구매한 로또 출력
- 당첨 번호 입력 및 검증 (재시도 로직)
- 보너스 번호 입력 및 검증 (재시도 로직)
- 당첨 결과 계산
- 당첨 통계 및 수익률 출력
- 각 입력 단계에서 예외 발생 시 해당 단계부터 재시도
재시도 흐름:
- 구매 금액 예외 → 구매 금액 입력부터 재시도
- 당첨 번호 예외 → 당첨 번호 입력부터 재시도
- 보너스 번호 예외 → 보너스 번호 입력부터 재시도
커밋: feat(service): LottoGameService 게임 진행 로직 구현
- LottoGameService 실행
- 예외는 Service 레이어에서 처리
커밋: feat(app): Application 메인 실행 구현
사용 시점: 메서드에 전달된 인자가 유효하지 않을 때
- 숫자 범위 오류 (1~45 범위 벗어남)
- 개수 오류 (6개가 아님)
- 중복 오류
- 단위 오류 (1,000원 단위 아님)
- null 또는 빈 값
예시:
if (numbers.size() != 6) {
throw new IllegalArgumentException("[ERROR] 로또 번호는 6개여야 합니다.");
}사용 시점: 문자열을 숫자로 변환할 수 없을 때
- 파싱 과정에서 발생
Integer.parseInt()실패 시
예시:
try {
return Integer.parseInt(input);
} catch (NumberFormatException e) {
throw new NumberFormatException("[ERROR] 구입 금액은 숫자여야 합니다.");
}사용 시점: 객체의 상태가 메서드 호출에 적합하지 않을 때
- 게임이 이미 종료된 상태에서 추가 작업 시도
- 초기화되지 않은 객체 사용
예시:
if (lottoTickets == null) {
throw new IllegalStateException("[ERROR] 로또가 발행되지 않았습니다.");
}- 레이어드 아키텍처 (Layered Architecture) 기반
- Package by Layer 방식
- Inside-Out 개발 (Domain → Parser → Validator → Util → View → Service → Application)
┌─────────────────┐
│ Application │ (진입점)
└────────┬────────┘
│
▼
┌─────────────────┐
│ Service │ (조율 + 재시도 로직)
└────┬────────────┘
│
├──────────┬──────────┬──────────┐
▼ ▼ ▼ ▼
┌─────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ Domain │ │ Parser │ │ View │ │ Util │
└─────────┘ └──────────┘ └──────────┘ └──────────┘
▲ ▲
│ │
┌────┴──────────┴────┐
│ Validator │
└────────────────────┘
- JDK 21에서 실행 가능
- indent depth 2 이하
- 3항 연산자 미사용
- else 예약어 미사용
- 함수 길이 15라인 이하
- 함수가 한 가지 일만 수행
- Java Enum 활용 (Rank)
- JUnit 5, AssertJ 테스트 작성
-
Randoms.pickUniqueNumbersInRange()사용 -
Console.readLine()사용 - 제공된 Lotto 클래스 활용 (필드 추가 불가)
- UI 로직 제외 단위 테스트 작성
-
IllegalArgumentException,IllegalStateException등 명확한 예외 타입 사용
| 상수 이름 | 사용 위치 | 상태 |
|---|---|---|
PURCHASE_AMOUNT_ZERO |
InputValidator | ✅ |
PURCHASE_AMOUNT_MINIMUM |
InputValidator | ✅ |
PURCHASE_AMOUNT_UNIT |
InputValidator | ✅ |
PURCHASE_AMOUNT_EMPTY |
PurchaseAmountParser | ✅ |
PURCHASE_AMOUNT_INVALID_FORMAT |
PurchaseAmountParser | ✅ |
LOTTO_NUMBERS_NULL |
Lotto, WinningLotto | ✅ |
LOTTO_NUMBERS_SIZE |
Lotto | ✅ |
LOTTO_NUMBERS_DUPLICATE |
Lotto | ✅ |
LOTTO_NUMBERS_RANGE |
Lotto | ✅ |
WINNING_NUMBERS_EMPTY |
WinningNumbersParser | ✅ |
WINNING_NUMBERS_INVALID_FORMAT |
WinningNumbersParser | ✅ |
WINNING_NUMBERS_EMPTY_VALUE |
WinningNumbersParser | ✅ |
BONUS_NUMBER_EMPTY |
BonusNumberParser | ✅ |
BONUS_NUMBER_INVALID_FORMAT |
BonusNumberParser | ✅ |
BONUS_NUMBER_RANGE |
WinningLotto | ✅ |
BONUS_NUMBER_DUPLICATE |
WinningLotto | ✅ |
LOTTO_TICKETS_NULL |
LottoTickets | ✅ |
LOTTO_TICKETS_EMPTY |
LottoTickets | ✅ |
LOTTO_RESULT_PURCHASE_AMOUNT |
LottoResult | ✅ |
| 상수 이름 | 사용 위치 | 용도 | 상태 |
|---|---|---|---|
INPUT_PURCHASE_AMOUNT |
InputView | 구매 금액 입력 안내 | ✅ |
INPUT_WINNING_NUMBERS |
InputView | 당첨 번호 입력 안내 | ✅ |
INPUT_BONUS_NUMBER |
InputView | 보너스 번호 입력 안내 | ✅ |
PURCHASED_LOTTO_COUNT_FORMAT |
OutputView | 구매 개수 출력 | ✅ |
WINNING_STATISTICS_HEADER |
OutputView | 통계 헤더 출력 | ✅ |
SEPARATOR |
OutputView | 구분선 출력 | ✅ |
RANK_FORMAT |
OutputView | 등수별 당첨 결과 출력 | ✅ |
PROFIT_RATE_FORMAT |
OutputView | 수익률 출력 | ✅ |
| 상수 이름 | 값 | 사용 위치 | 상태 |
|---|---|---|---|
MIN_LOTTO_NUMBER |
1 | Lotto, WinningLotto, LottoGenerator | ✅ |
MAX_LOTTO_NUMBER |
45 | Lotto, WinningLotto, LottoGenerator | ✅ |
LOTTO_NUMBER_COUNT |
6 | Lotto, LottoGenerator | ✅ |
LOTTO_PRICE |
1000 | InputValidator, LottoGenerator | ✅ |
- ErrorMessages: 에러 메시지만 관리
- ViewMessages: View 출력 메시지만 관리
- LottoConstants: 비즈니스 상수만 관리
- 모든 문자열 리터럴을 상수로 추출
- 한 곳에서 수정하면 전체에 반영
- 상수 이름만으로 용도를 파악 가능
- 접두사를 통한 그룹핑 (PURCHASE_AMOUNT_, LOTTO_NUMBERS_)
lotto/
└── common/
├── ErrorMessages.java # 에러 메시지
├── ViewMessages.java # View 메시지
└── LottoConstants.java # 비즈니스 상수
src/test/java/lotto/
├── domain/
│ ├── LottoTest.java
│ ├── RankTest.java
│ ├── WinningLottoTest.java
│ ├── LottoTicketsTest.java
│ └── LottoResultTest.java
├── parser/
│ ├── PurchaseAmountParserTest.java
│ ├── WinningNumbersParserTest.java
│ └── BonusNumberParserTest.java
├── validator/
│ └── InputValidatorTest.java
├── util/
│ └── LottoGeneratorTest.java
└── service/
└── LottoGameServiceTest.java
- 정상 케이스와 예외 케이스 모두 테스트
- 경계값 테스트 (0, 1, 45, 46 등)
-
@ParameterizedTest활용하여 여러 케이스 검증 -
assertThatThrownBy()로 예외 메시지까지 검증 - UI 로직(InputView, OutputView)은 테스트 제외
- Java Enum을 활용한 등수 관리
- 일급 컬렉션 패턴 적용 (LottoTickets, LottoResult)
- 예외 발생 시 재입력 로직 구현
- Parser 레이어 분리로 책임 명확화
- 다양한 예외 타입 활용 (IllegalArgumentException, NumberFormatException)
- 2주차 공통 피드백 반영
- 함수 분리를 통한 indent depth 관리
- Early return 패턴으로 else 제거
- 매직 넘버 상수화
- 도메인 로직과 UI 로직 분리
- 함수 길이 15라인 이하 유지
- Parser와 Validator의 경계 설정
- 재시도 로직의 중복 제거
- Enum을 활용한 등수 및 상금 관리
- 일급 컬렉션 적용 범위
- Java Enum 활용 (상태와 행위를 함께 관리)
- 일급 컬렉션 (First-Class Collection)
- 예외 처리