Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
eb33467
feat: 구입금액 입력 기능 구현
luzknar Aug 1, 2026
25a7a6d
feat: 기본 로또 숫자 생성 기능 구현
luzknar Aug 1, 2026
bb9a74e
feat: 로또 구입 금액에 따른 로또 발급 기능 구현
luzknar Aug 1, 2026
4bcc236
feat:구매한 로또들을 관리하는 일급컬렉션 Lottos 추가
luzknar Aug 1, 2026
183109c
feat: 출력 기능 구현
luzknar Aug 1, 2026
9f16875
feat: 메인메서드 구현
luzknar Aug 1, 2026
e66d10f
feat: 당첨 번호 입력 및 저장 구현
luzknar Aug 1, 2026
6791807
refactor: 입력 기능 View 내부로 이동
luzknar Aug 1, 2026
f0bb06e
refactor: 당첨 번호 정렬
luzknar Aug 1, 2026
16aad70
feat: 로또 1장당 몇 개의 숫자가 겹치는지 구하는 기능 구현
luzknar Aug 2, 2026
1925288
feat: 당첨 번호 비교 및 등수별 당첨 통계 집계 기능 구현
luzknar Aug 2, 2026
1fd4040
feat: 수익률 구하는 기능 구현
luzknar Aug 2, 2026
48c0c1d
feat: 당첨 통계 출력 기능 구현
luzknar Aug 2, 2026
6be5649
fix: InputView 버퍼 잔여 문제로 인한 당첨 번호 입력 오류 수정
luzknar Aug 2, 2026
e17cd36
feat: 보너스 볼 입력 기능 구현
luzknar Aug 2, 2026
464b224
feat: 2등+보너스볼 당첨자 구하는 기능 구현
luzknar Aug 2, 2026
76711b0
feat: 수동 로또 구입 입력 기능 구현
luzknar Aug 2, 2026
9c82f14
feat: 수동 로또 구매 기능 구현
luzknar Aug 2, 2026
63180a1
feat: 당첨 등수 관리 Enum 및 당첨 수 원시값 포장 객체 구현
luzknar Aug 3, 2026
a2e257b
refactor: Map과 Enum 기반의 로또 당첨 집계 로직으로 변경
luzknar Aug 3, 2026
88edef5
refactor: 하드코딩된 상금 상수 제거 및 Rank Enum, WinningStatistics 연동
luzknar Aug 3, 2026
a30b33a
feat: 리팩토링된 도메인 객체와 View 및 Main 연동
luzknar Aug 3, 2026
2bb1f54
Test: 맞춘 개수와 보너스볼 유무에 따라 올바른 Rank를 반환하는지 테스트
luzknar Aug 3, 2026
cdd23cf
Test: compareLottos() 실행 시 당첨 등수 별 수량이 Map에 정확히 집계되는지 테스트
luzknar Aug 3, 2026
a741178
refactor: 예외처리 추가
luzknar Aug 3, 2026
05a9d3d
test: 구매금액과 통계를 바탕으로 수익률이 올바르게 계산되는지 테스트
luzknar Aug 3, 2026
2f4ed32
test: 로또 객체 생성 시 예외 발생 검증 테스트 추가
luzknar Aug 4, 2026
1775991
refactor: 같은 기능을 하는 메서드 단순화
luzknar Aug 4, 2026
d145916
test: 수익률 테스트에서 금액을 로또 수량에 맞게 변경
luzknar Aug 4, 2026
c626494
refactor: Rank 필드에 fianl 제어자 추가
luzknar Aug 4, 2026
5b8dde5
refactor: 수동 로또 구입 및 당첨 번호 설정 로직을 PurchaseManage로 이동
luzknar Aug 4, 2026
0b6a05b
refactor: 구입 금액 입력 중 공백 및 문자 입력 시 예외처리 추가
luzknar Aug 4, 2026
d5f0598
refactor: 수동 로또 생성 로직을 makeManualLotto 메서드로 분리
luzknar Aug 4, 2026
023460d
refactor: 수동 구매 수량 입력 검증 예외처리 추가
luzknar Aug 4, 2026
7e26f3a
refactor: 수동 구매 수량 입력 검증 예외처리 내부로 이동
luzknar Aug 4, 2026
efe62fe
refactor: 수동으로 입력받는 로또에 대한 중복 검증 추가
luzknar Aug 5, 2026
bd23cfb
fix: 수익률 퍼센트 단위 계산 수정
luzknar Aug 5, 2026
ce67ece
refactor: 안쓰는 매개변수 제거
luzknar Aug 5, 2026
2e6e6f1
refactor: LottoNumber의 getLottoNumbers()에 방어적 복사 적용
luzknar Aug 5, 2026
f8ec6f5
refactor: 클래스명, 필드변수명 수정
luzknar Aug 6, 2026
1d02d5d
refactor: 구매 금액 및 수동수량을 검증과 수량 계산 책임을 갖는 객체 PurchaseAmount로 분리
luzknar Aug 7, 2026
69140af
refactor: 당첨 번호 및 보너스 번호 관리와 검증 책임을 WinningLotto로 이동
luzknar Aug 8, 2026
c6082f7
refactor: 수동구매 로직을 Lottos 내부로 이동 및 도메인 내 View 의존성 제거
luzknar Aug 8, 2026
5624c34
refactor: 당첨번호와 로또1 장 일치 개수 세는 기능 WinningLotto 내부로 이동
luzknar Aug 8, 2026
143eb9f
refactor: 구입금액에 대한 비즈니스 규칙 검증 추가
luzknar Aug 8, 2026
28a0430
refactor: WinnerNum 클래스 삭제 및 Integer타입으로 개수 관리
luzknar Aug 8, 2026
b1a420a
Test: 테스트 코드 수정
luzknar Aug 8, 2026
63db64c
refactor: 와일드카드 제거
luzknar Aug 8, 2026
845e5f5
refactor: 안쓰는 매개변수 삭제
luzknar Aug 8, 2026
45501b2
refactor: 매직넘버 상수화
luzknar Aug 9, 2026
cf6ab80
refactor: 어노테이션과 메서드 사이 공백 제거
luzknar Aug 9, 2026
f891cea
test: ExceptionTest 예외 검증 문법 수정
luzknar Aug 9, 2026
afb6aad
docs: README 업데이트
luzknar Aug 9, 2026
048f3b1
refactor: 코드 자동 정렬하여 가독성 개선
luzknar Aug 9, 2026
b8e511f
refactor: ExceptionTest 제거 및 객체 단위 테스트 클래스(LottoTest)로 이동
luzknar Aug 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions README.md

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

