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

## Level1. 로또 자동 구매

### - 요구사항

- [x] 로또 구입 금액을 입력하면 구입 금액에 해당하는 로또를 발급해야 한다.
- [x] 로또 1장의 가격은 1000원이다.

### - 새로운 프로그래밍 요구사항

- [x] 배열 대신 컬렉션을 사용한다.
- [x] 줄여 쓰지 않는다(축약 금지).
- [x] 함수(또는 메서드)의 길이가 10라인을 넘어가지 않도록 구현한다.
- [x] 함수(또는 메서드)가 한 가지 일만 하도록 최대한 작게 만들어라.

## Level2. 로또 당첨

### - 요구사항

- [x] 로또 당첨 번호를 받아 일치한 번호 수에 따라 당첨 결과를 보여준다.

### - 새로운 프로그래밍 요구사항

- [x] 모든 원시 값과 문자열을 포장한다.
- [x] 일급 컬렉션을 쓴다.

## Level3. 로또 2등 당첨

### - 요구사항

- [ ] 2등을 위한 보너스볼을 추첨한다.
- [ ] 당첨 통계에 2등을 추가한다.
- [ ] 2등 당첨 조건은 당첨 번호 5개 일치 + 보너스 볼 일치다.

### - 새로운 프로그래밍 요구사항

- [ ] Java Enum을 적용한다.

## Level4. 로또 수동 구매

### - 요구사항

- [ ] 현재 로또 생성기는 자동 생성 기능만 제공한다. 사용자가 수동으로 추첨 번호를 입력할 수 있도록 해야 한다.
- [ ] 입력한 금액, 자동 생성 숫자, 수동 생성 번호를 입력하도록 해야 한다.

## Level5. 리팩토링

### - 새로운 프로그래밍 요구사항

- [ ] 기존 프로그래밍 요구사항을 다시 한번 확인하고, 학습 테스트를 통해 학습한 내용을 반영한다.

### - 기존 프로그래밍 요구사항

- [ ] 자바 코드 컨벤션을 지키면서 프로그래밍한다.
- [ ] 기본적으로 Java Style Guide을 원칙으로 한다.
- [ ] indent(인덴트, 들여쓰기) depth를 2를 넘지 않도록 구현한다. 1까지만 허용한다.
- [ ] 예를 들어 while문 안에 if문이 있으면 들여쓰기는 2이다. 힌트: indent(인덴트, 들여쓰기) depth를 줄이는 좋은 방법은 함수(또는 메서드)를 분리하면
된다.
- [ ] 3항 연산자를 쓰지 않는다.
- [ ] else 예약어를 쓰지 않는다.
- [ ] else 예약어를 쓰지 말라고 하니 switch/case로 구현하는 경우가 있는데 switch/case도 허용하지 않는다. 힌트: if문에서 값을 반환하는 방식으로
구현하면 else 예약어를 사용하지 않아도 된다.
- [ ] 배열 대신 컬렉션을 사용한다.
- [ ] 줄여 쓰지 않는다(축약 금지).
- [ ] 함수(또는 메서드)의 길이가 10라인을 넘어가지 않도록 구현한다.
- [ ] 함수(또는 메서드)가 한 가지 일만 하도록 최대한 작게 만들어라.
- [ ] 모든 원시 값과 문자열을 포장한다.
- [ ] 일급 컬렉션을 쓴다.
- [ ] Java Enum을 적용한다.
12 changes: 12 additions & 0 deletions src/main/java/lotto/LottoController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package lotto;

import lotto.view.LottoInputView;
import lotto.view.LottoOutputView;

public class LottoController {

public static void main(String[] args) {
LottoService service = new LottoService(new LottoInputView(), new LottoOutputView());
Copy link
Author

Choose a reason for hiding this comment

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

  1. 지난 미션에선 Main이란 이름의 클래스에서 Controller를 조작하도록 구현했었는데,
    Controller가 하는 일과 네이밍이 잘 안맞는 듯해서 이번엔 Controller내 메인 클래스를 두고 프로그램 조작만을 담당하도록 했습니다.!

  2. 지난 미션에선 하나의 View 객체를 만들어서 입/출력 관련 역할을 모았었는데요,
    스캐너를 사용하는 건 같지만 출력/입력은 다른 역할이라는 생각이 들어 입/출력 View 클래스를 나누어 보았습니다.

Choose a reason for hiding this comment

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

예진님이 생각하시는 Controller의 역할은 무엇인가요? 그렇다면 Service의 역할은 무엇인가요?

Copy link
Author

Choose a reason for hiding this comment

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

Controller : 외부 요청을 받고, 그 요청을 처리할 적절한 로직으로 연결해주는 역할
Service : 비즈니스 로직을 담당하는 역할

이번 미션에선 요청이라고는 할 수 없지만, 실행 및 흐름을 조작을 하는 역할을 하고, 나머지 기능은 서비스단에서 구현하도록 했습니ㄷ!

service.start();
}
}
56 changes: 56 additions & 0 deletions src/main/java/lotto/LottoService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package lotto;

