Skip to content
Open
Show file tree
Hide file tree
Changes from 69 commits
Commits
Show all changes
79 commits
Select commit Hold shift + click to select a range
adf5bbd
feat: 입력 메시지 생성
mgim9316-a11y Mar 29, 2026
48c77a2
feat: Controller 호출
mgim9316-a11y Mar 29, 2026
474aadd
docs: 구현 순서 작성
mgim9316-a11y Mar 29, 2026
b5eb6ff
refactor: 메서드 이름 수정
mgim9316-a11y Mar 29, 2026
381d67b
feat: 구입 금액 작성 기능 추가
mgim9316-a11y Mar 29, 2026
d4c2f13
docs: 생략된 부분 수정
mgim9316-a11y Mar 29, 2026
15b6263
feat: 시도 횟수 관리 클래스 생성
mgim9316-a11y Mar 29, 2026
d5fff4a
feat: 시도 횟수 Controller로 호출 기능 추가
mgim9316-a11y Mar 29, 2026
b0d1d45
chore: 컨벤션 수정
mgim9316-a11y Mar 29, 2026
84eb0a5
feat: 로또 번호를 랜덤으로 작성하는 기능 추가 및 일급 컬렉션으로 관리
mgim9316-a11y Mar 29, 2026
c5de9ef
feat: 랜덤 로또 번호를 출력하는 기능 추가
mgim9316-a11y Mar 29, 2026
a5a6c94
feat: 당첨 번호를 입력 받는 기능 추가
mgim9316-a11y Mar 29, 2026
4200424
feat: 수익률을 계산하는 기능 추가
mgim9316-a11y Mar 29, 2026
96c0a6d
feat: Enum을 통한 도메인 규칙 캡슐화
mgim9316-a11y Mar 29, 2026
1a69d0a
feat: 구매값 반환
mgim9316-a11y Mar 29, 2026
41c7019
feat: 결과 출력 기능 생성
mgim9316-a11y Mar 29, 2026
e37570e
fix: 오류 수정
mgim9316-a11y Mar 29, 2026
bf79e60
feat: 당첨번호 입력 호출, 결과 출력 호출 기능 추가
mgim9316-a11y Mar 29, 2026
ab9294d
refactor: 효율적으로 출력기능 변경
mgim9316-a11y Mar 29, 2026
b2c9729
refactor: 효율적으로 출력기능 변경
mgim9316-a11y Mar 29, 2026
71766eb
refactor: 요구사항 출력 기능 생성
mgim9316-a11y Mar 29, 2026
3b833ed
feat: TrialNumber 검증기능 추가
mgim9316-a11y Mar 30, 2026
1cf15ec
feat: 당첨번호, 구입 금액 검증 기능 추가
mgim9316-a11y Mar 30, 2026
0df50c8
chore: 변수명 변경
mgim9316-a11y Mar 30, 2026
6e69fda
chore: 변수명 변경
mgim9316-a11y Mar 30, 2026
1dc050d
docs: Contoller의 기능 위주로 리드미 수정
mgim9316-a11y Mar 30, 2026
35e644e
chore: 공백 추가
mgim9316-a11y Mar 30, 2026
99e7044
chore: 변수명 수정
mgim9316-a11y Mar 30, 2026
b469635
chore: 변수명 수정
mgim9316-a11y Mar 31, 2026
1bc2243
refactor: 검증 순서 수정
mgim9316-a11y Mar 31, 2026
1bc0835
feat: 잘못 입력시 다시 숫자를 입력 받는 로직 추가
mgim9316-a11y Mar 31, 2026
4429df3
feat: 로또 배열을 생성하는 기능 분리
mgim9316-a11y Mar 31, 2026
33c6110
refactor: depth의 길이 조건 만족
mgim9316-a11y Mar 31, 2026
978b639
refactor: 변수명 변경
mgim9316-a11y Mar 31, 2026
5e9291d
refactor: 로또 생성 기능 분리
mgim9316-a11y Mar 31, 2026
167c8b0
refactor: domain test 코드 작성
mgim9316-a11y Mar 31, 2026
f22c39d
feat: 로또 번호 숫자 범위 확인 기능 추가
mgim9316-a11y Mar 31, 2026
9c7c8f2
refactor: depth1 넘지 않도록 수정
mgim9316-a11y Mar 31, 2026
a41be4e
chore: 공백추가
mgim9316-a11y Mar 31, 2026
e97bb51
chore: 공백추가
mgim9316-a11y Mar 31, 2026
ff87d7b
refactor: 와일드카드 수정, run 메서드 분리, depth1로 생성
mgim9316-a11y Apr 3, 2026
bdccdfe
refactor: depth1로 생성
mgim9316-a11y Apr 3, 2026
06e66e3
refactor: depth1로 생성
mgim9316-a11y Apr 3, 2026
9b880d8
refactor: given 주석 추가
mgim9316-a11y Apr 3, 2026
5db58c1
feat: 테스트 코드 작성
mgim9316-a11y Apr 3, 2026
f1dbc56
feat: 테스트 코드 작성
mgim9316-a11y Apr 3, 2026
40664db
refator: error메시지 생성, depth1 추가
mgim9316-a11y Apr 3, 2026
9182842
chore: 불필요한 주석 삭제
mgim9316-a11y Apr 3, 2026
b0e11d8
chore: 불필요한 주석 삭제
mgim9316-a11y Apr 3, 2026
d50e676
chore: 포멧수정
mgim9316-a11y Apr 3, 2026
cdf6530
chore: 포멧수정
mgim9316-a11y Apr 3, 2026
817ecf1
feat: 보너스 번호 입력 기능 추가
mgim9316-a11y Apr 5, 2026
3b3bdd5
feat: 보너스 번호 입력 기능 추가
mgim9316-a11y Apr 5, 2026
49c6584
feat: 수동으로 로또 번호 입력 기능 추가
mgim9316-a11y Apr 5, 2026
5e3513c
feat: 수동으로 입력 받은 로또 번호 결과 검증 기능 추가
mgim9316-a11y Apr 5, 2026
df71a62
refator:숫자, 숫자 검증 기능 수정
mgim9316-a11y Apr 5, 2026
c31ec9e
feat: 로또 랜덤을 테스트하는 코드 작성
mgim9316-a11y Apr 5, 2026
7ec8baa
feat: 랜덤에 대한 테스트 코드 작성
mgim9316-a11y Apr 5, 2026
602d461
refactor: 전체 구매한 수량에서 수동 로또 구매한 개수를 제외한 수만큼 자동으로 출력
mgim9316-a11y Apr 5, 2026
2a62420
refactor: 값을 하드코딩하지 않는다.
mgim9316-a11y Apr 5, 2026
0e8d69b
refactor: 값을 하드코딩하지 않는다.
mgim9316-a11y Apr 5, 2026
8f6e8c1
refactor: 와일드카드 수정
mgim9316-a11y Apr 5, 2026
38307a5
refactor: 순서 주석 처리
mgim9316-a11y Apr 6, 2026
96aeb0d
fix : 잘못된 출력 형식 수정
mgim9316-a11y Apr 6, 2026
af13a01
feat: 수기 로또 번호, 보너스 번호를 입력 받는 파일 생성
mgim9316-a11y Apr 6, 2026
63e613b
refactor: 로또 번호 validate 호출
mgim9316-a11y Apr 6, 2026
a5fa889
feat: 검증 기능 추가
mgim9316-a11y Apr 6, 2026
3df98b3
docs: 추가 기능에 대한 리드미 작성
mgim9316-a11y Apr 6, 2026
4135454
refactor: 개행 추가
mgim9316-a11y Apr 6, 2026
d7fafe3
refactor: 중복되는 검증값 단일화
mgim9316-a11y Apr 7, 2026
1649b26
refactor: 매직넘버 수정 리터럴 추가
mgim9316-a11y Apr 7, 2026
22d4d73
refactor: 보너스 일치 여부 필드 추가
mgim9316-a11y Apr 7, 2026
5b1ddd0
refactor: 메서드 이름 변경
mgim9316-a11y Apr 7, 2026
cccc8b0
feat: 공통된 이름 validator 추가
mgim9316-a11y Apr 7, 2026
f43ffd4
refactor: 메서드 이름 변경
mgim9316-a11y Apr 7, 2026
ec91681
refactor: 매직넘버 추가
mgim9316-a11y Apr 7, 2026
98ee0ee
refactor: 출력 메서드 변경
mgim9316-a11y Apr 7, 2026
26cc5de
refactor: 테스트코드 수정
mgim9316-a11y Apr 7, 2026
6c39761
chore: 개행추가
mgim9316-a11y Apr 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
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
* **[1] 구입 금액 입력 및 시도 횟수 계산**
* 숫자 형식, 1,000원 단위, 0 이하 값 입력 시 `IllegalArgumentException` 발생 및 재시도.
* **[2] 수동 로또 개수 입력 및 번호 생성**
* 총 발행 횟수 초과 및 음수 입력 검증.
* 1~45 범위의 중복되지 않는 6개 숫자 입력 검증.
* **[3] 자동 로또 번호 생성**
* 총 횟수에서 수동 횟수를 제외한 만큼 `RandomLottoNumberGenerator`를 통해 번호 자동 생성 및 오름차순 정렬.
* **[4] 로또 발급 내역 출력**
* 수동 및 자동 발급 수량과 전체 로또 번호 리스트 출력.
* **[5] 당첨 번호 및 보너스 번호 입력**
* 우승 로또 번호 입력 검증.
* 보너스 번호의 범위(1~45) 및 당첨 번호와의 중복 여부 검증.
* **[6] 당첨 결과 계산 및 통계 출력**
* `LottoResult`와 `Rank` Enum을 활용하여 일치 개수 및 상금 계산.
* 당첨 내역 출력 및 수익률(소수점 셋째 자리 내림 처리) 계산 후 출력.
8 changes: 8 additions & 0 deletions src/main/java/Application.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import controller.Controller;

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

