-
Notifications
You must be signed in to change notification settings - Fork 92
[그리디] 김하늘 로또 미션 3,4,5 단계 제출합니다. #148
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: kimsky247-coder
Are you sure you want to change the base?
[그리디] 김하늘 로또 미션 3,4,5 단계 제출합니다. #148
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
안녕하세요, 하늘님! 로또 미션 리뷰어 남해윤입니다~
미션으로 만나 뵙게 되어 반갑습니다! 저도 잘 부탁드립니다 😊
코드를 너무 잘 작성해주셔서 크게 리뷰할 점이 없네요🫢 이번 리뷰에서는 하늘님 질문에대한 답변과 약간의 오류에대한 코멘트 위주로 작성했습니다.
🤔 질문에 대한 답변
- 현재 LottoController가 View와 다수의 Model 클래스들을 직접 생성하고 호출하고 있어 의존성이 높은 것 같다고 느꼈습니다. 지금은 구조가 간단하지만, 프로젝트가 더 커진다면 이런 의존성들이 문제가 될 수 있다고 생각합니다. 어떻게 의존성을 관리하는 것이 좋을지 궁금합니다!
말씀하신 것처럼 지금은 구조가 단순하니 큰 문제가 없어 보일 수 있지만, 프로젝트가 커지면 Controller의 책임이 점점 무거워질 수 있겠죠.
저는 Controller가 Model과 View를 의존하는 건 MVC 패턴에서 자연스러운 흐름이라고 생각합니다. 그래서 의존 자체가 문제라기보다는, Controller가 얼마나 많은 책임을 가지느냐가 핵심 포인트일 수 있다는 생각이 들어요. 코드에는 정답이 없으니 같이 얘기 나누면서 더 좋은 구조를 고민해보면 좋겠습니다 🙂 저도 몇 가지 질문을 드려볼게요.
Q1) Model과 View는 어딘가에서 반드시 호출되어야 합니다. 하늘님은 그 책임이 어디에 있어야 한다고 생각하시나요?
Q2) 의존성이 많아졌을 때 구체적으로 어떤 상황에서 문제가 생길 수 있다고 보시나요?
- Can’t automatically merge. Don’t worry, you can still create the pull request.
충돌 관련 오류는 해결하셨나요?
⚙️ 실행 오류
다양한 예외케이스들을 잘 만들어 주셨더라구요~ 대부분의 경우 잘 작동했지만 약간의 실행 오류가 있어서 가져와봤습니다.
1. [수동 구매 번호 입력] 중복된 숫자를 입력한 경우


에러메세지는 당첨 번호는 중복이 없어야 합니다! 라는 식으로 바뀌어야할 것 같아요.
몇가지 더 테스트해보니 다른 경우들에는 중복 관련 에러가 뜨는데 위의 입력 경우에 잘못된 에러메세지가 뜨더라고요. 확인해주세요!
2. [로또 구입] 구입 금액으로 살 수 있는 로또 개수 < 수동으로 구매할 로또 개수 인 경우

