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단계 로또 제출합니다. #30

Merged
merged 92 commits into from
Feb 23, 2023

Conversation

tmdgh1592
Copy link
Member

안녕하세요 제리님!
제리님의 피드백 덕분에 구현과 구조에 대해서 더 생각해볼 수 있었습니다.
그런데 궁금한 점이 생겨서 질문을 남겨보려고 합니다..!

1단계 미션의 마지막 피드백으로
rank 의 각 element 가 보너스 번호 검사 여부를 알고 있다면, 아래 조건문도 제거할 수 있지 않을까요? :) 라고 리뷰를 남겨주셨는데

enum class Rank(val countOfMatch: Int, val winningMoney: Int) {
    FIRST(6, 2_000_000_000),
    SECOND(5, 30_000_000),
    THIRD(5, 1_500_000),
    FOURTH(4, 50_000),
    FIFTH(3, 5_000),
    MISS(0, 0),
    ;

    companion object {
        fun valueOf(countOfMatch: Int, matchBonus: Boolean): Rank {
            if (SECOND.countOfMatch == countOfMatch && matchBonus) return SECOND
            if (SECOND.countOfMatch == countOfMatch && !matchBonus) return THIRD

            return Rank.values().find { it.countOfMatch == countOfMatch } ?: MISS
        }
    }
}

제가 이해한 바로는 Rank의 프로퍼티로 matchBonus와 같은 Boolean 타입을 추가해서, valueOf() 메서드 호출시 2등에 대한 비교를 하지 않아도 되는 구조로 변경해보기를 추천하신 것 같습니다!

여기서 궁금한 점은 만약 matchBonus와 같은 프로퍼티가 추가된다면, FIRST부터 MISS까지 요소를 각각 다른 이름으로 하나씩 더 만들어줘야 하는 불편함이 발생할 수도 있지 않을까 입니다..!
그럼에도 valueOf()에서 비교하는 것보다 어떠한 부분에서 이점이 있는지 궁금합니다! 🙂

- lotto 패키지 내 Number 관련 클래스는 lotto.number 패키지로 이동
- Rank 클래스는 Rank 패키지로 분리
@vagabond95
Copy link

valueOf()에서 비교하는 것보다 어떠한 부분에서 이점이 있는지.
좋은 질문입니다 :)

말씀하신 것처럼 제가 제안 드린 방향은 enum 의 모든 element 에 추가 필드를 넣어줘야하는 일이 발생합니다.
반면, 앞으로 어떤 등수가 보너스 번호를 가지게 되더라도 if 문을 추가할 필요 없이, matchBonus 등의 필드만 수정하면 변경사항없이 요구사항을 만족할 수 있게 됩니다.

모든 구현에는 트레이드 오프요소가 존재합니다. 100% 장점만 있는 구현방식은 존재하지 않지요 ㅎㅎ
저는 이 중 후자의 요소를 더 중요하다고 생각하여 코멘트를 남겼습니다. 어떠한 비즈니스 로직을 수행할 때 일관된 규칙(보너스 번호 여부를 검사할 것인가?)이 아니라 예외처리 방식 (2등만 검사)으로 구현을 하게 되면, 세부 히스토리를 파악하는데 드는 에너지가 더 많이들고 휴먼 에러가 자주 발생하더라구요.

Copy link

@vagabond95 vagabond95 left a comment

Choose a reason for hiding this comment

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

2단계 미션도 잘 마무리해주셨네요 👍

질문 주신 내용을 비롯하여 몇가지 코멘트 남겼는데, 확인 후 리뷰요청 부탁드릴게요 :)

private val NUMBERS: Map<Int, LottoNumber> = (MIN_LOTTO_NUMBER..MAX_LOTTO_NUMBER).associateWith(::LottoNumber)

fun from(number: String): LottoNumber {
val lottoNumber = requireNotNull(number.trim().toIntOrNull()) { ERROR_MESSAGE_NOT_NUMERIC_LOTTO_NUMBER }

Choose a reason for hiding this comment

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

number 가 int 로 변환할 수 있는 숫자 형태인지는
입력 레벨에서도 검증이 가능할 것 같은데요.

타입에 대한 검증을 view 에서 하는게 적절할지, domain 에서 하는게 적절할지 고민해봐도 좋겠습니다 :)

