Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
23 changes: 23 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# 로또 - 클린 코드

# 자바 로또

## 기능 목록
- 로또 구입 금액을 입력받는다.
- 수동 구매 개수를 입력받는다.
- 수동 로또 번호를 입력받는다.
- 남은 금액만큼 자동 로또를 발행한다.
- 발행한 로또 목록을 출력한다.
- 당첨 번호와 보너스 번호를 입력받는다.
- 당첨 결과를 집계한다.
- 수익률을 계산해 출력한다.

## 예외 사항
- 구입 금액은 1000원 이상이어야 한다.
- 구입 금액은 1000원 단위여야 한다.
- 수동 구매 개수는 구매 가능한 로또 수를 초과할 수 없다.
- 로또 번호는 1~45 사이여야 한다.
- 로또 번호는 6개여야 한다.
- 로또 번호는 중복될 수 없다.
- 보너스 번호는 당첨 번호와 중복될 수 없다.

18 changes: 18 additions & 0 deletions src/main/java/Main.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import controller.Controller;
import domain.LottoService;
import domain.Statistic;
import domain.RandomLottoGenerator;
import view.InputView;
import view.OutputView;

public class Main {
public static void main(String[] args) {
Controller controller = new Controller(
new InputView(),
new OutputView(),
new LottoService(new RandomLottoGenerator()),
new Statistic()
);
controller.run();
}
}
47 changes: 47 additions & 0 deletions src/main/java/controller/Controller.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package controller;

import domain.LottoNumber;
import domain.LottoService;
import domain.LottoTicket;
import domain.Statistic;
import domain.WinningNumbers;
import domain.WinningRank;
import java.util.Map;
import view.InputView;
import view.OutputView;

import java.util.List;

public class Controller {
private final InputView inputView;
private final OutputView outputView;
private final LottoService lottoService;
private final Statistic statistic;

public Controller(InputView inputView, OutputView outputView,
LottoService lottoService, Statistic statistic) {
this.inputView = inputView;
this.outputView = outputView;
this.lottoService = lottoService;
this.statistic = statistic;
}

public void run(){
int money = inputView.getMoney();
int manualCount = inputView.getManualLottoCount();
List<List<Integer>> manualTicketNumbers = inputView.getManualTicketNumbers(manualCount);

List<LottoTicket> allTickets = lottoService.buyTickets(money, manualTicketNumbers);
outputView.printLottoList(manualCount, allTickets.size(), allTickets);

List<LottoNumber> winningNumberList = inputView.getWinningNumbers();
LottoNumber bonusNumber = inputView.getBonusNumber();

WinningNumbers winningNumbers = new WinningNumbers(winningNumberList, bonusNumber);

Map<WinningRank, Integer> winningResult = statistic.getWinningResult(allTickets, winningNumbers);
double revenue = statistic.getRevenue(money, winningResult);

outputView.printResult(winningResult, revenue);
}
}
7 changes: 7 additions & 0 deletions src/main/java/domain/LottoGenerator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package domain;

import java.util.List;