예외 케이스 등이 구체화 되었는데 리드미를 업데이트해볼까요?

Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# 로또 미션

----

### 기능 요구사항

- 로또 구입 금액을 입력하면 구입 금액에 해당하는 로또를 발급해야한다.
- 로또 1장의 가격은 1000원 이다.
- 로또 당첨 번호를 받아 일치한 번호 수에 따라 당첨 결과를 보여준다.
- 로또 2등을 위한 보너스볼을 추첨한다.
- 당첨 통계에 2등을 추가한다.(2등 당첨 조건은 당첨 번호 5개 일치 + 보너스 볼 일치다.)
- 사용자가 수동으로 추첨 번호를 입력할 수 있도록 해야한다.
- 입력한 금액, 자동 생성 숫자, 수동 생성 번호를 입력하도록 해야한다.
---

### 프로그래밍 요구사항

- 자바 코드 컨벤션을 지킨다.
- indent depth를 2를 넘지않도록 구현한다.
- 3항 연산자를 쓰지 않는다.
- else 예약어를 쓰지 않는다.
- 배열 대신 컬렉션을 사용한다.
- 축약 하지 않는다.
- 함수의 길이가 10라인을 넘지 않도록 구현한다.
- 모든 원시값과 문자열을 포장한다.
- 일급 컬렉션을 쓴다.
- Java Enum을 적용한다.

### 예외 처리 케이스
사용자가 잘못된 값을 입력한 경우 IllegalArgumentException 또는 NumberFormatException 예외를 발생시키고, 에러 메시지 출력 후 재입력을 받는다.

#### 구입 금액 입력 예외
- 숫자가 아닌 값을 입력한 경우
- 0원 미만의 금액을 입력한 경우 ("구입 금액은 0원 이상이어야 합니다.")
- 1,000원 단위로 떨어지지 않는 금액을 입력한 경우 ("구입 금액은 1000원 단위이어야 합니다.")

### 2. 수동 구매 수량 입력 예외
- 숫자가 아닌 값을 입력한 경우
- 수동 구매 수량이 0개 미만이거나 총 구입 가능 수량을 초과한 경우 ("수동 구매 수량은 0개 이상 N개 이하이어야 합니다.")

