Skip to content
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

[로또 2단계] 시아 미션 제출합니다. #135

Open
wants to merge 26 commits into
base: leeyerin0210
Choose a base branch
from

Conversation

Leeyerin0210
Copy link

셀프 체크리스트

  • 프로그램이 정상적으로 작동하는가?
  • 모든 테스트가 통과하는가?
  • 이전에 받은 피드백을 모두 반영하였는가?
  • 코딩 스타일 가이드를 준수하였는가?
    • IDE 코드 자동 정렬을 적용하였는가?
    • 린트 검사를 통과하였는가?
  • 프로그래밍 요구 사항을 준수하였는가?
  • README.md에 기능 목록을 정리하고 명확히 기술하였는가?

어떤 부분에 집중하여 리뷰해야 할까요?

객체지향적 사고와 MVC 패턴이 잘 지켜졌는지를 우선시 해서 확인해주세요!

코드 리뷰 커뮤니케이션

  • 이전 코드의 객체 별 협력 관계가 어느 정도 개선되었나요?
  • 출력 코드를 개선하기 위해 enum에 출력 관련 정보들을 넣어뒀는데, 이러면 뷰와 도메인이 너무 강하게 결합된 것일까요? 처음엔 아에 enum을 순회하면서 출력하도록 했다가, 너무 결합된 안 좋은 코드 같아서, 다시 result을 가져와서 출력하도록 했습니다.
  • 요구사항은 에러가 생겼을 때 null을 리턴하라고 했으나, null을 사용하면 후속 코드가 전부 꼬이는 것 같습니다. 그래서 잘못된 입력을 받았을 때 객체에서 null을 반환하고, 컨트롤러가 null을 확인 하면 다시 입력 받도록 하였는데, 이게 맞는 방법인지 잘 모르겠습니다. null을 받았을때 null 을 계속 가져갈 수 있도록 코드를 짜는 게 좋을까요, 아니면 null은 그 자리에서 다시 입력받아서 해결시키는게 나은 방법일까요?
  • 예외 발생 시 다시 입력받도록 해서 예외 처리 테스트가 불가능한데, 이런 경우에는 출력문과 직접 비교하여 테스트하면 되는 것일까요?
  • null를 받았을 때 다시 인풋받기 위해서 컨트롤러에 null 확인 로직을 넣어놓았는데, 이 정도 로직은 괜찮을까요?

📌 GitHub에서 PR에 댓글 남기는 방법

참고 자료

스크린샷

Copy link

@laco-dev laco-dev left a comment

Choose a reason for hiding this comment

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

2단계 고생 많으셨습니다.

논리적인 오류를 인지하는데 있어 고민을 많이 하신 흔적이 보입니다.
다만, 논리적인 오류에 대한 개념을 잘못 잡고 계신 부분이 문제가 된 것 같습니다.

크루들과 논리적인 오류는 무엇인가? 에 대한 주제부터 먼저 토론을 해봐야 할 것 같아요.

@Leeyerin0210
Copy link
Author

잘못된 입력을 받았을 때 처리하는 것/논리적 오류와 비 논리적 오류 분리를 중점으로 수정해보았습니다!
한 객체을 수정하니 영향받는 다른 객체들을 수정하다보니 한 커밋에 너무 많은 변경사항이 들어갔네요..죄송합니다.
항상 날카로운 리뷰 너무 감사드립니다! 제가 놓치고 있는게 있다면 계속해서 가감없이 말씀해주세요!

Copy link

@laco-dev laco-dev left a comment

Choose a reason for hiding this comment

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

예외 처리를 통해 에러가 발생하지 않도록 한다.

일반적으로 try-catch를 사용하지 않는 말 때문에 문제가 시작된 것으로 보이지만...
좋은 코드는 정말 이렇게 짜야 한다고..? 라는 어색함이 생기기를 바라고 있습니다 😄

예외 처리 관련해서는 더이상 개선하지는 마시고, 나중에 더 학습이 되신 후에 지금 코드를 돌아보시는 방법이 좋을 것 같아요.
나중에 자신의 코드를 돌아볼 때 느끼는 점이 훨씬 클 것 같다?

지금은 로또를 진행하기 위한 어색한 흐름을 해결하고 정리하는 것을 챙겨보시죠.

@Leeyerin0210
Copy link
Author