public interface LottoGenerator {
List<LottoNumber> generateNumbers();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

컬렉션과 배열은 그 개수를 한정짓지 못한다는 문제가 있어요. 이것도 처음보는 사람이라면 6개가 나온다는 사실을 몰랐을 수도 있습니다.
인터페이스를 설계할 때에는 '그 구현체가 앞으로 절대 잘못 구현되지 않을 정도'로 정확하고 명확하게 구현해주는게 좋습니다.

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

import java.util.Objects;

public class LottoNumber {
public static final int MIN_NUMBER = 1;
public static final int MAX_NUMBER = 45;
Comment on lines +6 to +7
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으로 열어줄 큰 이유가 없다면 최대한 닫아주는게 좋아보여요.

private final int value;

public LottoNumber(int value) {
validateBound(value);
this.value = value;
}

public int value() {
return value;
}

@Override
public boolean equals(Object object) {
if (this == object) {
return true;
}
if (!(object instanceof LottoNumber lottoNumber)) {
return false;
}
return value == lottoNumber.value;
}

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

@Override
public String toString() {
return String.valueOf(value);
}

private void validateBound(int value) {
if (value < MIN_NUMBER || value > MAX_NUMBER) {
throw new IllegalArgumentException(
String.format("로또 번호는 %d부터 %d 사이여야 합니다. 입력값: %d", MIN_NUMBER, MAX_NUMBER, value)
);
}
}
}
73 changes: 73 additions & 0 deletions src/main/java/domain/LottoService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package domain;

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

public class LottoService {
public static final int TICKET_PRICE = 1000;
LottoGenerator lottoGenerator;

public LottoService(LottoGenerator lottoGenerator) {
this.lottoGenerator = lottoGenerator;
}

public List<LottoTicket> buyTickets(int money, List<List<Integer>> manualNumbers) {
validateMoney(money);
validateManualCount(money, manualNumbers);

List<LottoTicket> tickets = new ArrayList<>(generateManualTickets(manualNumbers));

int autoCount = calculateAutoCount(money, manualNumbers.size());
tickets.addAll(generateAutoTickets(autoCount));

return tickets;
}

private List<LottoTicket> generateManualTickets(List<List<Integer>> manualNumbers) {
List<LottoTicket> tickets = new ArrayList<>();

for (List<Integer> numbers : manualNumbers) {
tickets.add(new LottoTicket(
numbers.stream()
.map(LottoNumber::new)
.toList()
));
}

return tickets;
}
Comment on lines +26 to +38
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

LottoNumber를 직접 다 만들어서 LottoTicket한테 '너 이거 써'라고 말하고 있어요.
Tell Dont Ask라는 격언과 상충되는 코드인데, 요 글을 읽어보면서 고민해보면 좋을 것 같아요~


private List<LottoTicket> generateAutoTickets(int autoCount) {
List<LottoTicket> tickets = new ArrayList<>();

for (int i = 0; i < autoCount; i++) {
tickets.add(new LottoTicket(lottoGenerator.generateNumbers()));
}

return tickets;
}
Comment on lines +26 to 48
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

이 로직은 LottoService가 들고 있는게 좋을까요 LottoTicket이 직접 들고 있는게 좋을까요?


private int calculateAutoCount(int money, int manualCount) {
return money / TICKET_PRICE - manualCount;
}

private void validateMoney(int money) {
if (money < TICKET_PRICE) {
throw new IllegalArgumentException("1000원 이상의 금액을 입력해주세요.");
}

if (money % TICKET_PRICE != 0) {
throw new IllegalArgumentException("1000원 단위의 금액을 입력해주세요.");
}
}

private void validateManualCount(int money, List<List<Integer>> manualNumbers) {
if (manualNumbers == null) {
throw new IllegalArgumentException("수동 번호는 비어 있을 수 없습니다.");
}

if (manualNumbers.size() > money / TICKET_PRICE) {
throw new IllegalArgumentException("수동 구매 수량이 구입 금액을 초과할 수 없습니다.");
}
}
Comment on lines 54 to 72
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

요런 로직이 Service에 많아지면 점점 코드가 읽기 어려워질 것 같아요. 유지보수하기도 어렵고요.
무엇보다 LottoService는 어떤 상태를 가지지 않는 객체인 것 같은데, 그래서 이런 객체가 중요한 로직을 들고 있는 것도 고민 해볼만한 점이에요.

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

import java.util.Comparator;
import java.util.List;

public class LottoTicket {
private static final int LOTTO_SIZE = 6;
List<LottoNumber> lottoNumbers;

public LottoTicket (List<LottoNumber> lottoNumbers) {
validateSize(lottoNumbers);
validateDuplicate(lottoNumbers);
this.lottoNumbers = lottoNumbers;
}

public String toDisplayString() {
return lottoNumbers.stream()
.sorted(Comparator.comparingInt(LottoNumber::value))
.toString();
}
Comment on lines 16 to 20
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

toString이 아닌 별도 메서드를 만들어주신 이유가 뭔가요?
추가적으로, 이런 뷰와 관련된 기능이 도메인에 존재해도 괜찮을지 고민해보면 좋을 것 같아요.


public WinningRank getWinningRank(WinningNumbers winningNumbers) {
int matchCount = countMatchNumbers(winningNumbers);
boolean bonusMatched = lottoNumbers.contains(winningNumbers.getBonusNumber());
return WinningRank.of(matchCount, bonusMatched);
}

private int countMatchNumbers(WinningNumbers winningNumbers) {
int count = 0;
for (LottoNumber winningNumber : winningNumbers.getLottoNumbers()) {
if (lottoNumbers.contains(winningNumber)) {
count++;
}
}
return count;
}

private void validateSize(List<LottoNumber> numbers) {
if (numbers == null || numbers.size() != LOTTO_SIZE) {
throw new IllegalArgumentException("로또 번호는 6개여야 합니다.");
}
}

private void validateDuplicate(List<LottoNumber> numbers) {
long distinctCount = numbers.stream()
.distinct()
.count();

if (distinctCount != LOTTO_SIZE) {
throw new IllegalArgumentException("로또 번호는 중복될 수 없습니다.");
}
}
}
30 changes: 30 additions & 0 deletions src/main/java/domain/RandomLottoGenerator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package domain;

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

public class RandomLottoGenerator implements LottoGenerator {
private static final int LOTTO_SIZE = 6;
private static final int LOTTO_MAX_NUMBER = 45;
private static final int LOTTO_MIN_NUM = 1;

Random random = new Random();
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 List<LottoNumber> generateNumbers() {
List<LottoNumber> numbers = new ArrayList<>();

while (numbers.size() < LOTTO_SIZE) {
LottoNumber lottoNumber = new LottoNumber(generateRandomNumber());
if (!numbers.contains(lottoNumber)) {
numbers.add(lottoNumber);
}
}

return numbers;
}

private int generateRandomNumber() {
return LOTTO_MIN_NUM + random.nextInt(LOTTO_MAX_NUMBER);
}
}
38 changes: 38 additions & 0 deletions src/main/java/domain/Statistic.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package domain;

import java.util.EnumMap;
import java.util.List;
import java.util.Map;

public class Statistic {

public Map<WinningRank, Integer> getWinningResult(List<LottoTicket> tickets, WinningNumbers winningNumbers) {
Map<WinningRank, Integer> result = new EnumMap<>(WinningRank.class);

for (WinningRank rank : WinningRank.values()) {
result.put(rank, 0);
}

for (LottoTicket ticket : tickets) {
WinningRank rank = ticket.getWinningRank(winningNumbers);

if (rank == WinningRank.MISS) {
continue;
}

result.put(rank, result.get(rank) + 1);
}

return result;
}

public double getRevenue(int money, Map<WinningRank, Integer> result) {
long totalPrize = 0;

for (WinningRank rank : WinningRank.values()) {
totalPrize += (long) result.get(rank) * rank.getPrize();
}

return (double) totalPrize / money;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

이거 100 곱해줘야 하지 않나요? 큰 상관은 없긴 합니다 ㅎㅎ.

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

import java.util.List;

public class WinningNumbers {
private static final int LOTTO_SIZE = 6;
private final List<LottoNumber> lottoNumbers;
private final LottoNumber bonusNumber;

public WinningNumbers(List<LottoNumber> lottoNumbers, LottoNumber bonusNumber) {
validateSize(lottoNumbers);
validateDuplicate(lottoNumbers, bonusNumber);
this.lottoNumbers = lottoNumbers;
this.bonusNumber = bonusNumber;
}

public List<LottoNumber> getLottoNumbers() {
return lottoNumbers;
}

public LottoNumber getBonusNumber() {
return bonusNumber;
}

private void validateSize(List<LottoNumber> numbers) {
if (numbers == null || numbers.size() != LOTTO_SIZE) {
throw new IllegalArgumentException("당첨 번호는 6개여야 합니다.");
}
}

private void validateDuplicate(List<LottoNumber> numbers, LottoNumber bonusNumber) {
if (numbers.contains(bonusNumber)) {
throw new IllegalArgumentException("보너스 번호는 당첨 번호와 중복될 수 없습니다.");
}

long distinctCount = numbers.stream()
.distinct()
.count();
if (distinctCount != LOTTO_SIZE) {
throw new IllegalArgumentException("당첨 번호는 중복될 수 없습니다.");
}
}
Comment on lines +25 to +42
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

LottoTicket과 비슷한 로직과 검증이 있어요.
LottoTicket을 활용해서 작성하는 방법도 좋을 것 같아요. (어렵지만요)

}
Loading