### 3. 로또 번호 (수동 번호 및 당첨 번호) 검증 예외
- 로또 번호가 6개가 아닌 경우 ("로또 번호는 6개여야 합니다.")
- 로또 번호가 1~45 범위를 벗어난 경우 ("로또 번호는 1부터 45까지여야 합니다.")
- 하나의 로또 내에 중복된 번호가 존재하는 경우 ("중복된 로또 번호가 존재합니다.")

### 4. 보너스 볼 입력 예외
- 숫자가 아닌 값을 입력한 경우
- 보너스 볼 숫자가 1~45 범위를 벗어난 경우 ("보너스 볼은 1부터 45 사이의 숫자여야 합니다.")
- 당첨 번호 6개와 중복되는 숫자를 입력한 경우 ("중복된 로또 번호가 존재합니다.")
30 changes: 30 additions & 0 deletions src/main/java/Main.java

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

하나의 메서드에 쭉 로직이 작성되어있네요 🥲
메서드 분리를 통해 가독성도 높이고, 책임별로 로직을 묶어볼까요?

Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import domain.*;
import view.InputView;
import view.ResultView;

import java.util.List;

public class Main {
public static void main(String[] args) {
final int price = InputView.getPurchaseAmount();
final int manualCount = InputView.getManualPurchaseAmount(price);

PurchaseAmount purchaseAmount = new PurchaseAmount(price, manualCount);
PurchaseManage purchaseManage = new PurchaseManage(purchaseAmount);
List<String> manualInputs = InputView.getManualPurchasedLottos(manualCount);

Lottos lottos = purchaseManage.buyLottos(manualInputs);

ResultView.showNum(lottos);

String enteredWinningNumber = InputView.getWinningNumber();
int bonusBall = InputView.getBonusNumber();
WinningLotto winningLotto = new WinningLotto(enteredWinningNumber, bonusBall);
WinningStatistics winningStatistics = new WinningStatistics();

winningStatistics.compareLottos(winningLotto, lottos);
ProfitRate profitRate = new ProfitRate(price, winningStatistics);
ResultView.showStatistics(profitRate, winningStatistics);
}

}
67 changes: 67 additions & 0 deletions src/main/java/domain/Lotto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package domain;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.HashSet;

public class Lotto {
private static final List<Integer> NUMBERS = new ArrayList<>();
public static final int MAX_NUMBER = 45;
public static final int MIN_NUMBER = 1;
public static final int LOTTO_SIZE = 6;

static {
for (int i = 1; i <= MAX_NUMBER; i++) {
NUMBERS.add(i);
}
}

private final List<Integer> numbers;

public Lotto() {
List<Integer> numbers = new ArrayList<>(NUMBERS);
Collections.shuffle(numbers);
this.numbers = new ArrayList<>(numbers.subList(0, 6));
Collections.sort(this.numbers);
}

public Lotto(List<Integer> manualNumbers) {
validateLottoNumber(manualNumbers);
validateDuplication(manualNumbers);
this.numbers = new ArrayList<>(manualNumbers);
Collections.sort(this.numbers);
}

private void validateLottoNumber(List<Integer> manualNumbers) {
for (int number : manualNumbers) {
validateNumber(number);
}
if (manualNumbers.size() != LOTTO_SIZE) {
throw new IllegalArgumentException("로또 번호는 6개여야 합니다.");
}
}

private void validateNumber(int number) {
if (number < MIN_NUMBER || number > MAX_NUMBER) {
throw new IllegalArgumentException("로또 번호는 1부터 45까지여야 합니다.");
}
}

private void validateDuplication(List<Integer> manualNumbers) {
Set<Integer> uniqueNumbers = new HashSet<>(manualNumbers);
if (uniqueNumbers.size() != manualNumbers.size()) {
throw new IllegalArgumentException("중복된 로또 번호가 존재합니다.");
}
}

public List<Integer> getLottoNumbers() {
return List.copyOf(numbers);
}
Comment on lines +59 to +61

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getter로 lottoNumbers를 그대로 넘겨주면 어떤 문제가 발생할 수 있을까요?

추가로 방어적 복사 에 대해 들어보셨나요?
학습 후 그 내용을 작성해주세요!

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

final 키워드는 lottoNumbers 변수의 재할당만 막아줄 뿐, 참조하는 내부 요소를 변경하는 것은 막지 못합니다. 따라서 외부에서 getLottoNumbers를 호출한뒤 .add(), .remove(), clear() 같은 메서드를 호출해서 수정이 가능하다는 문제가 있습니다. LottoNumber 객체의 내부 검증을 완벽히 통과해서 생성되었더라도, 외부에서 상태가 오염되는 위험이 생깁니다.

방어적 복사란 내부의 객체를 반환 할 때, 객체의 복사본을 만들어서 반환하는 것입니다.
방어적 복사를 사용하는 방법에는

