Skip to content
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
e37e586
feat: 로또 번호 생성 기능 구현
johaeunn Jul 31, 2026
1aaeca7
feat: 구입 금액에 따라 로또를 생성하는 Lottos 구현
johaeunn Jul 31, 2026
9fa4cfd
feat: 로또 번호 정렬 기능 추가
johaeunn Jul 31, 2026
488dc15
feat: 로또 입출력 기능 구현
johaeunn Jul 31, 2026
ce2dd97
feat: 로또 애플리케이션 실행 흐름 구현
johaeunn Jul 31, 2026
237b079
refactor: 구매 금액을 객체로 포장
johaeunn Jul 31, 2026
0f17986
refactor: 로또 번호를 객체로 포장
johaeunn Jul 31, 2026
a64e5db
feat: 당첨 번호 검증 및 당첨 통계 기능 구현
johaeunn Jul 31, 2026
2edb4e4
feat: 보너스 번호를 반영한 2등 당첨 기능 구현
johaeunn Aug 2, 2026
6cd46af
refactor: 로또 당첨 등수와 상금 관리를 Enum으로 변경
johaeunn Aug 2, 2026
3356338
feat: 로또 수동 구매 기능 구현
johaeunn Aug 2, 2026
bc267ed
refactor: 와일드카드 import를 명시적 import로 변경
johaeunn Aug 2, 2026
3fda90c
refactor: 검증 로직을 별도 메서드로 분리
johaeunn Aug 2, 2026
779b1e8
test: 전체 수동 구매 시 자동 구매 개수 테스트 추가
johaeunn Aug 2, 2026
e398223
test: LottoRank와 LottoStatistics 테스트 추가
johaeunn Aug 2, 2026
43a4e1e
test: LottoNumber와 중복되는 Lotto 범위 테스트 제거
johaeunn Aug 2, 2026
59618e0
docs: 로또 미션 README 작성
johaeunn Aug 2, 2026
212d96c
test: 로또 수익률 계산 테스트 추가
johaeunn Aug 2, 2026
3d834f6
fix: 구입 금액을 1000원 단위로 제한
johaeunn Aug 5, 2026
8727403
refactor: LottoStatistics 생성 책임을 Application으로 이동
johaeunn Aug 5, 2026
14aae3a
test: WinningNumbers 정상 생성 테스트 의도 명확화
johaeunn Aug 5, 2026
d973598
refactor: LottoRank에 MISS 상태 추가
johaeunn Aug 5, 2026
4e1590b
refactor: 당첨 정보를 WinningLotto로 통합
johaeunn Aug 5, 2026
a100ceb
refactor: 로또 자동 생성 책임을 LottoGenerator로 분리
johaeunn Aug 5, 2026
a3c5304
test: 수동 로또의 구매 결과 포함 여부 검증
johaeunn Aug 5, 2026
ce39227
refactor: Application 실행 흐름을 의미 단위로 분리
johaeunn Aug 5, 2026
8730e7c
refactor: 메서드 배치 순서를 호출 흐름에 맞게 정리
johaeunn Aug 5, 2026
f19affd
refactor: WinningNumbers를 Lotto로 통합
johaeunn Aug 7, 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
146 changes: 146 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
# 로또 - 클린 코드

## 미션 소개

로또 구매 금액과 수동 구매 번호를 입력받아 로또를 발급하고,
지난주 당첨 번호와 보너스 번호를 기준으로 당첨 결과와 수익률을 계산하는 프로그램

---

## 요구사항

- 로또 한 장의 가격은 1,000원이다.
- 로또 번호는 1부터 45 사이의 숫자이다.
- 하나의 로또는 서로 다른 6개의 번호로 구성된다.
- 구입 금액에 따라 구매 가능한 로또 개수를 계산한다.
- 사용자가 원하는 개수만큼 로또 번호를 수동으로 입력할 수 있다.
- 수동으로 구매하고 남은 개수는 자동으로 생성한다.
- 자동 로또 번호는 중복되지 않는 6개의 숫자로 생성한다.
- 지난주 당첨 번호 6개와 보너스 번호 1개를 입력받는다.
- 당첨 번호와 보너스 번호는 중복될 수 없다.
- 당첨 번호의 일치 개수와 보너스 번호 일치 여부에 따라 등수를 결정한다.
- 당첨 결과를 등수별로 집계한다.
- 총 당첨 금액을 기준으로 수익률을 계산한다.
- Java Enum을 활용하여 로또 등수와 상금을 관리한다.
- 메서드의 길이는 최대한 10라인을 넘지 않도록 구현한다.
- 들여쓰기 depth는 1을 넘지 않도록 구현한다.