import java.util.List;
import java.util.stream.Collectors;
import lotto.model.Lotto;
import lotto.model.LottoGenerator;
import lotto.model.LottoTicket;
import lotto.model.MatchCount;
import lotto.model.Money;
import lotto.model.WinningNumbers;
import lotto.model.WinningResult;
import lotto.view.LottoInputView;
import lotto.view.LottoOutputView;

public class LottoService {

private final LottoInputView inputView;
private final LottoOutputView outputView;
private final LottoGenerator lottoGenerator;

public LottoService(LottoInputView inputView, LottoOutputView outputView) {
this.inputView = inputView;
this.outputView = outputView;
this.lottoGenerator = new LottoGenerator();
}

public void start() {
Money purchaseMoney = new Money(inputView.inputMoney());

List<Lotto> lottos = purchaseLottos(purchaseMoney);

LottoTicket lottoTicket = purchaseMoney.calculateTicketCount();
Copy link
Author

Choose a reason for hiding this comment

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

LottoTicket은 로또를 몇개 구입했는 지에 대한 정보를 가진 객체입니다.
원시값은 사용하지 않는 다는 이번 요구사항이 있는데, 만약 이 메서드 내에서
int lottoTicketCount = purchaseMoney.calculateTicketCount().getCount();를 해버리면 요구사항에 어긋나게 되는 걸까요?

int lottoTicketCount으로 사용하게 되면 이 메서드 내에서만 사용하는 거니 중복에 대한 고려가 없어도 되고, 오히려 더 명시적이지 않은가? 라는 고민을 했었습니답!

Choose a reason for hiding this comment

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

LottoTicket이 정말 필요한 객체일까요?

요구사항에서는 모든 원시값을 포장하라고 했어요.

그렇다고 프로젝트에 사용되는 모든 원시값을 포장하면 어떻게 될까요?

Copy link
Author

Choose a reason for hiding this comment

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

그렇다고 프로젝트에 사용되는 모든 원시값을 포장하면 어떻게 될까요?

저도 진행하면서 정말 필요할까라는 생각을 했는데, 요구사항인 모든 이라는 말에 아닌 거 같으면서도 모든 원시값을 포장했네요😅
어제 미팅에서 처럼 요구사항이라 할지라도 아닌 것 같을 땐 의문을 가지는 자세를 가져야 할 것 같아요!


outputView.printPurchasedLottoCount(lottoTicket.getCount());
outputView.printLottos(lottos);

WinningNumbers winningNumbers = new WinningNumbers(inputView.inputWinningNumber());

WinningResult winningResult = calculateWinningResult(lottos, winningNumbers);

outputView.printWinningStatistics(purchaseMoney.getValue(),
winningResult.getWinningStatistics());

Choose a reason for hiding this comment

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

거의 각 코드마다 줄바꿈이 있네요!

어떤 기준을 통해 줄바꿈을 해주셨나요?

Copy link
Author

Choose a reason for hiding this comment

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

outputView는 똑같은 출력이랑 붙였고 나머지는.. 큰 기준없이 띄웠는데 가독성이 떨어지나요?? 더 나은 방향이 있을까요?😅

}

private List<Lotto> purchaseLottos(Money money) {
return lottoGenerator.generate(money.calculateTicketCount().getCount());
}

private WinningResult calculateWinningResult(List<Lotto> lottos, WinningNumbers winningNumbers) {
List<MatchCount> matchCounts = lottos.stream()
.map(lotto -> lotto.matchWith(winningNumbers))
.collect(Collectors.toList());

return new WinningResult(matchCounts);
}
}
26 changes: 26 additions & 0 deletions src/main/java/lotto/model/Lotto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package lotto.model;

import java.util.List;