  1. new ArrayList<>()로 새로운 List를 생성해서 반환하는 방법
  2. List.copyOf()로 Collection 반환
  3. 복사 생성자 + Collections.unmodifiableList로 Collection 반환

new ArrayList<>() 로 복사한 리스트는 변경이 가능하기 때문에, List.copyOf()를 적용하였습니다. 방어적 복사를 통해 외부와의 참조를 끊어내는 동시에, 반환된 리스트 역시 변경 불가능한 상태로 만들어 객체의 불변성과 캡슐화를 확보하려 했습니다.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

잘 학습하셨네요👍 위 클래스에도 적용해보시죠!


public int get(int index) {
return numbers.get(index);
}

}
16 changes: 16 additions & 0 deletions src/main/java/domain/LottoParser.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package domain;

import java.util.List;
import java.util.ArrayList;

public class LottoParser {

public static List<Integer> parseInput(String input) {
List<Integer> numbers = new ArrayList<>();
for (String value : input.split(",")) {
numbers.add(Integer.parseInt(value.trim()));
}

return numbers;
}
}
36 changes: 36 additions & 0 deletions src/main/java/domain/Lottos.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package domain;

import java.util.ArrayList;
import java.util.List;

public class Lottos {
private List<Lotto> userLottos;

public Lottos() {
this.userLottos = new ArrayList<>();
}
public Lottos(List<Lotto> lottos) {
this.userLottos = lottos;
}

public void makeManualLottos(List<String> manualInputs) {
for (String manualInput : manualInputs) {
Lotto manualLotto = new Lotto(LottoParser.parseInput(manualInput));
this.userLottos.add(manualLotto);
}
}

public void makeAutomaticLottos(int automaticLottoCount) {
for (int i = 0; i < automaticLottoCount; i++) {
Lotto automaticLotto = new Lotto();
this.userLottos.add(automaticLotto);
}
}

public int size() {
return userLottos.size();
}
public List<Lotto> getLottos() {
return List.copyOf(userLottos);
}
}
28 changes: 28 additions & 0 deletions src/main/java/domain/ProfitRate.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package domain;

import java.util.Map;

public class ProfitRate {
private final int price;
private final WinningStatistics winningStatistics;

public ProfitRate(int price, WinningStatistics winningStatistics) {
this.price = price;
this.winningStatistics = winningStatistics;
}

public long getTotalProfit() {
long totalProfit = 0;
Map<Rank, Integer> statistics = winningStatistics.getWinningStatistics();

for (Rank rank : statistics.keySet()) {
int count = statistics.get(rank);
totalProfit += (long) rank.getPrize() * count;
}
return totalProfit;
}

public double getProfitRate() {
return ((double) getTotalProfit() / price) * 100;
}
}
34 changes: 34 additions & 0 deletions src/main/java/domain/PurchaseAmount.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package domain;

public class PurchaseAmount {
public static final int LOTTO_PRICE = 1000;
private final int purchasePrice;
private final int totalCount;
private final int manualCount;

public PurchaseAmount(int price, int manualCount) {
this.purchasePrice = price;
totalCount = purchasePrice / LOTTO_PRICE;
validateManualCount(manualCount);
this.manualCount = manualCount;
}

private void validatePurchaseAmount(int purchasePrice) {
if (purchasePrice < 0) {
throw new IllegalArgumentException("구입 금액은 0원 이상이어야 합니다.");
}
if (purchasePrice % LOTTO_PRICE != 0) {
throw new IllegalArgumentException("구입 금액은 1000원 단위이어야 합니다.");
}
}

private void validateManualCount(int manualCount) {
if (totalCount < manualCount || manualCount < 0) {
throw new IllegalArgumentException("수동 구매 수량은 0개 이상 " + totalCount + "개 이하이어야 합니다.");
}
}

public int calculateAutomaticCount() {
return totalCount - manualCount;
}
}
19 changes: 19 additions & 0 deletions src/main/java/domain/PurchaseManage.java

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 클래스의 역할에 대해서도 쭉 리스트업해보면 좋을 것같아요.
PurchaseManager가 하고있는 역할이 많은 것 같아요!