구입 금액이 2000원이면 구입 가능한 로또 개수가 2개여야 하는데, 수동으로 구매할 로또의 수가 그 이상이어도 에러가 뜨지 않고, 그 개수만큼 수동 구매 번호 입력도 가능하네요!
해당 에러도 수정이 필요할 것 같습니다.
private long calculateTotalPrize(Map<Rank, Integer> matchResult) { | ||
long totalPrize = 0; | ||
for (Rank rank : matchResult.keySet()) { | ||
int count = matchResult.get(rank); | ||
totalPrize += (long) rank.getPrize() * count; | ||
} | ||
return totalPrize; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
해당 로직을 controller에 둔 이유가 궁금합니다!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
처음에는 컨트롤러가 모델로부터 받은 결과 데이터를 최종 출력을 위해 가공하는 역할의 일부라고 생각해서 calculateTotalPrize
메서드를 컨트롤러에 두었습니다. 하지만 다시 생각해보니, 모델이 책임을 갖는 것이 맞을 것 같습니다. Lottos
클래스로 로직을 옮기도록 수정했습니다.
} | ||
} | ||
|
||
private Lottos purchseLottos(int purchaseAmount, int manualCount) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
사소한 오타가 있네요!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
제가 이 부분을 놓쳤습니다. 수정하였습니다!
src/main/java/model/Lotto.java
Outdated
validateSize(sortedNumbers); | ||
validateDuplicates(sortedNumbers); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
지난 스터디에서 말씀드린대로 자동으로 정렬과 중복제거를 해주는 자료구조가 있습니다!
WinningNumbers에는 private final Set<LottoNumber> numbers;
이런식으로 선언을 하셨는데 해당 클래스에서는 List 형태로 final 없이 선언한 이유가 있나요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lotto
클래스의 내부 자료구조를 기존의 List에서 TreeSet
으로 변경했습니다. sorted()
를 호출하지 않아 코드가 간결해진 것 같습니다. final은 제가 놓쳤습니다. 수정하였습니다!
private void showWinningResults(Lottos lottos, WinningNumbers winningNumbers) { | ||
Map<Rank, Integer> matchResult = lottos.calculateResult(winningNumbers); | ||
|
||
long totalPrize = calculateTotalPrize(matchResult); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
돈의 단위를 long으로 설정하신 부분 정말 좋은 선택인 것 같아요 👍
로또 상금의 경우 int로 처리하면 약 21억을 넘어갈 수 있기 때문에, 저도 long을 사용하는 게 훨씬 안전하다고 생각합니다!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이미 테스트 코드를 잘 작성해 주셨지만, given-when-then 구조를 참고하셔도 좋을 것 같아요~
given when then
테스트 코드를 작성하는데 많이 사용되는 패턴이에요. 테스트 코드 짤 때 도움이 될 것 같아 첨부했습니다!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
given-when-then 구조로 테스트를 작성할 때, 테스트의 의도가 명확해지고, 가독성이 좋아지는 것 같습니다. 다음부터 테스트 코드를 작성할 때, give-when-then구조를 잡고 테스트를 작성해보겠습니다
질문에 대한 답변
저도 해윤님과 같이 책임이 컨트롤러에 있어야 한다고 생각합니다.
유연성이 저하되는 문제가 가장 크다고 생각합니다. 만약 로또 생성 규칙이 복잡해져
터미널로 할 수 있는 여러가지 방법을 시도해 보았는데, 해결되지 않아서 머지 전에 깃허브에서 수동으로 해결해야할 것 같습니다😢 오류 처리 로직
확인해보니
금액 입력 후 총 구매 가능 개수를 파악하고, 수동 구매 개수를 입력받았을 때 바로 검증하는 로직이 누락되었습니다. |
질문1.
좋은 고민이네요! 컨트롤러를 분리하는 방법은 대표적으로 도메인별과 기능별로 구분하는 방법이 있는 것 같아요. 지금은 컨트롤러가 하나뿐이지만, 현재 하늘님이 작성하신 방식은 도메인 단위로 컨트롤러를 구분한 형태로 보입니다. 현재 도메인이 '로또' 하나뿐이기 때문에 LottoController로 설계하신 게 아주 자연스러워요. (물론 '주문', '로또'처럼 도메인을 더 세분화할 수도 있겠지만, 현재 주문 단계의 책임이 "로또 구매" 정도로 한정적이어서 이 시점에서는 분리하기엔 너무 작아 보이기도 합니다.) 반면 기능별로 구현한다면 말씀하신 것처럼 PurchaseController, ResultController 등으로 나눌 수도 있겠죠. 보통 실무에서는 도메인별로 컨트롤러를 구별하는 방식을 더 자주 사용하는 것 같은데, 혹시 그 장점이 무엇일지 함께 고민해 보면 좋을 것 같습니다. 두 방식을 현재 로또 미션에 적용했을 때 어떤 장단점이 있을지 생각해 본다면, 하늘님의 질문에 대한 해답을 스스로 찾으실 수 있을 거예요! 😃 질문2.
정확한 지적입니다! MVC 패턴은 구조가 단순하지만, 프로젝트 규모가 커질수록 컨트롤러가 너무 많은 역할을 맡게 되면서 View와 Model에 대한 결합도가 높아지는 문제가 실제로 자주 발생합니다. 이 문제를 해결하기 위해, Controller가 new Lotto()처럼 구체적인 모델 객체를 직접 생성하지 않도록 말씀하신 LottoFactory 같은 중간 생성 책임 클래스를 두는 것은 매우 좋은 접근 방식입니다. 이렇게 하면 Controller는 구체적인 구현에 의존하지 않고, 생성 로직이 변경되더라도 유연하게 대응할 수 있겠죠. 추가로, 의존성 주입(DI)이나 서비스 레이어 분리 같은 방법도 있어요. 이런 방법을 적용하면 객체 간 결합도를 더 낮추고 테스트나 확장 측면에서도 유리해질 수 있으니 찾아보시면 도움이 될 것 같습니다! 👍 |
src/main/java/model/Lotto.java
Outdated
|
||
public Lotto(List<Integer> numbers) { | ||
validateSize(numbers); | ||
validateDuplicates(numbers); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Set 자료구조 형태로 로또 리스트를 만드셨는데, 숫자 중복 검사 로직을 유지한 이유는 무엇인가요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
입력이 잘못되었을 때, 명확한 원인을 알려주는 것이 좋다고 생각해서 유지했었습니다. 하지만 Set의 특성을 활용하여 코드를 간결하게 만드는 것이 더 좋을 것 같아 크기 검사 로직만 남겨 "로또 번호는 중복 없이 6개여야 합니다"라는 메세지를 사용하도록 수정하였습니다!
src/main/java/model/Lotto.java
Outdated
private final Set<LottoNumber> numbers; | ||
|
||
public Lotto(List<Integer> numbers) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lotto
번호들은 Set<LottoNumber>
로 저장하는데, 생성자는 매개변수를 List<Integer>
형태로 받고 있네요.
입력 단계에서 순서나 중복 검증을 위해 List
를 선택하신 건가요?
48번째 줄의 convertToLottoNumbers
메서드의 매개변수의 형식 또한 마찬가지로 List<Integer>
인 이유가 있나요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
처음 설계할 때는 말씀하신 대로 중복 오류 검증 로직을 위해 List
로 받도록 설계했습니다. convertToLottoNumbers
의 매개 변수가 List<Integer>
인 이유도 List
형식으로 받은 후 Set<LottoNumber>
로 바꾸는 역할을 하기 떄문이었습니다. 현재는 중복 검증 로직을 삭제했지만, 외부에서 들어온 데이터를 자신의 규칙에 맞게 검증하고, 형태를 변환하는 것이 모델의 역할이라고 생각하여 외부의 데이터를 그대로 받을 수 있는 List
를 사용하였습니다
src/main/java/model/Lotto.java
Outdated
public boolean bonusMatch(LottoNumber bonusBall) { | ||
return numbers.contains(bonusBall); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
True False를 반환하는 boolean형의 메서드는 네이밍을 is + 명사
이런식으로 많이 해요. 메서드명을 조금 더 직관적으로 수정해보는 것은 어떨까요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
말씀해주신 대로 무엇을 반환하는 메서드인지 잘 드러나지 않는 것 같습니다. 메서드 명을 IsBonusBallMatch
로 바꾸어 더 직관적으로 보이게 수정하였습니다
List<Integer> lottoNumbers = Arrays.stream(numbers.split(",")) | ||
.map(String::trim) | ||
.map(Integer::parseInt) | ||
.collect(Collectors.toList()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
,
를 매직넘버화 해도 좋을 것 같습니다!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
winningLottos도 그렇고 입력받은 lotto를 ,
를 기준으로 파싱하는 로직을 하늘님은 Controller에서 작성해주셨군요!
저는 로또 미션을 할 때 해당 로직을 어디에 위치시켜야하는가에대한 고민이 많았어요. 하늘님은 어떤 이유로 Controller에 해당 로직을 두셨나요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
","를 private static final String DELIMITER = ",";
로 상수화하였습니다! 입력 형식도 변할 수 있는 규칙이기 때문에 상수화 하여 가독성을 올리고, 유지보수성을 높이는 것이 좋은 것 같습니다
처음에는 컨트롤러가 View에서 받은 입력을 Model에 전달할 때 가공하는 역할을 해야한다고 생각했었습니다. 그러나 코멘트를 보고 다시 고민해 보니, 입력과 관련된 처리는 InputView
가 담당하는 것이 맞다고 생각하여 로직을 옮겼습니다. InputView
로 옮겼을 때 코드가 더 간결해지는 것도 장점인 것 같습니다!
src/test/java/model/LottoTest.java
Outdated
} | ||
|
||
; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
엔터가 잘못들어갔네요!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
실수로 들어간 것 같습니다. 수정하였습니다!
@Test | ||
void Lotto_생성_시_숫자가_6개가_아니면_예외가_발생한다() { | ||
//given | ||
List<Integer> numbersWithFive = List.of(1, 2, 3, 4, 5); | ||
|
||
//when & then | ||
assertThatThrownBy(() -> new Lotto(numbersWithFive)) | ||
.isInstanceOf(IllegalArgumentException.class); | ||
} | ||
|
||
@Test | ||
void Lotto_생성_시_중복된_숫자가_있으면_예외가_발생한다() { | ||
//given | ||
List<Integer> numbersWithDuplicates = List.of(1, 2, 3, 4, 5, 5); | ||
|
||
//when & then | ||
assertThatThrownBy(() -> new Lotto(numbersWithDuplicates)) | ||
.isInstanceOf(IllegalArgumentException.class); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
현재는 validateSize()
와 validateDuplicates()
가 로또 생성 시 명시적으로 포함되어 있어서, 비즈니스로직 검증을 정상적으로 하고있어요.
하지만 만약 추후에 Set으로 완전히 대체된다면 중복 및 개수 검증이 컬렉션 자체에 위임되므로, 테스트 의도가 모호해질 수 있을 것 같아요!
나중에 다른 미션에서 테스트 코드를 작성할 때에도, 내가 작성한 로직이 정말 비즈니스 로직을 검증하는 코드가 맞는지, 아니면 Java의 내장 기능을 검증하는 것인지 구별하시면 좋을 것 같아서 코멘트 남겼습니다~
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
만약 중복예외 로직을 삭제한 후에도 테스트를 사용한다면 Java컬렉션의 내장 기능을 테스트하게 될 수도 있을 것 같습니다. 테스트의 의도를 명확하게 인지하고, 모호해지지 않도록 주의하겠습니다!!
컨트롤러를 도메인 책임 분리와 기능별 분리로 나누는 방식이 있다는 것은 처음 알게 되었습니다 . 말씀해주신 대로, 각 방식의 장단점을 더 찾아보고 공부해보겠습니다. |
Set<LottoNumber> numbers = convertToLottoNumberSet(initialNumbers); | ||
validateSize(numbers); | ||
validateDuplicates(numbers); | ||
this.numbers = convertToLottoNumbers(numbers); | ||
this.numbers = new TreeSet<>(numbers); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
convertToLottoNumberSet 메서드를 사용해 numbers를 Treeset형태로 만들어주기 때문에 한번 더 TreeSet으로 감쌀 필요는 없어보여요!
public Lotto(List<Integer> initialNumbers) {
Set<LottoNumber> lottoNumbers = convertToLottoNumberSet(initialNumbers);
validateSize(lottoNumbers);
this.numbers = lottoNumbers;
}
또는
public Lotto(List<Integer> initialNumbers) {
this.numbers = convertToLottoNumberSet(initialNumbers);
validateSize(this.numbers);
}
이런식으로 해도 될 것 같은데, 혹시 제가 놓친 의도가 있을까요?!
모두 잘 반영해주셨네요! 수고하셨습니다👏 다음 미션도 파이팅🔥🔥🔥 |
안녕하세요 해윤님!
로또 미션 3, 4, 5단계 리뷰이로 만나게 된 김하늘입니다. 리뷰이로서 뵙는건 처음이네요! 부족한 부분에 대한 많은 가르침 부탁드립니다🙇
프로젝트 구조
현재 프로젝트 구조는 다음과 같습니다
궁금한 점
추가로, 현재
Can’t automatically merge. Don’t worry, you can still create the pull request.
라는 경고 문구가 떠있습니다. 최대한 해결 방법을 찾으려 노력했으나, 방법을 찾지 못해 우선 리뷰 요청 드립니다. 최대한 빠르게 해결해 보겠습니다😭