import domain.Lotto;
import domain.LottoMachine;
import domain.LottoTickets;
import domain.TrialNumber;
import domain.LottoResult;
import domain.RandomLottoNumberGenerator;
import domain.validator.BonusNumberValidator;
import domain.validator.ManualCountValidator;

import view.InputView;
import view.OutputView;

import java.util.List;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

public class Controller {

public void run() {
// [1] 구입 금액을 입력 받는다.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

주석을 사용해서 단계를 나눠주셨네요. 주석이 필요했던 이유가 무엇인가요?

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.

전체적인 구조와 위임 흐름을 리뷰어님께서 빠르게 파악하실 수 있도록 진입점인 Controller 에 주석을 추가했습니다!

Controller 는 클라이언트 요청을 받아 Domain 및 View 계층으로 위임하는 역할을 하므로, 이곳에 호출 순서를 명시하는 것이 전체적인 의도 전달에 유리할 것이라 판단했습니다!!

TrialNumber trialNumber = getTrialNumber();
// [2] 수기로 입력할 로또 갯수를 입력 받는다.
int manualTrialCount = getManualTrialCount(trialNumber.getTrialNumber());
// [3] 수기로 입력 받을 로또 번호를 입력 받는다.
LottoTickets manualTickets = getManualLottoTickets(manualTrialCount);
// [4] 자동으로 생성할 로또 번호를 계산한다.
int autoTrialCount = trialNumber.getTrialNumber() - manualTrialCount;
// [5] 자동 로또 번호를 생성한다.
LottoTickets autoTickets = issueLottoTickets(autoTrialCount, manualTrialCount);
// [6] 우승 로또 번호를 입력 받는다.
Lotto winningLotto = getWinningLotto();
// [7] 보너스 번호를 입력받는다.
int bonusNumber = getBonusNumber(winningLotto);
// [8] 로또 결과를 계산한다.
LottoResult statisticsResult = calculateAndPrintResults(autoTickets, winningLotto, bonusNumber, manualTickets);
// [9] 최종 결과를 출력한다.
OutputView.printWinningStatistics(statisticsResult, trialNumber.getPurchaseAmount());
}

private TrialNumber getTrialNumber() {
return retry(() -> {
OutputView.printInputPurchaseAmount();
return new TrialNumber(InputView.inputPurchaseMoney());
});
}

private int getManualTrialCount(int totalTrialCount) {
return retry(() -> {
OutputView.printManualTrialCount();
int manualCount = InputView.inputManualLottoNumberTrialCount();
ManualCountValidator.validate(totalTrialCount, manualCount);
return manualCount;
});
}

private LottoTickets getManualLottoTickets(int trialCount) {
if (trialCount == 0) {
return new LottoTickets(List.of());
}
return retry(() -> {
OutputView.printManualLottoTickets();
List<Lotto> manualLottos = IntStream.range(0, trialCount)
.mapToObj(i -> new Lotto(InputView.inputManualLottoNumber()))
.collect(Collectors.toList());
return new LottoTickets(manualLottos);
});
}

private LottoTickets issueLottoTickets(int trialCount, int manualCount) {
LottoMachine lottoMachine = new LottoMachine(new RandomLottoNumberGenerator());
List<Lotto> generatedLottos = lottoMachine.issue(trialCount);
LottoTickets lottoTickets = new LottoTickets(generatedLottos);

OutputView.printLottoNumber(lottoTickets, trialCount, manualCount);
return lottoTickets;
}

private Lotto getWinningLotto() {
return retry(() -> {
OutputView.printInputWinningNumber();
return new Lotto(InputView.inputWinningNumber());
});
}

private int getBonusNumber(Lotto winningLotto) {
return retry(() -> {
OutputView.printBonusNumber();
int bonusNumber = InputView.inputBonusNumber();
BonusNumberValidator.validate(winningLotto, bonusNumber);
return bonusNumber;
});
}

private LottoResult calculateAndPrintResults(LottoTickets autoTickets, Lotto winningLotto, int bonusNumber, LottoTickets manualTickets) {
return new LottoResult(autoTickets, winningLotto, bonusNumber, manualTickets);
}

private <T> T retry(Supplier<T> supplier) {
try {
return supplier.get();
} catch (IllegalArgumentException e) {
OutputView.printErrorMessage(e.getMessage());
return retry(supplier);
}
}
}
43 changes: 43 additions & 0 deletions src/main/java/domain/Lotto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package domain;

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