Copy link
Member Author

Choose a reason for hiding this comment

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

제리님의 피드백과 미션의 힌트를 함께 참고하여 수정해보았습니다.
논리적인 오류가 아닌, 단순히 값을 잘못 입력받은 상황이기 때문에 예외를 발생시키기보다 에러메시지를 출력해주고 재입력받도록 수정하였습니다!

init {
require(amount >= MIN_AMOUNT) { ERROR_MESSAGE_NEGATIVE_AMOUNT }
}

fun divideByThousand() = amount.div(THOUSAND)
fun divideBy(divisor: Money) = amount.div(divisor.amount)

Choose a reason for hiding this comment

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

해당 함수도 operator 로 사용해보는 것은 어떨까요?

사칙연산에 대한 일관적인 구현 방식이 제공되면 좋을 것 같아요.

Copy link
Member Author

Choose a reason for hiding this comment

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

감사합니다 : )
해당 메서드를 operator 로 변경하였습니다!

println(PURCHASED_LOTTO_SIZE_FORMAT.format(lottos.size))
lottos.map { it.getSortedLotto() }.forEach { sortedNumber ->
fun printPurchasedLottos(manualLottos: List<ManualLotto>, autoLottos: List<AutoLotto>) {
println()

Choose a reason for hiding this comment

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

println(..) 에 대한 구현을 별도 함수로 한번 감싸보는 것은 어떨까요?

만약 표준 입출력에 대한 스펙 혹은 구현사항이 바뀔경우, 현재 구조에서는 println() 이 사용된 모든 코드에 변경이 필요 할 것 같아요.

Copy link
Member Author

Choose a reason for hiding this comment

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

오.. 미처 거기까지 생각하지 못했습니다!
표준 입출력에 대한 스펙과 구현이 언젠가 변경될 수도 있음을 감안하는 좋은 시야를 얻을 것 같습니다~ 👏


import domain.lotto.number.LottoNumber

class AutoLotto(lottoNumbers: List<LottoNumber>) : PurchasedLotto(lottoNumbers)

Choose a reason for hiding this comment

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

AutoLotto 와 ManualLotto 의 구현상 차이가 없는데, 별도 클래스로 나누는 이유가 있을까요?
클래스로 나눴다면, 관련된 비즈니스 로직을 각 클래스에서 스스로 처리할 수 있도록 역할을 좀 더 부여해보면 좋겠어요.

Copy link
Member Author

@tmdgh1592 tmdgh1592 Feb 21, 2023

Choose a reason for hiding this comment

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

출력 예시를 보면 수동으로 3장, 자동으로 11개를 구매했습니다.를 출력하고, 그 아래에 수동 로또 번호와 자동 로또 번호를 순서대로 출력하고 있습니다.
만약 ResultView에서 fun printPurchasedLottos(manualLottos: List<PurchasedLotto>, autoLottos: List<PurchasedLotto>)와 같은 형태로 전달받도록 한다면, 타입이 같다보니 실수로 반대로 출력하는 경우가 발생할 수도 있어서 이 둘을 별도의 클래스로 분리하고 상속하였습니다!
하지만 제리님 말씀대로 클래스로 나눴다면, 관련 비즈니스 로직을 포함하고 있으면 더 좋을 듯 싶습니다!
리팩터링 과정에서 고민해보겠습니다 : )

@tmdgh1592
Copy link
Member Author

valueOf()에서 비교하는 것보다 어떠한 부분에서 이점이 있는지.
좋은 질문입니다 :)

말씀하신 것처럼 제가 제안 드린 방향은 enum 의 모든 element 에 추가 필드를 넣어줘야하는 일이 발생합니다. 반면, 앞으로 어떤 등수가 보너스 번호를 가지게 되더라도 if 문을 추가할 필요 없이, matchBonus 등의 필드만 수정하면 변경사항없이 요구사항을 만족할 수 있게 됩니다.