public class Lotto {

private final LottoNumbers numbers;

Choose a reason for hiding this comment

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

Lotto, LottoNumbers 둘의 차이는 뭔가요? 각자 어떤 역할을 맡고 있나요?

Copy link
Author

Choose a reason for hiding this comment

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

하나의 로또티켓이 있고, 이 티켓이 보유한 번호 집합을 LottoNumbers로 갖고 있도록 했는데요,
로또 티켓 안에 로또 번호가 여러개일 수 있는 점을 생각해보면, 포장이 명확하지 않았던 것 같아 아래와 같이 수정했습니다.

public class Lottos {

    private final List<LottoNumbers> lottoNumbers;


public Lotto(List<Integer> numbers) {
this.numbers = new LottoNumbers(numbers);
}

Choose a reason for hiding this comment

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

Suggested change
public Lotto(List<Integer> numbers) {
this.numbers = new LottoNumbers(numbers);
}
public Lotto(LottoNumbers LottoNumbers) {
this.numbers = LottoNumbers;
}

생성자의 인자로 LottoNumbers이 아닌, List<Integer> 타입을 받으신 이유가 있으실까요?

Copy link
Author

Choose a reason for hiding this comment

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

앗 수정했습니다🤣🤣


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

public MatchCount matchWith(WinningNumbers winningNumbers) {
return new MatchCount(calculateMatchCount(winningNumbers));
}

private int calculateMatchCount(WinningNumbers winningNumbers) {
return (int) numbers.getNumbers().stream()
.filter(winningNumbers.getNumbers()::contains)
.count();
}
}
33 changes: 33 additions & 0 deletions src/main/java/lotto/model/LottoGenerator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package lotto.model;

import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

public class LottoGenerator {

private static final int MAX_NUMBER = 45;
private static final int LOTTO_SIZE = 6;

public List<Lotto> generate(int count) {
return IntStream.range(0, count)
.mapToObj(i -> new Lotto(pickFirstSixSorted(createShuffledNumbers())))
.collect(Collectors.toList());
}

private List<Integer> createShuffledNumbers() {
List<Integer> numbers = IntStream.rangeClosed(1, MAX_NUMBER)
.boxed()
.collect(Collectors.toList());
Collections.shuffle(numbers);
return numbers;
}

private List<Integer> pickFirstSixSorted(List<Integer> numbers) {
return numbers.stream()
.limit(LOTTO_SIZE)
.sorted()
.collect(Collectors.toList());
}
}

Choose a reason for hiding this comment

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

로또 번호를 생성해 주는 클래스군요!

해당 클래스에서 생성된 로또 번호는 1~45 사이의 중복되지 않은 값 6개 라는 것을 알 수 있어요.

하지만 그 의미는 해당 클래스 내에서만 유효하고, 해당 클래스를 사용하는 곳에서는 그 의미를 찾을 수 없어요.

LottoGenerator lottoGenerator = new LottoGenerator();
List<Integer> lottoNumbers = lottoGenerator.generate();

lottoNumbers.add(1);

그렇다면 해당 클래스를 사용하는 곳에서도 그 의미를 어떻게 전달할 수 있을까요?

Copy link
Author

Choose a reason for hiding this comment

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

List<Integer> 대신 의미를 가진 LottoNumbers객체를 생성하면 의미가 명확할 것 같습니다

26 changes: 26 additions & 0 deletions src/main/java/lotto/model/LottoNumbers.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package lotto.model;

import java.util.Collections;
import java.util.List;

public class LottoNumbers {

private static final int LOTTO_SIZE = 6;
private final List<Integer> numbers;

public LottoNumbers(List<Integer> numbers) {
validate(numbers);
this.numbers = numbers;
}

Choose a reason for hiding this comment

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

LottoNumbers는 로또 번호에 대한 지식을 가지고 있군요!

그 지식 중 하나는 "반드시 6개의 번호를 가진다"에요.

즉, LottoNumbers는 필드로 가지는 List<Integer>에 6개의 숫자가 반드시 포함되어야 해요.

이 규칙은 절대로 변하면 안 되는 도메인 규칙이에요.
(또는 다른 나라의 로또를 구현해야 하거나, 우리나라의 로또의 규칙이 달라지거나 하면 변할 수 있겠지만..)

하지만 지금 구현에서는 정말 "6개"의 숫자가 변경되지 않는다고 보장할 수 있을까요?

List<Integer> numbers = new ArrayList();
LottoNumbers lottoNumbers = new LottoNumbers(numbers);
numbers.clear();

assertThat(lottoNumbers.getNumbers()).hasSize(6);

Copy link
Author

Choose a reason for hiding this comment

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

거기까지 방어해야한단 생각을 못했던 것 같아요!!
민지님의 코드를 참고해 this.numbers = List.copyOf(numbers);로 수정하여 추후 변경이 불가하도록 했습니다


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

// 수정 불가능한 숫자 리스트를 반환
public List<Integer> getNumbers() {
return Collections.unmodifiableList(numbers);
}
}
14 changes: 14 additions & 0 deletions src/main/java/lotto/model/LottoTicket.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package lotto.model;

public class LottoTicket {

private final int count;

public LottoTicket(int count) {
this.count = count;
}

public int getCount() {
return count;
}
}
14 changes: 14 additions & 0 deletions src/main/java/lotto/model/MatchCount.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package lotto.model;

public class MatchCount {

private final int count;

public MatchCount(int count) {
this.count = count;
}

public int getCount() {
return count;
}
}
20 changes: 20 additions & 0 deletions src/main/java/lotto/model/Money.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package lotto.model;

public class Money {

Choose a reason for hiding this comment

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

int money = 1000 대신 Money money = new Money(1000)과 같이 사용하면, 명확하게 돈이라는 의미를 가지게 할 수 있겠군요!

또한 내부 필드가 final으로 지정되어, 여러 스레드에서 공유해도 안전할 것 같네요!

이러한 객체 구현을 "값 객체(Value Object)"라고 불러요.

값 객체는 상태를 변경 메서드를 제공하지 않으며, 그 자체로 식별성을 가져요.

하지만 지금 구현에서도 그럴까요?

Money first = new Money(100);
Money second = new Money(100);

assertThat(first).isEqualTo(second);

Copy link
Author

Choose a reason for hiding this comment

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

감사합니다 수정했습니다 !!


private static final int LOTTO_PRICE = 1000;
private final int value;

Choose a reason for hiding this comment

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

로또의 당첨금은 21억인데요, 만약 1등이 2번 당첨되면 어떻게 될까요?

Copy link
Author

Choose a reason for hiding this comment

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

그래서 출력할 때 long 타입으로 상금을 출력했습니다..!
long totalPrize = 0L;

21억이 넘는 금액을 구매할 일을 생각해서 long으로 선언해야할까요? 제가 이해한 게 맞을까요?


public Money(int value) {
this.value = value;
}

public int getValue() {
return value;
}

public LottoTicket calculateTicketCount() {
Copy link
Author

Choose a reason for hiding this comment

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

로또를 몇개 구매했는지 알고싶다면 얼마를 지불했는지 알아야하니까 Money객체 내에서 계산하도록 했는데요,
로직상으론 문제 없는 것 같은데 뭔가 Money라는 객체에서 로또를 몇개 구입했는지 아는게 명시적인가? 라는 고민이 들었습니다

Choose a reason for hiding this comment

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

로또의 구매라는 역할을 하는 클래스가 있으면 되지 않을까요?

Copy link
Author

@yaevrai yaevrai Jun 1, 2025

Choose a reason for hiding this comment

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

PurchaseLotto라는 클래스를 만들어서 역할을 나눠보았습니다 !
그런데 그러면 일급컬렉션이어야한다는 요구사항에 어긋나게 되는 걸까요?ㅠㅠ

return new LottoTicket(value / LOTTO_PRICE);
}

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

import java.util.List;

public class WinningNumbers {

private final LottoNumbers numbers;

public WinningNumbers(List<Integer> numbers) {
this.numbers = new LottoNumbers(numbers);
}

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

}
49 changes: 49 additions & 0 deletions src/main/java/lotto/model/WinningResult.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package lotto.model;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class WinningResult {

private static final Map<Integer, Integer> PRIZE_MONEY = Map.of(
3, 5_000,
4, 50_000,
5, 1_500_000,
6, 2_000_000_000
);

Choose a reason for hiding this comment

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

해당 Map을 보았을 때, Key와 Value가 명확한 의미를 가지고 있나요?

Copy link
Author

Choose a reason for hiding this comment

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

명확하지 않은 것 같습니다. 여기서도 마찬가지로 순위를 나타내는 enum을 통해 명시적으로 사용하도록 개선했습니다.


private final Map<Integer, Integer> matchResults;

public WinningResult(List<MatchCount> matchCounts) {
this.matchResults = calculateResults(matchCounts);
}

private Map<Integer, Integer> calculateResults(List<MatchCount> matchCounts) {
Map<Integer, Integer> results = new HashMap<>();
PRIZE_MONEY.keySet().forEach(matchCount -> results.put(matchCount, 0));

for (MatchCount matchCount : matchCounts) {
int count = matchCount.getCount();
if (count >= 3) {
results.put(count, results.get(count) + 1);
}
}

Choose a reason for hiding this comment

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

indent(인덴트, 들여쓰기) depth를 2를 넘지 않도록 구현한다. 1까지만 허용한다.

Choose a reason for hiding this comment

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

코드를 보니, 맞춘 개수가 3개 이상이면, 결과의 개수를 하나씩 증가시키는 로직 같네요!

하지만 너무 장황하다는 느낌이 들고, 굳이 Map을 사용하지 않아도 됐을 것 같아요.

Map을 사용하지 않고, 객체를 만들어서 표현해 볼 수 있을까요?


return results;
}

public Map<String, Long> getWinningStatistics() {
Map<String, Long> statistics = new HashMap<>();

matchResults.forEach((matchCount, count) ->
statistics.put(String.valueOf(matchCount), (long) count));

long totalPrize = matchResults.entrySet().stream()
Copy link
Author

Choose a reason for hiding this comment

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

제가 Map - entrySet에 대한 이해도가 높지않아 해당 부분에서 Gpt의 도움을 받았습니다 😅
결과를 단순 조회할 땐 .forEach(), 값을 연산할 땐 .stream()을 사용하는 걸 권장한다고 하여 이렇게 작성했는데
바보같은 질문일 수도 있을 것 같지만.. 성능적으로는 이렇게 구현하는 게 권장될 수 있지만 .stream() 등을 잘 모르는 사람이 봤을때
같은 matchResults를 다른 메서드를 통해 조작한다는 게 통일성 없어보이거나 이해가 어렵지 않으려나? 가독성이 별로지않나? 라는 개인적인 생각을 했습니다ㅎㅎ 아무래도 기능/성능적 부분이 우선일까요? 의견을 공유받고 싶습니다!_!

Choose a reason for hiding this comment

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

성능이라고 한다면 어느 정도의 성능을 보장하는 것을 목표로 하시나요?

컴퓨터는 생각보다 매우 빠르고, 자바 또한 성능이 느리다는 말은 옛말이 된지 오래에요.
(제가 가진 M1 맥북은 160억 개의 트랜지스터가 있고, 초당 11조 번의 연산력을 제공한다고 해요)

그렇다면 지금 구현해 주신 로또 게임이 성능을 신경 써야 할 정도의 연산이 필요할까요?

그게 아니라면 무엇을 선택해야 할까요?

Copy link
Author

Choose a reason for hiding this comment

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

관련 링크 감사합니다!_! 😃
성능을 크게 고려하지 않아도 되는 부분이면, 가독성이 우선이 될 것 같아. stream을 사용하는 게 좋을 거란 생각이 듭니다.

.mapToLong(entry -> (long) PRIZE_MONEY.get(entry.getKey()) * entry.getValue())

Choose a reason for hiding this comment

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

entry.getKey(), entry.getValue()는 각각 무슨 의미를 가지나요?

Copy link
Author

Choose a reason for hiding this comment

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

entry.getKey() : 매치된 수
entry.getValue() : 상금

입니다. 그런데 명확하지 않은 것 같아 enum을 만들어 수정했습니다!

.sum();

statistics.put("total", totalPrize);
return statistics;
}
}
30 changes: 30 additions & 0 deletions src/main/java/lotto/view/LottoInputView.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package lotto.view;

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

public class LottoInputView {

public int inputMoney() {
System.out.println("구입금액을 입력해 주세요.");
Scanner scanner = new Scanner(System.in);

return scanner.nextInt();
}

public List<Integer> inputWinningNumber() {
System.out.println("지난 주 당첨 번호를 입력해 주세요.");
Scanner scanner = new Scanner(System.in);

String winningNumbersString = scanner.nextLine();
String[] numbers = winningNumbersString.split(",");
ArrayList<Integer> winningNumbers = new ArrayList<>();

for (String num : numbers) {
winningNumbers.add(Integer.parseInt(num.trim()));
}

return winningNumbers;
}
}
Loading