public class Lotto {
private static final int MIN_NUMBER = 1;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

정렬 단축키 한번만 눌러주세요~

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.

제가 윈도우 컴퓨터를 사용하고 있는데 단축키가 인텔리제에서 먹히는 게 아니라 다른 곳에서 먹힌다고 알고 있는데 최대한 빨리 개선해보겠습니다...

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

private final List<Integer> lottoNumber;

public Lotto(List<Integer> lottoNumber) {
validate(lottoNumber);
validateRange(lottoNumber);
this.lottoNumber = lottoNumber;
}

private void validate(List<Integer> lottoNumber) {
if (lottoNumber.size() != LOTTO_SIZE) {
throw new IllegalArgumentException("[ERROR] 로또 번호는 6개여야 합니다.");
}
if (new HashSet<>(lottoNumber).size() != LOTTO_SIZE) {
throw new IllegalArgumentException("[ERROR] 로또 번호에 중복된 숫자가 있습니다.");
}
}


private void validateRange(List<Integer> lottoNumber) {
if (lottoNumber.stream().anyMatch(this::isOutOfRange)) {
throw new IllegalArgumentException("[ERROR] 로또 번호는 1부터 45 사이의 숫자여야 합니다.");
}
}

private boolean isOutOfRange(int number) {
return number < MIN_NUMBER || number > MAX_NUMBER;
}

public List<Integer> getNumbers() {
return Collections.unmodifiableList(lottoNumber);
}
}
19 changes: 19 additions & 0 deletions src/main/java/domain/LottoMachine.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package domain;

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