모든 구현에는 트레이드 오프요소가 존재합니다. 100% 장점만 있는 구현방식은 존재하지 않지요 ㅎㅎ 저는 이 중 후자의 요소를 더 중요하다고 생각하여 코멘트를 남겼습니다. 어떠한 비즈니스 로직을 수행할 때 일관된 규칙(보너스 번호 여부를 검사할 것인가?)이 아니라 예외처리 방식 (2등만 검사)으로 구현을 하게 되면, 세부 히스토리를 파악하는데 드는 에너지가 더 많이들고 휴먼 에러가 자주 발생하더라구요.

답변해주셔서 감사합니다!
메서드의 로직을 단순화시키는 방법을 택함으로써 Human Error를 줄일 수 있는 점을 감안하면 제리님께서 말씀하신 방식에 더 눈길이 갑니다 : )

Copy link

@vagabond95 vagabond95 left a comment

Choose a reason for hiding this comment

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

빠르게 코멘트 반영해주셨네요 👍

미처 코멘트 드리지 못했던 부분들도 스스로 개선하시는 부분도 멋지십니다 ㅎㅎ
관련하여 몇가지 고민해볼 점에 대해 추가로 코멘트 남겼는데 확인 부탁드릴게요!

}

private fun purchaseLotto(): Pair<Money, List<PurchasedLotto>> {
private fun inputLottos(): Triple<List<PurchasedLotto>, List<PurchasedLotto>, Money> {

Choose a reason for hiding this comment

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

인풋을 각 함수로 분리해보는 것은 어떨까요?
현재 구조에서는 input 으로 받는 값이 추가될 때마다 리턴형이 계속 바뀌어야 할 것 같아요.

Copy link
Member Author

Choose a reason for hiding this comment

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

관련 메서드끼리 묶어보려고 하다보니 오히려 변경에 취약한 구조가 된 것 같습니다..!
별도의 메서드로 분리해보겠습니다!


fun from(number: String): LottoNumber =
LottoNumber(requireNotNull(number.toIntOrNull()) { ERROR_MESSAGE_NOT_NUMERIC_LOTTO_NUMBER })
private val NUMBERS: Map<Int, LottoNumber> = (MIN_LOTTO_NUMBER..MAX_LOTTO_NUMBER).associateWith(::LottoNumber)

Choose a reason for hiding this comment

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

캐시 활용 👍


return Rank.values().find { it.countOfMatch == countOfMatch } ?: MISS
}
fun valueOf(countOfMatch: Int, matchBonus: Boolean): Rank =

Choose a reason for hiding this comment

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

보너스 번호를 요구하는 등수가 추가되더라도 해당 함수에는 변경이 발생하지 않겠네요 👍

): Pair<Money, List<PurchasedLotto>> {
require(lottoSize.value == lottoNumbers.size) { ERROR_MESSAGE_INVALID_PURCHASING_LOTTO_SIZE }
require(money.isGreaterThan(Money(LOTTO_PRICE * lottoSize.value))) { ERROR_MESSAGE_INSUFFICIENT_AMOUNT }
return Pair(

Choose a reason for hiding this comment

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

Pair 를 사용하는 것이 나쁜 것은 아니지만, 부득이한 경우가 아니라면 별도 클래스를 생성하고 그것을 이용해보는 것은 어떨까요?
Pair 단위의 데이터는 왜 묶였는지, 각각이 어떤 역할을 하는지 알기 어려울 수 있을 것 같아요.

Copy link
Member Author

Choose a reason for hiding this comment

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

확실히 Pair로만 반환한다면, 각 Pair의 요소가 어떤 의미인지 파악하기 어려울 수 있을 것 같습니다!
반환하고자 하는 객체를 담고 있는 별도의 data class를 생성해서 return해보는 방향으로 구현해보겠습니다.

기존 코드가 Pair 객체로 변수를 초기화 할 때 구조 분해 방식을 사용하고 있었기 때문에, 이를 수정하지 않아도 되는 data class를 사용하여 수정해보겠습니다!

Copy link

@vagabond95 vagabond95 left a comment

Choose a reason for hiding this comment

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

추가적으로 코멘트 드릴 부분이 보이지 않습니다 👍
로또 미션 진행하시느라 고생 많으셨어요.
해당 미션을 통해 얻어가는 것이 있으셨길 바랍니다 :)

남은 미션도 화이팅이십니다!

@vagabond95 vagabond95 merged commit 1b130ea into woowacourse:tmdgh1592 Feb 23, 2023
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.

3 participants