### 추가 구현 기준
- 최소 구입 금액은 로또 1장 가격과 동일한 1,000원으로 정한다.
- 1,000원 미만의 금액을 입력하면 예외가 발생한다.

---

## 기능 목록

### 로또 구입

- [x] 구입 금액 입력
- [x] 최소 구입 금액 검증
- [x] 구입 금액에 따른 총 구매 가능 개수 계산
- [x] 수동 구매 개수 입력
- [x] 수동 구매 개수가 음수인지 검증
- [x] 수동 구매 개수가 전체 구매 가능 개수를 초과하는지 검증
- [x] 수동 구매 개수를 제외한 자동 구매 개수 계산

### 로또 생성

- [x] 수동 로또 번호 입력
- [x] 수동 로또 번호 개수 검증
- [x] 수동 로또 번호 중복 검증
- [x] 자동 로또 번호 생성
- [x] 로또 번호 범위 검증
- [x] 로또 번호 정렬
- [x] 수동 로또와 자동 로또를 하나의 컬렉션으로 관리

### 당첨 번호

- [x] 지난주 당첨 번호 입력
- [x] 당첨 번호 개수 검증
- [x] 당첨 번호 중복 검증
- [x] 보너스 번호 입력
- [x] 보너스 번호와 당첨 번호의 중복 검증

### 당첨 결과

- [x] 로또와 당첨 번호의 일치 개수 계산
- [x] 보너스 번호 일치 여부 확인
- [x] 일치 개수와 보너스 번호에 따른 당첨 등수 결정
- [x] 등수별 당첨 개수 집계
- [x] 총 당첨 금액 계산
- [x] 수익률 계산
- [x] 당첨 통계 출력

---

## 당첨 기준

| 등수 | 당첨 기준 | 상금 |
| --- | --- | ---: |
| 1등 | 6개 번호 일치 | 2,000,000,000원 |
| 2등 | 5개 번호 + 보너스 번호 일치 | 30,000,000원 |
| 3등 | 5개 번호 일치 | 1,500,000원 |
| 4등 | 4개 번호 일치 | 50,000원 |
| 5등 | 3개 번호 일치 | 5,000원 |

---

## 주요 객체

| 객체 | 역할 |
| --- | --- |
| `PurchaseAmount` | 구입 금액을 관리하고 구매 가능한 로또 개수를 계산한다. |
| `LottoNumber` | 로또 번호 하나를 관리하고 번호의 범위를 검증한다. |
| `Lotto` | 6개의 로또 번호를 관리하고 자동 또는 수동으로 로또를 생성한다. |
| `Lottos` | 여러 개의 로또를 관리한다. |
| `WinningNumbers` | 당첨 번호를 관리하고 로또와 일치하는 번호 개수를 계산한다. |
| `LottoRank` | 당첨 등수와 상금을 관리한다. |
| `LottoStatistics` | 등수별 당첨 결과와 수익률을 계산한다. |
| `InputView` | 사용자 입력을 담당한다. |
| `OutputView` | 로또 및 당첨 결과 출력을 담당한다. |

---

## 테스트

### PurchaseAmountTest

- 구입 금액에 따른 로또 구매 개수 계산
- 최소 구입 금액 검증
- 수동 구매 개수가 음수인 경우 검증
- 수동 구매 개수가 구매 가능 개수를 초과하는 경우 검증
- 수동 구매 개수를 제외한 자동 구매 개수 계산
- 모든 로또를 수동으로 구매한 경우 자동 구매 개수가 0인지 확인

### LottoNumberTest

- 로또 번호의 최소/최대 범위 확인
- 로또 번호가 유효 범위를 벗어난 경우 예외 확인

### LottoTest

- 로또가 6개의 번호로 생성되는지 확인
- 로또 번호 중복 여부 확인
- 직접 지정한 번호가 6개가 아닌 경우 예외 확인

### LottosTest

- 수동 로또와 자동 로또를 합한 개수만큼 로또가 생성되는지 확인
- 수동 구매가 없는 경우 자동 로또만 생성되는지 확인

### WinningNumbersTest