public class LottoMachine {
private final LottoNumberGenerator generator;

public LottoMachine(LottoNumberGenerator generator) {
this.generator = generator;
}

public List<Lotto> issue(int trialCount) {
return IntStream.range(0, trialCount)
.mapToObj(i -> new Lotto(generator.generate()))
.collect(Collectors.toList());
}
}
7 changes: 7 additions & 0 deletions src/main/java/domain/LottoNumberGenerator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package domain;

import java.util.List;

public interface LottoNumberGenerator {
List<Integer> generate();
}
58 changes: 58 additions & 0 deletions src/main/java/domain/LottoResult.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package domain;

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

public class LottoResult {
private final Map<Rank, Integer> matchResults;
private final int bonusNumber;
private final int ZERO = 0;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

지금 코드로는 상수가 매 인스턴스마다 할당이 될 것 같아요!
만약 수정한다면 어떻게 수정할 수 있고, 지금과 어떤 차이가 있길래 그 방법을 사용하셨는 지도 함께 남겨주시면 좋을 것 같습니다~

+) 추가적으로 민욱님만의 상수의 의미에 대해 묻고 싶어요!
어떨 때를 매직넘버로 정의하고 상수로 바꿔야 하는 걸까요?

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.

static 을 사용하는 것입니다 !
static final 로 선언하면 클래스가 메모리에 로드될 때 한 번만 할당되어 모든 인스턴스가 공유합니다!
+
저는 코드 내부에서 숫자를 사용할 때, 그 값을 하드코딩을 하지 않는다라는 것을 알고 있었습니다.
근데 근본적으로 왜 값을 하드코딩을 하지 않을까에 대해서는 생각해보지 않았습니다.
이번 리뷰를 계기로 그 이유에 대해서 학습하고 생각해보았습니다 !