오히려 Lotto List를 필드로 가지고있는 Lottos가 가져야할 역할도 이 클래스가 가지고 있는 것 같은데 일단 리스트업 후 다른 여러 클래스들로 적절히 분리해보시죵

Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package domain;

import java.util.List;

public class PurchaseManage{
private final PurchaseAmount purchaseAmount;

public PurchaseManage(PurchaseAmount purchaseAmount) {
this.purchaseAmount = purchaseAmount;
}
public Lottos buyLottos(List<String> manualInputs) {
int automaticLottoCount = purchaseAmount.calculateAutomaticCount();
Lottos lottos = new Lottos();
lottos.makeManualLottos(manualInputs);
lottos.makeAutomaticLottos(automaticLottoCount);

return lottos;
}
}
37 changes: 37 additions & 0 deletions src/main/java/domain/Rank.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package domain;

public enum Rank {
FIRST_PLACE(6, 2000000000, false),
SECOND_PLACE_BONUS(5, 30000000, true),
SECOND_PLACE(5, 1500000, false),
THIRD_PLACE(4, 50000, false),
FOURTH_PLACE(3, 5000, false),
MISS(0, 0, false);

private final int matchBallNum;
private final int prize;
private final boolean hasBonusBall;

Rank(int matchBallNum, int prize, boolean hasBonusBall) {
this.matchBallNum = matchBallNum;
this.prize = prize;
this.hasBonusBall = hasBonusBall;
}

public static Rank getRank(int matchBallNum, boolean hasBonusBall) {
if (matchBallNum == FIRST_PLACE.getMatchBallNum()) { return FIRST_PLACE; }
if (matchBallNum == SECOND_PLACE_BONUS.getMatchBallNum() && hasBonusBall) { return SECOND_PLACE_BONUS; }
if (matchBallNum == SECOND_PLACE.getMatchBallNum() && !hasBonusBall) { return SECOND_PLACE; }
if (matchBallNum == THIRD_PLACE.getMatchBallNum()) { return THIRD_PLACE; }
if (matchBallNum == FOURTH_PLACE.getMatchBallNum()) { return FOURTH_PLACE; }
return MISS;
}

public int getMatchBallNum() {
return matchBallNum;
}

public int getPrize() {
return prize;
}
}
53 changes: 53 additions & 0 deletions src/main/java/domain/WinningLotto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package domain;

import java.util.HashSet;
import java.util.List;
import java.util.Set;

public class WinningLotto {
private final Lotto winningLotto;
private final int bonusBall;
private int count = 0;
private boolean bonusFlag;

public WinningLotto(String enteredWinningLotto, int bonusBall) {
LottoParser lottoParser = new LottoParser();
this.winningLotto = new Lotto(lottoParser.parseInput(enteredWinningLotto));
validateBonusBall(bonusBall);
this.bonusBall = bonusBall;
}

private void validateBonusBall(int bonusBall) {
if (bonusBall < Lotto.MIN_NUMBER || bonusBall > Lotto.MAX_NUMBER) {
throw new IllegalArgumentException("보너스 볼은 1부터 45 사이의 숫자여야 합니다.");
}
List<Integer> copiedWinningNumber = winningLotto.getLottoNumbers();
Set<Integer> uniqueBonusball = new HashSet<>(copiedWinningNumber);
if (!uniqueBonusball.add(bonusBall)) throw new IllegalArgumentException("중복된 로또 번호가 존재합니다.");
}

public int match(Lotto lotto) {
count = 0;
List<Integer> copiedLotto = lotto.getLottoNumbers();
for (int i = 0; i < Lotto.LOTTO_SIZE; i ++) {
compareNumbers(copiedLotto, i);
}
if (count == 5) {
bonusFlag = hasBonusNumber(copiedLotto);
}
return count;
}
public void compareNumbers(List<Integer> copiedLotto, int i) {
if (copiedLotto.contains(winningLotto.get(i))) {
count++;
}
}

public boolean hasBonusNumber(List<Integer> copiedLotto) {
return copiedLotto.contains(bonusBall);
}

public boolean getBonusFlag() {
return bonusFlag;
}
}
Loading