- 당첨 번호가 6개인지 확인
- 당첨 번호 중복 검증
- 보너스 번호와 당첨 번호 중복 검증

### LottoRankTest

- 일치 개수와 보너스 번호 일치 여부에 따라 올바른 당첨 등수를 반환하는지 확인
- 당첨 기준에 미달하면 당첨되지 않는지 확인

### LottoStatisticsTest

- 실제 로또의 당첨 결과를 등수별로 올바르게 집계하는지 확인


53 changes: 53 additions & 0 deletions src/main/java/Application.java

@quddaz quddaz Aug 5, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Application의 책임을 정리한 점이 좋습니다.

여기서 한 단계 더 나아가고 싶다면, 로또 시스템의 실행 흐름을 담당하는 객체를 별도로 만들어보는 방법도 있습니다. Application은 엄밀히 말하면 프로그램의 진입점이자 실행기 역할에 가깝기 때문에, 로또 구매부터 당첨 정보 생성과 통계 계산까지의 흐름을 LottoGame, LottoController과 같은 객체에 위임할 수 있습니다.

이렇게 하면 해당 실행기가 사용할 LottoGenerator도 외부에서 주입할 수 있어, 테스트에서 자동 생성 결과를 원하는 값으로 제어하기 쉬워집니다.

현재 구조로도 전체 흐름은 충분히 읽히지만, 실행 흐름을 별도 객체에 위임하는 구조도 생각해볼 수 있습니다. 다만 현재 규모에서 객체를 추가하는 비용보다 얻는 이점이 큰지 함께 비교해보면 좋겠습니다.

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.

실행 흐름을 별도 객체로 분리하면 Application 을 진입점 역할에 집중시킬 수 있고, LottoGenerator 를 외부에서 주입하여 실행 흐름까지 테스트하기 쉬워진다는 장점이 있다는 것을 이해하였습니다.

다만 현재는 로또 구매, 당첨 정보 생성, 통계 출력 정도로 실행 단계가 비교적 단순하고, 실제 계산이나 규칙은 각각의 도메인 객체가 담당하고 있어 Application은 주로 객체를 생성하고 연결하는 역할을 하고 있습니다. 이 상태에서 LottoGame이나 LottoController를 추가하면 현재로서는 기존 흐름을 한 번 더 감싸는 역할이 대부분일 것 같다는 생각이 들었습니다.

따라서 지금은 현재 구조를 유지하되, 이후 실행 흐름이 더 복잡해지거나 LottoGenerator 를 주입하여 전체 흐름을 제어하는 테스트가 필요해진다면 별도의 실행 객체를 분리하는 방향을 고려해보겠습니다. 감사합니다!

Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import domain.LottoNumber;
import domain.LottoStatistics;
import domain.Lottos;
import domain.PurchaseAmount;
import domain.WinningLotto;
import domain.WinningNumbers;

import view.InputView;
import view.OutputView;

import java.util.List;

public class Application {
public static void main(String[] args) {
InputView inputView = new InputView();
OutputView outputView = new OutputView();

PurchaseAmount purchaseAmount = new PurchaseAmount(inputView.readPurchaseAmount());

Lottos lottos = purchaseLottos(inputView, outputView, purchaseAmount);

WinningLotto winningLotto = createWinningLotto(inputView);

printStatistics(outputView, lottos, winningLotto, purchaseAmount);
}

private static Lottos purchaseLottos(InputView inputView, OutputView outputView, PurchaseAmount purchaseAmount) {
int manualLottoCount = inputView.readManualLottoCount();
int autoLottoCount = purchaseAmount.calculateAutoLottoCount(manualLottoCount);

List<List<Integer>> manualLottoNumbers = inputView.readManualLottoNumbers(manualLottoCount);

Lottos lottos = new Lottos(manualLottoNumbers, autoLottoCount);

outputView.printPurchasedCount(manualLottoCount, autoLottoCount);
outputView.printLottos(lottos);

return lottos;
}

private static WinningLotto createWinningLotto(InputView inputView) {
WinningNumbers winningNumbers = new WinningNumbers(inputView.readWinningNumbers());
LottoNumber bonusNumber = new LottoNumber(inputView.readBonusNumber());

return new WinningLotto(winningNumbers, bonusNumber);
}

private static void printStatistics(OutputView outputView, Lottos lottos,
WinningLotto winningLotto, PurchaseAmount purchaseAmount) {
LottoStatistics lottoStatistics = new LottoStatistics(lottos, winningLotto);
outputView.printLottoStatistics(purchaseAmount, lottoStatistics);
}
}
44 changes: 44 additions & 0 deletions src/main/java/domain/Lotto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package domain;

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