예를 들어 MIN_LOTTO_NUMBER = 1이라고 선언하면, 이 숫자가 로또 번호의 최솟값이라는 의미가 분명하게 전달됩니다. 반면 현재 제 코드에 작성된 ZERO = 0은 숫자 자체를 영단어로 바꿨을 뿐, 값에 대한 어떠한 문맥적 의미도 부여하지 못한다고 판단했습니다.

따라서 다른 사람이 코드를 읽을 때 값의 의도를 납득할 수 있도록, INITIAL_NUMBER = 0과 같이 명확한 의미를 부여하는 방식으로 상수를 명명하겠습니다 !

private final int ONE = 1;
private final int HUNDRED = 100;

public LottoResult(LottoTickets lottoTickets, Lotto winningLotto, int bonusNumber,LottoTickets manualLottoTickets) {
this.matchResults = new EnumMap<>(Rank.class);
this.bonusNumber = bonusNumber;
initResults();
calculate(lottoTickets.getLottoNumber(), winningLotto.getNumbers(), this.bonusNumber, manualLottoTickets.getLottoNumber());
}

private void initResults() {
for (Rank rank : Rank.values()) {
matchResults.put(rank, ZERO);
}
}

private void calculate(List<Lotto> lottoNumber, List<Integer> winningNumbers, int bonusNumber,List<Lotto> manullottoNumber) {
lottoNumber.forEach(lotto -> updateMatchResult(lotto, winningNumbers, bonusNumber));
manullottoNumber.forEach(lotto -> updateMatchResult(lotto, winningNumbers, bonusNumber));

}

