Conversation
chemistryx
left a comment
There was a problem hiding this comment.
민욱님 안녕하세요~! 미션 진행하시느라 고생하셨어요 😀 전반적으로 계층 분리와 도메인 설계 방향이 잘 잡혀있는 것 같은데,
고민하신 내용과 질문 사항에 대해 답변을 드려보도록 할게요. 추가로 코드 관련 수정 요구사항은 코멘트 달아두었으니 확인해주세요!
질문에 대한 답변
LottoTickets생성 책임 분리
고민하신 대로 LottoMachine을 통한 분리가 더 적절해 보여요. LottoTickets의 본질은 생성된 티켓들을 저장하고 관리하는 것인데, 현재는 생성 책임까지 가지고 있어 SRP를 위반하고 있어요.
LottoMachine machine = new LottoMachine(generator);
LottoTickets tickets = machine.issue(trialNumber.getTrialNumber());이런 식으로 분리하면 LottoTickets는 완성된 리스트만 주입받아 관리에만 집중할 수 있고, 테스트도 훨씬 쉬워질 것 같아요.
제가 생각했을 때 역할 분리의 기준은 변경 이유가 같은가인 것 같아요. 생성 로직이 바뀌는 이유(e.g., 로또 생성 방식 변경)와 관리 로직이 바뀌는 이유(e.g., 당첨 확인 or 티켓 조회 방식 변경)는 다르므로 분리하는 것이 맞다고 생각해요.
기능 분리와 일급 컬렉션
View가 입력 형식에 대한 검증(숫자 or 쉼표 등)을 수행하고 파싱하는 것까지는 적절하게 잘 구성되어 있어요. 다만 비즈니스 규칙은(범위, 개수, 중복)은 도메인 객체가 담당해야 하는데, 현재 당첨 번호(winningNumber)가 List<Integer> 형식으로 흘러가고 있는 문제가 있어요. 관련해서는 별도 코멘트로 남겨두었으니 참고해주세요~
Enum 활용
Rank enum의 경우 적절하게 설계를 해주신 것 같아요. valueOfMatchCount()로 도메인 로직까지 내부에 캡슐화한 것 좋네요 👍
추가 요구사항
- 현재 유효하지 않은 값 입력 시 예외를 던지고 프로그램이 종료되는데요, 만약 종료되지 않고 재입력을 받을 수 있도록 처리해본다면 사용자 경험이 더 좋아질 것 같아요!
- 아직 테스트 코드가 작성되어 있지 않은 것 같은데요, 도메인 로직 위주로 단위 테스트를 작성해보시면 좋을 것 같아요.
관련해서 이해가 잘 안되시는 부분이나 추가적인 질문이 있으시면 언제든지 편하게 말씀해주세요~
| // [5] 당첨번호와 로또 번호 비교해서 결과 탐색하기 | ||
| CalculateLottoNumber statisticsResult = new CalculateLottoNumber(lottoNumber, winningNumbers); | ||
| // [6] 결과 출력하기 | ||
| int PurchaseAmount = trialNumber.getPurchaseAmount(); |
There was a problem hiding this comment.
| int PurchaseAmount = trialNumber.getPurchaseAmount(); | |
| int purchaseAmount = trialNumber.getPurchaseAmount(); |
변수의 경우 camelCase로 적용해주시는게 좋을 것 같아요! 다른 곳에서는 camelCase로 적용해주신 걸 보면 이 부분만 깜빡하신 것 같네요 ㅎㅎ
혹시 컨벤션 관련해서 더 궁금하신 점이 있다면 이 글 참고해보시면 좋을 것 같아요~
There was a problem hiding this comment.
지금 클래스명 CalculateLottoNumber는 동사 형태인데요, 명사로 수정이 필요해보여요.
맥락 상 당첨 통계 결과를 담고 있는 클래스인 것 같은데, LottoResult 또는 LottoStatistics는 어떨까요?
There was a problem hiding this comment.
클래스명을 동사로 하는 것보다는 명사로 하는군요!
하나 또 배워갑니다!!
| private void validate(List<Integer> lottoNumber) { | ||
| if (lottoNumber.size() != 6) { | ||
| throw new IllegalArgumentException("[ERROR] 로또 번호는 6개여야 합니다."); | ||
| } | ||
| if (new HashSet<>(lottoNumber).size() != 6) { | ||
| throw new IllegalArgumentException("[ERROR] 로또 번호에 중복된 숫자가 있습니다."); | ||
| } | ||
| } |
There was a problem hiding this comment.
현재 번호 개수와 중복 여부만 검증하고 번호 범위(1 ~ 45)는 검증하지 않고 있어요.
RandomLottoNumberGenerator가 생성 시점에서 범위를 보장하기는 하지만, Lotto는 LottoNumberGenerator 인터페이스를 통해 생성되기 때문에 어떤 구현체가 올지 알 수 없어요.
만약 다른 구현체에서 범위 밖 숫자를 넣는다면 현재 위 검증 로직에서는 정상적으로 통과되는 문제가 있을 것 같아요.
따라서 도메인 객체는 입력 출처와는 상관 없이 자신을 스스로 검증하는 방식이 더 적절해 보이는데, 어떻게 생각하시나요?
만약 그렇게 수정한다면, Lotto 내부에 범위 검증을 추가하는 방식으로 접근하면 좋을 것 같아요~
There was a problem hiding this comment.
사용자기 입력한 winningNumber를 객체화를 한다면 값이 검증이 안 되기 때문에 해당 validate가 필요하다고 생각합니다! 리팩토링 하겠습니다!
| OutputView.printLottoNumber(lottoNumber,trialCount); | ||
| // [4] 지난주 당첨 번호 입력 받기 | ||
| OutputView.printInputWinningNumber(); | ||
| List<Integer> winningNumbers = InputView.inputWinningNumber(); |
There was a problem hiding this comment.
지난주 당첨번호에 대해서, 원시값을 포장한다는 생각을 못했었습니다...
| this.purchaseAmount = purchaseAmount; | ||
| validateAmount(purchaseAmount); |
There was a problem hiding this comment.
지금 인자로 입력된 값을 검증 전에 필드에 먼저 할당하고 있어요.
이후 검증 단계에서 어차피 예외가 던져져서 상관 없긴 하지만, 검증을 먼저 수행하고 값을 할당하는 방식으로 가는게 좀 더 의도가 명확히 드러날 것 같습니다!
| public int getTrialNumber() { | ||
| return trialCount; | ||
| }public int getPurchaseAmount() { | ||
| return purchaseAmount; | ||
| } |
There was a problem hiding this comment.
| public int getTrialNumber() { | |
| return trialCount; | |
| }public int getPurchaseAmount() { | |
| return purchaseAmount; | |
| } | |
| public int getTrialNumber() { | |
| return trialCount; | |
| } | |
| public int getPurchaseAmount() { | |
| return purchaseAmount; | |
| } |
요거 개행 추가해주세요!
IntelliJ 사용중이사라면 Cmd + Option + L 단축키로 자동으로 포맷팅 할 수 있어요! (윈도우는 Ctrl + Alt + L이라고 합니다)
++ 이 파일 말고도 다른 파일들도 포맷팅이 필요한 부분이 좀 보이는데, 전체적으로 한번 수행해주시면 좋을 것 같아요~
| LottoTickets lottoNumber = new LottoTickets(trialNumber.getTrialNumber(), generator); | ||
| int trialCount = trialNumber.getTrialNumber(); | ||
| OutputView.printLottoNumber(lottoNumber,trialCount); |
There was a problem hiding this comment.
trialNumber.getTrialNumber()가 두 번 호출되고 있는데, 하나로 통일해서 사용할 수 있지 않을까요?
There was a problem hiding this comment.
과제 제출에 급급해서 사소한 부분들을 많이 놓쳤네요...
언급해주셔서 감사합니다!...
| for (Integer number : lottoNumber) { | ||
| if (winningNumbers.contains(number)) { | ||
| count++; | ||
| } | ||
| } |
There was a problem hiding this comment.
요거 depth가 1을 넘네요 😢
어떻게 수정해볼 수 있을까요?
There was a problem hiding this comment.
for-if 문에 편해져서 해당 코드를 작성했습니다.
자바에 Stream을 적극적으로 활용해보겠습니다!
src/main/java/view/OutputView.java
Outdated
| for (Rank rank : Rank.values()) { | ||
| if (rank == Rank.NONE) { | ||
| continue; // 미당첨 내역은 출력에서 제외 | ||
| } | ||
| System.out.printf("%d개 일치 (%d원)- %d개\n", | ||
| rank.getMatchCount(), | ||
| rank.getPrizeMoney(), | ||
| result.getRankCount(rank)); | ||
| } |
There was a problem hiding this comment.
요 친구도 depth가 1을 넘기는데, 출력 대상 Rank 목록 자체를 사전에 분리해보는 것은 어떨까요?
e.g.,
for (Rank rank : Rank.winningRanks()) {
System.out.printf(...);
}
chemistryx
left a comment
There was a problem hiding this comment.
@hapdaypy 빠르게 반영해주셨네요!
개선 포인트 몇개 관련해서 코멘트 달아두었는데 확인 부탁드려요~
아 그리고 LottoResult와 Rank.valueOfMatchCount()에 대한 테스트도 추가되면 좋을 것 같아요!
LottoResult는 당첨 계산과 수익률 계산을 담당하고, Rank.valueOfMatchCount()는 매치 수에 따라 올바른 Rank를 반환하는 핵심 로직이라고 생각이 들어서 이 부분에 대한 테스트도 추가되면 좋을 것 같습니다~
| @@ -0,0 +1,51 @@ | |||
| package controller; | |||
|
|
|||
| import domain.*; | |||
There was a problem hiding this comment.
위와 같은 형태로 구성된 import문을 와일드카드 import라고 하는데요, 관련해서 명준님 PR에 남겨둔 코멘트가 있어 공유해드립니다!
next-step/java-racingcar-simple-playground#181 (comment)
| for (Integer number : lottoNumber) { | ||
| if (number < MIN_NUMBER || number > MAX_NUMBER) { | ||
| throw new IllegalArgumentException("[ERROR] 로또 번호는 1부터 45 사이의 숫자여야 합니다."); | ||
| } | ||
| } |
There was a problem hiding this comment.
요기 값 검증하는 로직 추가하면서 depth가 2가 되어버렸네요 😢 어떻게 수정할 수 있을까요?
| @DisplayName("요청한 시도 횟수만큼 로또를 발행한다.") | ||
| @Test | ||
| void issueLottos() { | ||
|
|
| try { | ||
| return supplier.get(); | ||
| } catch (IllegalArgumentException e) { | ||
| System.out.println(e.getMessage()); |
There was a problem hiding this comment.
오류 메시지에 대한 출력을 Controller가 담당하고 있어요. OutputView가 출력을 담당하도록 하는 것이 더 명확해보이는데 어떻게 생각하시나요?
| while (true) { | ||
| try { | ||
| return supplier.get(); | ||
| } catch (IllegalArgumentException e) { | ||
| System.out.println(e.getMessage()); | ||
| } | ||
| } |
There was a problem hiding this comment.
현재 while(true)안에 try catch 문이 있어서 depth가 2가 되었어요. 한번 개선해볼 수 있을까요?
그리고 재시도 로직에 Supplier를 사용해주셨는데요, 선택하신 이유가 궁금합니다!
| public void run() { | ||
| // [1], [2] 구입 금액 입력 및 시도 횟수 생성 (예외 발생 시 재입력) | ||
| TrialNumber trialNumber = retry(() -> { | ||
| OutputView.printInputPurchaseAmount(); | ||
| int amount = InputView.inputPurchaseMoney(); | ||
| return new TrialNumber(amount); | ||
| }); | ||
|
|
||
| int trialCount = trialNumber.getTrialNumber(); | ||
| // [3] 로또 머신을 통한 로또 발행 및 티켓 관리 객체 초기화 | ||
| LottoMachine lottoMachine = new LottoMachine(new RandomLottoNumberGenerator()); | ||
| List<Lotto> generatedLottos = lottoMachine.issue(trialCount); | ||
| LottoTickets lottoTickets = new LottoTickets(generatedLottos); | ||
|
|
||
| OutputView.printLottoNumber(lottoTickets, trialCount); | ||
|
|
||
| // [4] 지난주 당첨 번호 입력 받기 및 Lotto 객체로 포장 (타입 오류 수정, 재입력 적용) | ||
| Lotto winningLotto = retry(() -> { | ||
| OutputView.printInputWinningNumber(); | ||
| List<Integer> inputWinningNumbers = InputView.inputWinningNumber(); | ||
| return new Lotto(inputWinningNumbers); | ||
| }); | ||
|
|
||
| // [5] 당첨 번호와 로또 번호 비교해서 결과 탐색하기 | ||
| LottoResult statisticsResult = new LottoResult(lottoTickets, winningLotto); | ||
|
|
||
| // [6] 결과 출력하기 | ||
| int purchaseAmount = trialNumber.getPurchaseAmount(); | ||
| OutputView.printWinningStatistics(statisticsResult, purchaseAmount); | ||
| } |
There was a problem hiding this comment.
run()메소드가 살짝 길어보이는 감이 있는데요, 각 단계를 각각의 메소드로 분리해보면 전체 흐름이 한눈에 보이도록 정리할 수 있을 것 같아요!

🙋♂️ 인사
안녕하세요, 그리디 4기 백엔드 김민욱입니다.
이번 미션을 통해 MVC 패턴, 단일 책임 원칙(SRP), Enum, 일급 컬렉션의 적용과 각 클래스의 책임 분리에 집중하여 구현했습니다.
환절기에 감기 조심하세요!
🚗 단계별 설명
1단계 - <로또 자동 구매>
구입 금액을 입력받아 형식을 검증하고, 해당 금액에 비례하는 수량만큼 로또 번호를 자동 생성합니다.
2단계 - <로또 당첨>
지난주 당첨 번호를 입력받고, 발행된 로또 번호와 대조하여 당첨 통계(일치 개수 및 상금)와 총 수익률을 계산하여 출력합니다.
🤔 고민한 내용
여러 개의 Lotto 객체를 일급 컬렉션(LottoTickets)으로 묶어 관리할 때, 객체 생성의 책임을 어디에 둘 것인가를 고민했습니다.
[1번 방안] 컨트롤러에서 반복문을 통해 Lotto 객체를 생성하여 컬렉션에 주입하는 방식.
[2번 방안] LottoTickets가 생성 시점에 발행 횟수와 생성기(Generator)를 전달받아 내부에서 스스로 Lotto 리스트를 생성하는 방식.
[2번 방안]은 컨트롤러가 도메인의 세부 생성 로직에 관여하는 것을 막고, 객체 스스로 상태를 초기화하는 'Tell, Don't Ask(묻지 말고 시켜라)' 원칙을 준수합니다. 하지만 일급 컬렉션인 LottoTickets에 '생성'과 '데이터 관리'라는 두 가지 책임이 혼재되어 단일 책임 원칙(SRP)을 위반하는 것은 아닌지 의문이 들었습니다.
LottoTickets의 본질적인 목적은 생성된 로또 번호들을 저장하고 반환하는 것이기 때문입니다.
[대안 및 리팩토링 방향]
이를 해결하기 위해 LottoMachine(또는 생성 팩토리)이라는 발행 전담 객체를 분리하는 방안을 고려했습니다. LottoMachine이 List 생성을 전담하고, LottoTickets는 완성된 리스트를 주입받아 관리(저장, 출력, 당첨 확인)만 담당하도록 역할을 분리하는 것입니다.
리뷰어님께: 위 대안처럼 생성 책임을 가진 객체와 일급 컬렉션을 완전히 분리하여 SRP를 극대화하는 것이 객체지향 설계 관점에서 적절한지, 아니면 일급 컬렉션 내부에서 팩토리 역할을 겸하는 수준이 적당한지(과도한 설계인지) 의견이 궁금합니다. 객체의 역할을 얼만큼 세밀하게 나누어야 하는지 기준을 잡고 싶습니다.
❓ 질문사항
기능 분리와 일급 컬렉션: 입력된 문자열의 파싱 책임(View)과 도메인 데이터의 일급 컬렉션화(Domain) 사이의 역할 분리가 적절하게 이루어졌는지 피드백 부탁드립니다.
Enum 활용의 적정성: 당첨 기준(일치 횟수)과 보상(당첨금)이라는 밀접하게 연관된 상수들을 하나로 묶기 위해 Enum을 도입했습니다. 상태와 행위를 하나의 타입으로 캡슐화하여 타입 안전성을 확보한다는 Enum의 설계 목적에 부합하게 사용되었는지 검증받고 싶습니다.
⭐ 향후 개선사항