public class Lotto {
private static final int NUMBER_COUNT = 6;

private final List<LottoNumber> numbers = new ArrayList<>();

public Lotto(List<Integer> numberValues) {
validateNumberCount(numberValues);
validateDuplicateNumbers(numberValues);

convertToLottoNumbers(numberValues);

Collections.sort(numbers);
}

public List<LottoNumber> getNumbers() {
return List.copyOf(numbers);
}

private void convertToLottoNumbers(List<Integer> numberValues) {
for (int number : numberValues) {
LottoNumber lottoNumber = new LottoNumber(number);
numbers.add(lottoNumber);
}
}

private void validateNumberCount(List<Integer> numberValues) {
if (numberValues.size() != NUMBER_COUNT) {
throw new IllegalArgumentException("로또 번호는 " + NUMBER_COUNT + "개여야 합니다.");
}
}

private void validateDuplicateNumbers(List<Integer> numberValues) {
if (new HashSet<>(numberValues).size() != numberValues.size()) {
throw new IllegalArgumentException("로또 번호는 중복될 수 없습니다.");
}
}
}
40 changes: 40 additions & 0 deletions src/main/java/domain/LottoGenerator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package domain;

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

public class LottoGenerator {
private static final int NUMBER_COUNT = 6;
private static final int RANDOM_NUMBER_BOUND = 45;

private final Random random = new Random();

public Lotto generate() {
return new Lotto(generateNumbers());
}

private List<Integer> generateNumbers() {
List<Integer> numbers = new ArrayList<>();

for (int i = 0; i < NUMBER_COUNT; i++) {
numbers.add(generateRandomNumber(numbers));
}

return numbers;
}

private int generateRandomNumber(List<Integer> numbers) {
int number;

do {
number = random.nextInt(RANDOM_NUMBER_BOUND) + 1;
} while (containsNumber(numbers, number));

return number;
}

private boolean containsNumber(List<Integer> numbers, int number) {
return numbers.contains(number);
}
}
51 changes: 51 additions & 0 deletions src/main/java/domain/LottoNumber.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package domain;

import java.util.Objects;

public class LottoNumber implements Comparable<LottoNumber>{
private static final int MIN_LOTTO_NUMBER = 1;
private static final int MAX_LOTTO_NUMBER = 45;

private final int number;

public LottoNumber(int number) {
validateNumberRange(number);

this.number = number;
}

public int getNumber() {
return number;
}

@Override
public int compareTo(LottoNumber lottoNumber) {
return Integer.compare(this.number, lottoNumber.number);
}

@Override
public boolean equals(Object object) {
if (this == object) {
return true;
}

if (!(object instanceof LottoNumber)) {
return false;
}

LottoNumber lottoNumber = (LottoNumber) object;

return this.number == lottoNumber.number;
}

@Override
public int hashCode() {
return Objects.hash(number);
}

private void validateNumberRange(int number) {
if (number < MIN_LOTTO_NUMBER || number > MAX_LOTTO_NUMBER) {
throw new IllegalArgumentException("로또 번호는 1부터 45 사이의 숫자여야 합니다.");
}
}
}
44 changes: 44 additions & 0 deletions src/main/java/domain/LottoRank.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package domain;

import java.util.Arrays;
import java.util.List;

public enum LottoRank {
MISS(0,0),
FIFTH(3, 5000),
FOURTH(4,50000),
THIRD(5, 1500000),
SECOND(5, 30000000),
FIRST(6, 2000000000);

private final int matchCount;
private final long prize;

LottoRank(int matchCount, long prize) {
this.matchCount = matchCount;
this.prize = prize;
}

public static LottoRank from(int matchCount, boolean bonusMatched) {
if (matchCount == 6) return FIRST;
if (matchCount == 5 && bonusMatched) return SECOND;
if (matchCount == 5) return THIRD;
if (matchCount == 4) return FOURTH;
if (matchCount == 3) return FIFTH;
return MISS;
}

public int getMatchCount() {
return matchCount;
}

public long getPrize() {
return prize;
}

public static List<LottoRank> winningRanks() {
return Arrays.stream(values())
.filter(rank -> rank != MISS)
.toList();
}
}
Loading