private void updateMatchResult(Lotto lotto, List<Integer> winningNumbers, int bonusNumber) {
int matchCount = countMatch(lotto.getNumbers(), winningNumbers);
boolean matchBonus = lotto.getNumbers().contains(bonusNumber);
Rank rank = Rank.valueOf(matchCount, matchBonus);
matchResults.put(rank, matchResults.get(rank) + ONE);
}

private int countMatch(List<Integer> lottoNumber, List<Integer> winningNumbers) {
return (int) lottoNumber.stream()
.filter(winningNumbers::contains)
.count();
}

public double calculateProfitRate(int purchaseAmount) {
long totalPrize = ZERO;
for (Map.Entry<Rank, Integer> entry : matchResults.entrySet()) {
totalPrize += (long) entry.getKey().getPrizeMoney() * entry.getValue();
}
double rawProfitRate = (double) totalPrize / purchaseAmount;
return Math.floor(rawProfitRate * HUNDRED) / 100.0;
}

public int getRankCount(Rank rank) {
return matchResults.get(rank);
}
}
16 changes: 16 additions & 0 deletions src/main/java/domain/LottoTickets.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package domain;

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

public class LottoTickets {
private final List<Lotto> lottoTickets;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

일급 컬렉션으로 만들어주신 것 같아요! 다만, 민욱님이 생각하는 일급 컬렉션의 의미는 무엇이고 언제 일급 컬렉션으로 관리하나요?

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.

제가 생각하는 일급 컬렉션은 기본 컬렉션을 객체로 한 번 더 포장(Wrapping)하여 캡슐화한 클래스입니다.

List generatedLottos 자체는 단순히 객체를 담은 자바의 기본 컬렉션이라고 생각했습니다. 그렇기 때문에 LottoTickets 클래스로 한 번 더 포장하여 값을 스스로 보장하고 캡슐화된 안전한 일급 컬렉션을 만들었습니다 !

이렇게 만들어진 일급 컬렉션을 사용하면 Controller와 Domain 계층 간에 값을 전달할 때 안전하며, 원하는 정보의 출력, 저장, 가공 등 유지보수 측면에서 이점을 얻을 수 있다고 판단했습니다.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

정의를 잘 정리해주셔서 이해하기 쉬웠어요! 특히 "값을 스스로 보장하고 캡슐화된 안전한"이라는 표현이 아주 정석이군요 👍

그럼 그 정의를 가지고 지금 코드를 한번 봐보도록 할게요.
현재 Controller에서는 manualTickets와 autoTickets라는 두 개의 LottoTickets를 따로 들고 다니고, LottoResult 생성자도 둘을 각각 받아서 내부에서 두 번 순회하고 있어요.
그런데 도메인 관점에서 보면, 사용자가 산 로또는 결과적으로 "이번주에 내가 산 로또 묶음" 한 덩어리이라는 생각이 들어서요! 수동/자동은 어떻게 만들어졌는지에 대한 방법의 일종일 뿐, 당첨 계산이나 수익률 계산 입장에서는 구분할 이유가 있을까요?

그렇다면 질문을 드려볼게요. 민욱님이 정의하신 "값을 스스로 보장하고 캡슐화된" 일급 컬렉션이라면, 두 묶음을 하나로 합치는 책임은 어디에 있어야 자연스러울까요?


public LottoTickets(List<Lotto> lottoTickets) {
this.lottoTickets = lottoTickets;
}

public List<Lotto> getLottoNumber() {
return Collections.unmodifiableList(lottoTickets);
}
}
30 changes: 30 additions & 0 deletions src/main/java/domain/RandomLottoNumberGenerator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package domain;

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