너무 늦게 완성했습니다. 죄송합니다.
전반적으로 조금 더 보기 좋은 코드가 된 것 같습니다.
어떻게 하면 객체 별 결합을 더 강하게 할 수 있을까요?
또한 출력을 위한 메세지 오브젝트를 만들었는데, 이 오브젝트 내의 값을 컨트롤러가 직접 넣어서 뷰에게 보내는게 괜찮을까요?

Copy link

@laco-dev laco-dev left a comment

Choose a reason for hiding this comment

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

시아 고생 많으셨습니다.

전부 갈아엎고 다시 시도한 것만으로도 우선 칭찬드리고 싶습니다.
TDD 관점에서 리뷰를 드렸으며 객체지향적인 사고와 API 설계 관점에서 다시 처음부터 의견 남겼습니다.

객체가 스스로 해야할 역할을 팩토리 메서드나 다른 클래스에 만들어 자율적인 객체와는 거리가 더 멀어진 것 같습니다.

더 성장하시고 싶어 하신 모습에 따라 피드백이 다소 쎄게 들어갔으니 양해 부탁해요. 😢
할 수 있는 만큼만 반영 하시고 6시 전에 제출하려고 하다말고 올리는 것보다는 차라리 전부 반영하고 나중에 다시 올려주시는게 나을 것 같아요.

Comment on lines 10 to 18
fun valueOf(value: Int): LottoNumber {
require(value in VALID_RANGE) { RANGE_ERROR }
return LottoNumber(value)
}

fun createOrNull(value: Int): LottoNumber? {
if (validation(value)) return LottoNumber(value)
return null
}
Copy link

Choose a reason for hiding this comment

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

API 설계자의 입장으로서 바라보면 2가지가 아쉬운데요.

  1. 생성 인터페이스에 일관성이 부족하다
    하나는 valueOf 다른 하나는 create로 이어지는 흐름이 부자연스러워요.
    예를 들어 toInt(), toIntOrNull() 같은 예시를 들 수 있겠죠.

  2. private constructor가 만능은 아닙니다.
    data class로 만들면 불가능한 옵션이기도 하고 클래스의 논리적인 흐름을 외부로 넘겼을 뿐이라서요.

init { require(...) }

fun valueOf(value: Int): LottoNumber  {
   LottoNumber(...)
}

fun valueOfOrNull(value: Int): LottoNumber ? {
    runCatching { valueOf(value) }
       .getOrNull()
}

Copy link
Author

Choose a reason for hiding this comment

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

19253ff
생성 일관성을 제시해주신 코드를 바탕으로 추가하였습니다!

README.md Outdated
Comment on lines 24 to 26
- [x] 로또 1매에는 6개의 로또 번호가 존재한다.
- [x] 로또 번호는 중복되지 않는다.
- [x] 로또 번호는 오름차순으로 정렬된다.
Copy link

Choose a reason for hiding this comment

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

feat : 로또 객체 생성

TDD로 한다면 위 기능 3가지를 모두 별도의 커밋으로 하는게 좋았을 것 같아요.
지금은 이미 완성된 모든 기능을 이해한 채로 객체부터 만들게 되니까요

Copy link
Author

Choose a reason for hiding this comment

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

유념하겠습니다!

Comment on lines 14 to 19
fun createOrNull(numberList: List<LottoNumber>): Lotto? =
when {
numberList.size != LOTTO_NUMBER_QUANTITY -> null
numberList.distinctBy { it.value }.size != numberList.size -> null
else -> Lotto(numberList)
}
Copy link

Choose a reason for hiding this comment

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

마찬가지로 정적 팩토리 메서드가 객체를 생성하는 책임 외에도, 비즈니스 로직을 가지기 때문에 책임이 여러개로 나뉘었다고 생각합니다.

생성자에서 이미 유효성 검사를 하고 있다면 굳이 중복할 필요가 없습니다.