public class RandomLottoNumberGenerator implements LottoNumberGenerator {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

개행이 두개 들어가있네요~

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.

넵 확인했습니다!
개행 단축키를 빠르게 적용할 수 있도록 하겠습니다 !


private static final int MIN_LOTTO_NUMBER = 1;
private static final int MAX_LOTTO_NUMBER = 45;
private static final int LOTTO_NUMBER_COUNT = 6;

private final List<Integer> lottoPool;

public RandomLottoNumberGenerator() {
this.lottoPool = new ArrayList<>();
for (int i = MIN_LOTTO_NUMBER; i <= MAX_LOTTO_NUMBER; i++) {
lottoPool.add(i);
}
}

@Override
public List<Integer> generate() {
Collections.shuffle(lottoPool);
List<Integer> selectedNumbers = new ArrayList<>(lottoPool.subList(0, LOTTO_NUMBER_COUNT));
Collections.sort(selectedNumbers);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

정렬은 도메인의 책임일까요? 민욱님은 어떻게 생각하시나요?

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.

정렬은 도메인의 책임입니다 !
도메인의 책임이라고 생각하는 이유는 다음과 같습니다 !

  1. 정렬은 단순한 출력 형태의 변형이 아니라, 도메인 객체가 올바른 상태를 보장하기 위해 지켜야 하는 비즈니스 로직(규칙)이기 때문입니다.
  2. 만약 입력을 담당하는 InputView 에서 정렬을 수행하게 되면, View 계층이 비즈니스 로직을 처리하게 되므로 계층 간의 역할이 혼재되어 단일 책임 원칙에 위배됩니다.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

(진짜 단순히 궁금해서 더 여쭤봅니다 ㅎ.ㅎ 민욱님 의견을 펼쳐주세요)

민욱님의 답변을 토대로 생각해보면 정렬 = 비지니스 로직 이라고 말씀해주신 것 같아요!

정렬은 (단순한 출력 형태의 변형이 아니라, )도메인 객체가 올바른 상태를 보장하기 위해...

정렬이 되지 않으면 도메인 객체가 구체적으로 어떤 올바르지 않은 상태에 놓이게 되나요? 🤔
예를 들어 정렬되지 않은 상태에서 기능적으로 문제가 생기는 시나리오가 무엇이 있는지 민욱님의 좀 더 구체적인 생각을 들어보고 싶어요!

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.

제 의견을 리뷰어님에게 전달할 수 있게 되어 영광입니다....... 😆

이전에 제가 "정렬은 데이터를 이동시키기 때문에 비즈니스 로직이다"라고 자신 있게 말씀드렸던 부분은 다소 단편적인 주장이었습니다. 다시 깊게 생각해 본 결과, 정렬의 책임에는 정해진 정답이 없으며 요구사항에 따라 도메인에 있을 수도 있고 View 계층에 있을 수도 있다고 제 생각을 정정하고 싶습니다!

  1. 도메인에 위임해야 하는 상황
    예를 들어 6장의 로또를 오름차순으로 정렬한 뒤, 같은 자리의 숫자끼리 비교하여 평균값을 계산하는 요구사항이 있다면 어떨까요? 이때의 정렬은 비즈니스 모델의 핵심적인 역할을 수행하므로 당연히 도메인에 있어야 합니다!

  2. View에서 처리해도 무방한 상황
    반면, 단순히 사용자에게 정돈된 형태로 보여주기 위한 목적이라면, View 계층에서 로또 번호를 출력할 때 정렬을 수행해도 무방합니다!

이번 미션에 한정해서 생각해 보면, 로또 번호를 정렬하는 작업이 도메인의 핵심 계산 로직에는 아무런 영향을 주지 않습니다! 따라서 단순 출력을 위한 정렬이라면 View 계층에서 처리해도 문제가 되지 않는다고 판단했습니다.

"정렬은 무조건 도메인의 역할이다!"라는 이전의 제 주장을 철회하고, 상황과 요구사항의 목적에 맞게 책임을 분리해야 한다는 점을 깨닫게 되었습니다...

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

단순히 상황과 요구사항에 따라 책임의 위치가 달라질 수 있다고 말씀해주신 게 아니라,
구체적인 상황을 예시로 들어 합당한 근거와 함께 설명해주시니 훨씬 설득력이 높았어요 👍👍👍

생각은 언제든 정정될 수 있는 것이니~, 다음에도 주저하지 말고 민욱님의 의견을 피력해주세요!! ㅎㅎ
(그리고 그 방식이 더욱 기억에 오래 남을 거에요)

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

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public enum Rank {
NONE(0, 0),
THREE(3, 5000),
FOUR(4, 50000),
FIVE(5, 1500000),
FIVE_BONUS(5, 30000000),
SIX(6, 2000000000);

private final int matchCount;
private final int prizeMoney;

Rank(int matchCount, int prizeMoney) {
this.matchCount = matchCount;
this.prizeMoney = prizeMoney;
}

public static Rank valueOf(int matchCount, boolean matchBonus) {
if (matchCount == 5 && matchBonus) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

rank != FIVE_BONUS처럼 특정 값을 이름으로 필터링하고 있는데, 이렇게 하면 enum의 정체성을 외부 로직이 판단하게 되는 건 아닐까요? enum 스스로 "나는 보너스다"를 표현할 수 있는 방법도 고민해 보시면 좋을 것 같아요!

추가로, valueOf라는 이름은 Enum.valueOf(String)로 이미 존재하는 메서드인데, 시그니처가 다르더라도 혼동을 줄 수 있을 것 같아요!

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.

해당 방식대로라면 enum의 정체성이 외부 로직에 의해 판단될 수 있겠네요 !
(enum의 정체성은 외부에 의해서 판단되면 안되는군요 ! 제가 그것을 몰랐습니다!) 그렇다면 이것을 개선할 수 있는 방법은 enum '보너스 일치 여부'를 나타내는 필드를 추가하면 되겠군요 !

객체의 이름이 아닌 객체가 가진 조건을 만족하는 상태(값)를 기준으로 로직이 동작하게 설계를 하도록 하겠습니다 !

시그니처가 다르더라도 혼동을 주는 것에 동의를 합니다 ! 메서드 이름을 다시 한 번 고민해야겠습니다 !

return FIVE_BONUS;
}
return Arrays.stream(values())
.filter(rank -> rank.matchCount == matchCount && rank != FIVE_BONUS)
.findFirst()
.orElse(NONE);
}

public static List<Rank> getWinningRanks() {
return Arrays.stream(values())
.filter(rank -> rank != NONE)
.collect(Collectors.toList());
}

public String getMessage() {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

getMessage()가 사용자에게 보여줄 출력 문자열을 직접 만들고 있는데, 이 역할이 도메인 객체인 enum에 있는 게 적절할까요? 이 메시지가 바뀌어야 할 때 누가 수정되어야 하는지 생각해 보시면 좋을 것 같아요!

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.

getMessage() 의 호출에 대해서 해당 메서드가 어디에 있으면 좋을지 고민이 되었는데 메시지가 바뀌었을 때, 누가 수정이 되어야하는지에 대해서 생각해보니 getMessage()가 어디에 위치되어야 하는지에 대해서 정할 수 있었습니다 !

메서드의 역할에 대해서 애매함을 느낄 때, 요구사항이 변경되면 어디에서 변경을 해줘야할 지에 대해서 고민함으로써 문제를 해결할 수 있겠네요 !!

if (this == FIVE_BONUS) {
return matchCount + "개 일치, 보너스 볼 일치 (" + prizeMoney + "원)- ";
}
return matchCount + "개 일치 (" + prizeMoney + "원)- ";
}

public int getPrizeMoney() {
return prizeMoney;
}

public int getMatchCount() {
return matchCount;
}
}
Loading