class Amount private constructor(
val money: Int,
) {
fun getCount(lottoPrize: Int): Int = money / lottoPrize
Copy link

Choose a reason for hiding this comment

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

Amount를 유연하게 만들었다면, 더이상 "로또" 라는 관심사를 가질 필요가 있을까요?
이 클래스는 LottoAmount가 되어야 했을까요?

Comment on lines 8 to 11
fun paymentOrNull(payMoney: Int): Amount? {
if (money < payMoney) return null
return Amount(money - payMoney)
}
Copy link

Choose a reason for hiding this comment

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

금액은 음수일 수 없다는 것을 null로 하는게 자연스러운 흐름일까요?
물론 마이너스가 되면 논리적으로 문제가 될 수 있으니 null을 반환할 수 도 있다고 생각하지만.
아무리 빼더라도 0보다 작아지지 않는다는 개념을 적용하면 이렇게 만들어 볼 수도 있습니다.

data class Amount(val money: Int) {
  require(money >= 0)

 fun pay(other: Amount): Amount {
   val newMoney = (money - other.money).coerceAtLeast(0)
   return copy(money = newMoney)
 }
}

결과적으로 이 함수를 사용할 때 마다 null을 컨트롤 해야한다는게 매우 번거롭다고 느껴지니까요

Comment on lines 67 to 69
is LottoCreationResult.Failure.InvalidCount -> outputView.printErrorMessage(Message.errorInvalidLotto())
is LottoCreationResult.Failure.DuplicatedNumbers -> outputView.printErrorMessage(Message.errorInvalidLotto())
is LottoCreationResult.Failure.NotSorted -> outputView.printErrorMessage(Message.errorInvalidLotto())
Copy link

Choose a reason for hiding this comment

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

커스텀 예외를 만들었지만 결국 동일한 출력이 발생하는 거라면 오버엔지니어링으로 보이네요!
이럴거면 굳이 타입을 만들 필요가 있었을까요?

runCatching { 
   val lotto = Lotto.valueOf(...)
}.getOrElse { ... }

Comment on lines 84 to 88
val result =
WinningLotto.create(
inputView.getWinningLotto().mapNotNull { LottoNumber.createOrNull(it) },
LottoNumber.createOrNull(inputView.getBonusNumber()) ?: return getWinningLotto(),
)
Copy link

Choose a reason for hiding this comment

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

코드 깊이가 너무 들어갔어요.
요구사항에 있듯이 코드 indent가 깊어지지 않도록 정리해보면 좋을 것 같아요

Comment on lines 91 to 93
is WinningLottoCreationResult.Failure.NumberSizeError -> outputView.printErrorMessage(Message.errorInvalidWinningNumbers())
is WinningLottoCreationResult.Failure.BonusNumberDuplicated -> outputView.printErrorMessage(Message.errorInvalidBonusNumber())
is WinningLottoCreationResult.Failure.DuplicatedNumbers -> outputView.printErrorMessage(Message.errorInvalidWinningNumbers())
Copy link

Choose a reason for hiding this comment

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

또한 출력을 위한 메세지 오브젝트를 만들었는데, 이 오브젝트 내의 값을 컨트롤러가 직접 넣어서 뷰에게 보내는게 괜찮을까요?

왜 괜찮다고 생각하는지 궁금합니다.
Result 타입을 그대로 OutputView로 전달하면 안되는건가요?

Copy link
Author

@Leeyerin0210 Leeyerin0210 Mar 6, 2025

Choose a reason for hiding this comment

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

result 타입을 그대로 뷰에 전달하면 뷰가 너무 도메인에 대한 세부 정보를 알게 되는 것이 아닌가..해서 이렇게 작성했습니다!

Comment on lines 98 to 102
private fun sortResultsByOriginalRankOrder(ranks: List<Rank>): List<Pair<Rank, Int>> =
Rank.entries
.filter { it != Rank.MISS }
.map { rank -> rank to ranks.count { it == rank } }
.reversed()
Copy link

@laco-dev laco-dev Mar 3, 2025

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.

33d22ea
전의 코드에서는 Rank를 순회하면서 ranks 순회하면서 매번 맞는 값을 찾는 식이었는데, 새로 고친 코드에서는 ranks 한번만 순회해서 맞는 키값에 1을 더하는 식으로 바꾸었습니다!

Comment on lines 16 to 19
class LottoController(
val inputView: InputView,
val outputView: OutputView,
) {
Copy link

Choose a reason for hiding this comment

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

feat : 입출력 기능 추가

이게 어떻게 봐야 입출력 기능추가인가요..?

Copy link
Author

Choose a reason for hiding this comment

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

입출력 기능을 실행시키기 위한 컨트롤러도 포함되는 줄 알았습니다. 앞으로는 컨트롤러는 따로 커밋하도록 하겠습니다!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants