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단계] 로또 구현 #578

Open
wants to merge 10 commits into
base: cafemug
Choose a base branch
from
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,8 @@
- 힌트: indent(인덴트, 들여쓰기) depth를 줄이는 좋은 방법은 함수(또는 메서드)를 분리하면 된다.
- 함수(또는 메서드)의 길이가 10라인을 넘어가지 않도록 구현한다.
- 함수(또는 메서드)가 한 가지 일만 잘 하도록 구현한다.

## 로또 기능 요구사항
- 로또 구입 금액을 입력하면 구입 금액에 해당하는 로또를 발급해야 한다.
- 로또 1장의 가격은 1000원이다.

Empty file removed src/main/kotlin/.gitkeep
Empty file.
8 changes: 8 additions & 0 deletions src/main/kotlin/lotto/common/ExceptionCode.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package lotto.common

sealed class ExceptionCode {
object NotAllowNullOrBlank : IllegalArgumentException("Input에 Null이나 빈 값이 있으면 안됩니다")
object NotMatchNumeric : IllegalArgumentException("Input이 숫자가 아닙니다")
object NotFindSeparator : IllegalArgumentException("Input에 구분자 ,가 없습니다")
object NotWinLotteryCount : IllegalArgumentException("당첨번호가 6자리가 아닙니다")
}
Comment on lines +3 to +8

Choose a reason for hiding this comment

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

요건 개인 취향일 수 있는데 저는 가급적이면 커스텀 예외가 아니라 표준 라이브러리에 존재하는 예외(IllegalArgument, IllegalState 등)을 이용해 처리해보아도 좋을 것 같아요. 표준 라이브러리의 예외는 많은 개발자가 알고 있으므로 널리 알려진 요소를 재사용하면 다른 사람들이 API를 더 쉽게 배우고 이해할 수 있는 장점이 있습니다.

예외 메시지의 중복이 문제가 될 수 있는데 중복이 엄청 많은게 아니라면 그냥 그때그때 필요한 도메인에 예외 메시지를 남겨도 된다고 생각해요.

가급적 표준 예외를 다루라고 했지만 무조건적으로 표준 예외를 사용하지 않고 아래와 같은 경우는 커스텀 예외를 만드는게 더 좋을수도 있습니다.

  • 컨트롤러 레이어에서 JPA의 Dataintegrityviolationexception 과 같은 저수준 예외를 활용하는 경우
  • require나 check에서 발생시키는 예외가 이미 구현된 예외 처리 구문과 겹쳐 예외를 구분해야 하거나 하는 경우

30 changes: 30 additions & 0 deletions src/main/kotlin/lotto/common/InputValidation.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package lotto.common

class InputValidation {
fun amountValidate(input: String?): Int {
require(!input.isNullOrBlank()) {
throw ExceptionCode.NotAllowNullOrBlank
}

require(input.matches(Regex("\\d+"))) {
throw ExceptionCode.NotMatchNumeric
}
return input.toInt()
}

fun winLotteryValidation(input: String?): List<Int> {
require(!input.isNullOrBlank()) {
throw ExceptionCode.NotAllowNullOrBlank
}

require(input.matches(Regex("(.*),(.*)"))) {
throw ExceptionCode.NotFindSeparator
}

val winLottery = input.split(",").map { it.toInt() }
if (winLottery.size != 6) {
Comment on lines +24 to +25

Choose a reason for hiding this comment

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

당첨 번호도 일반 로또 게임 하나와 동일하게 6개의 숫자, 1-46까지의 숫자라는 동일한 제약 조건을 가집니다.

input에서 검증하더라도 이후 로직에 따라 로또 도메인 객체에 7개의 숫자, 46이 넘는 숫자, 음수 등이 들어가 제약 조건이 깨질 수 있습니다. input단에서 검증하지 않고 로또 도메인 객체를 통해 검증해보세요.

throw ExceptionCode.NotWinLotteryCount
}
return winLottery
}
}
38 changes: 38 additions & 0 deletions src/main/kotlin/lotto/domain/Draw.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package lotto.domain

import lotto.entity.Lottery
import lotto.entity.WinLotteryResult

class Draw {
private val lotteryPrice = 1000
fun calculateBuyNum(amount: Int): Int {
return amount / lotteryPrice
}

fun drawLotteries(num: Int): List<Lottery> {
return (1..num).map {
Lottery(makeShuffleNumbers())
}
}

fun calculateWin(winNumbers: List<Int>, lotteries: List<Lottery>): WinLotteryResult {
val winLotteryResult = WinLotteryResult()
winLotteryResult.apply {
lotteries.forEach { checkWin(it.values.intersect(winNumbers.toSet()).size, this) }
}
return winLotteryResult
}

private fun checkWin(num: Int, winLotteryResult: WinLotteryResult) {
when (num) {
3 -> winLotteryResult.matchThree.matchNum += 1
4 -> winLotteryResult.matchFour.matchNum += 1
5 -> winLotteryResult.matchFive.matchNum += 1
6 -> winLotteryResult.matchSix.matchNum += 1
Comment on lines +27 to +31

Choose a reason for hiding this comment

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

로또의 요구사항이 변경될 일은 거의 없겠지만 여기에 당첨 숫자가 늘어났을 때 when 구문에 코드를 작성하는걸 잊으면 버그가 발생할 수 있겠네요. enum 등을 활용해 코드를 개선해보세요.

Comment on lines +18 to +31

Choose a reason for hiding this comment

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

만약 enum 등을 활용해 당첨 결과(1등, 2등, 3등 등)을 분류하고 Map<Enum, Int>와 같은 자료구조를 활용하신다고 하면 코틀린 stdlib에서 제공하는 groupingby와 유사한 함수들을 활용해 결과 추출을 불변으로 처리해볼 수 있을 것 같아요.

}
}

private fun makeShuffleNumbers(): List<Int> {
return (1..45).shuffled().subList(0, 6).sorted()
}
}
3 changes: 3 additions & 0 deletions src/main/kotlin/lotto/entity/Lottery.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package lotto.entity

data class Lottery(val values: List<Int>)

Choose a reason for hiding this comment

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

지금은 항상 makeShuffleNumbers을 통해 Lottery가 생성되겠지만 이후에 다른 사람과 협업을 하는 과정에서 values에 listOf(1,2,3,4,5,6,7)이나 listOf(50,60,70,80,90,100)을 넣는 경우가 생길 수 있습니다.

당장 테스트만 봐도 간단한 실수로 제약이 깨질 수 있어보이네요.

val lotteries = listOf(Lottery(listOf(1, 2, 3, 11, 12, 13)), Lottery(listOf(1, 2, 3, 4, 5, 6)))

input 쪽에서 제약 조건을 검증할수도 있겠지만 이런 도메인 클래스를 통해 예외를 검증해보시면 좋을 것 같아요.

14 changes: 14 additions & 0 deletions src/main/kotlin/lotto/entity/WinLottery.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package lotto.entity

private const val rewardThree = 5000
private const val rewardFour = 50000
private const val rewardFive = 1500000
private const val rewardSix = 2000000000

data class WinLottery(var matchNum: Int = 0, val reward: Int, val count: Int = 0)
data class WinLotteryResult(
var matchThree: WinLottery = WinLottery(0, rewardThree),
var matchFour: WinLottery = WinLottery(0, rewardFour),
var matchFive: WinLottery = WinLottery(0, rewardFive),
var matchSix: WinLottery = WinLottery(0, rewardSix)
)
Comment on lines +3 to +14

Choose a reason for hiding this comment

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

enum 등을 활용해 1등, 2등, 3등 당첨 개수와 금액을 별도로 추출해 관리하면 어떨까요?

27 changes: 27 additions & 0 deletions src/main/kotlin/lotto/lotto.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package lotto

import lotto.common.InputValidation
import lotto.domain.Draw
import lotto.view.ResultView
import view.InputView

fun main() {
val inputView = InputView()
val resultView = ResultView()
val draw = Draw()
val inputValidation = InputValidation()

// 구입하기
val amount = inputValidation.amountValidate(inputView.start())
val num = draw.calculateBuyNum(amount)
resultView.buyLottery(num)

// 로또 구입 내역 보여주기
val lotteries = draw.drawLotteries(num)
lotteries.forEach { resultView.printLottery(it) }

// 지난주 당첨번호 입력 받기
val winLottery = inputValidation.winLotteryValidation(inputView.winLottery())
val winLotteryResult = draw.calculateWin(winLottery, lotteries)
resultView.printWinResult(winLotteryResult)
}
15 changes: 15 additions & 0 deletions src/main/kotlin/lotto/view/InputView.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package view

class InputView {
fun start(): String? {
val startMsg = "구입금액을 입력해주세요"
println(startMsg)
return readln()
}

fun winLottery(): String? {
var winLotteryMsg = "지난 주 당첨 번호를 입력해주세요."
println(winLotteryMsg)
return readln()
}
}
28 changes: 28 additions & 0 deletions src/main/kotlin/lotto/view/ResultView.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package lotto.view

import lotto.entity.Lottery
import lotto.entity.WinLottery
import lotto.entity.WinLotteryResult

class ResultView {
fun buyLottery(num: Int) {
var buyLotteryMsg = "${num}개를 구매했습니다"
println(buyLotteryMsg)
}

fun printLottery(lottery: Lottery) {
println(lottery.values)
}

fun printWinResult(winLotteryResult: WinLotteryResult) {
printWin(3, winLotteryResult.matchThree)
printWin(4, winLotteryResult.matchFour)
printWin(5, winLotteryResult.matchFive)
printWin(6, winLotteryResult.matchSix)
}

private fun printWin(matchNum: Int, winLottery: WinLottery) {
val printWinMsg = "${matchNum}개 일치 (${winLottery.reward}원) - ${winLottery.matchNum}개"
println(printWinMsg)
}
}
9 changes: 9 additions & 0 deletions src/main/kotlin/stringCaculator/StringCaculator.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import stringCaculator.domain.Calculator
import stringCaculator.view.InputView
import stringCaculator.view.OutputView

fun main() {
val text = InputView().input()
val result = Calculator().sum(text)
OutputView().printConsole(result)
}
8 changes: 8 additions & 0 deletions src/main/kotlin/stringCaculator/domain/Calculator.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package stringCaculator.domain

class Calculator {
fun sum(input: String?): Int {
if (input.isNullOrBlank()) return 0
return Seperator().parse(input).sum()
}
}
36 changes: 36 additions & 0 deletions src/main/kotlin/stringCaculator/domain/Seperator.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package stringCaculator.domain

class Seperator {

fun parse(text: String): List<Int> {
val result = parseDelimiter(text)
checkMinusInt(result)
return result.map { it.toInt() }.toList()
}

private fun parseDelimiter(text: String): List<String> {
return if (hasCustomDelimeter(text)) {
customDelimiter(text)
} else {
defaultDelimiter(text)
}
}

private fun checkMinusInt(input: List<String>) {
require(input.all { it.toInt() >= 0 }) { throw IllegalArgumentException(ExceptionCode.NOT_ALLOWED_MINUS.getMessage()) }
}

private fun defaultDelimiter(text: String): List<String> {
return text.split(",|:".toRegex())
}

private fun customDelimiter(text: String): List<String> {
val result = Regex("//(.)\n(.*)").find(text)
val customDelimiter = result!!.groupValues[1]
return result.groupValues[2].split(customDelimiter)
}

private fun hasCustomDelimeter(text: String): Boolean {
return Regex("//(.)\n(.*)").find(text) != null
}
}
11 changes: 11 additions & 0 deletions src/main/kotlin/stringCaculator/enums/ExceptionCode.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@

enum class ExceptionCode(
private val message: String,
) {
NOT_ALLOWED_MINUS("input에 음수가 있으면 안됩니다"),
;

fun getMessage(): String {
return this.message
}
}
9 changes: 9 additions & 0 deletions src/main/kotlin/stringCaculator/view/InputView.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package stringCaculator.view

class InputView {
private val guideInputMsg = "값을 입력해주세요"
fun input(): String? {
println(guideInputMsg)
return readln()
}
}
7 changes: 7 additions & 0 deletions src/main/kotlin/stringCaculator/view/OutputView.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package stringCaculator.view

class OutputView {
fun printConsole(text: Int) {
println(text)
}
}
Empty file removed src/test/kotlin/.gitkeep
Empty file.
89 changes: 89 additions & 0 deletions src/test/kotlin/lotto/common/InputValidationTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package lotto.common

import org.assertj.core.api.Assertions
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.NullAndEmptySource
import org.junit.jupiter.params.provider.ValueSource

class InputValidationTest {
private val inputValidation = InputValidation()

@DisplayName("구입금액에 null값이 들어오면 에러를 낸다")
@ParameterizedTest
@NullAndEmptySource
fun amountIsNotAllowedNull(input: String?) {
Assertions.assertThatThrownBy {
inputValidation.amountValidate(input)
}.isInstanceOf(ExceptionCode.NotAllowNullOrBlank::class.java)
}

@ParameterizedTest
@DisplayName("구입금액에 빈값이 들어오면 에러를 낸다")
@ValueSource(strings = ["", " "])
fun amountIsNotAllowedEmpty(input: String?) {
Assertions.assertThatThrownBy {
inputValidation.amountValidate(input)
}.isInstanceOf(ExceptionCode.NotAllowNullOrBlank::class.java)
}

@ParameterizedTest
@DisplayName("구입금액에 숫자가 아니면 에러를 낸다")
@ValueSource(strings = ["test", "!123", "zz1+"])
fun amountIsNotMatchNumeric(input: String) {
Assertions.assertThatThrownBy {
inputValidation.amountValidate(input)
}.isInstanceOf(ExceptionCode.NotMatchNumeric::class.java)
}

@ParameterizedTest
@DisplayName("구입금액에 숫자 string가 들어오면 int로 바꿔준다")
@ValueSource(strings = ["123", "11", "33"])
fun amountStringToDigit(input: String) {
Assertions.assertThat(inputValidation.amountValidate(input)).isEqualTo(input.toInt())
}

@DisplayName("당첨번호에 null값이 들어오면 에러를 낸다")
@ParameterizedTest
@NullAndEmptySource
fun winLotteryIsNotAllowedNull(input: String?) {
Assertions.assertThatThrownBy {
inputValidation.winLotteryValidation(input)
}.isInstanceOf(ExceptionCode.NotAllowNullOrBlank::class.java)
}

@ParameterizedTest
@DisplayName("당첨번호에 빈값이 들어오면 에러를 낸다")
@ValueSource(strings = ["", " "])
fun winLotteryIsNotAllowedEmpty(input: String?) {
Assertions.assertThatThrownBy {
inputValidation.winLotteryValidation(input)
}.isInstanceOf(ExceptionCode.NotAllowNullOrBlank::class.java)
}

@ParameterizedTest
@DisplayName("당첨번호에 , 구분자가 없으면 에러를 낸다")
@ValueSource(strings = ["1;2;3;4", "123451234", "1234512345"])
fun winLotteryIsNotFindSeparator(input: String) {
Assertions.assertThatThrownBy {
inputValidation.winLotteryValidation(input)
}.isInstanceOf(ExceptionCode.NotFindSeparator::class.java)
}

@ParameterizedTest
@DisplayName("당첨번호가 6자리가 아니면 에러를 낸다")
@ValueSource(strings = ["1,2,3,4", "5,6,7,8", "10,11"])
fun winLotteryIsNotSixNum(input: String) {
Assertions.assertThatThrownBy {
inputValidation.winLotteryValidation(input)
}.isInstanceOf(ExceptionCode.NotWinLotteryCount::class.java)
}

@ParameterizedTest
@DisplayName("당첨번호가 , 구분자로 split한다")
@ValueSource(strings = ["1,2,3,4,5,6", "5,6,7,8,9,10"])
fun winLotteryStringToDigit(input: String) {
Assertions.assertThat(inputValidation.winLotteryValidation(input))
.isEqualTo(input.split(",").map { it.toInt() })
}
}
30 changes: 30 additions & 0 deletions src/test/kotlin/lotto/domain/DrawTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package lotto.domain

import lotto.entity.Lottery
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.ValueSource

class DrawTest {
private val draw = Draw()
private val lotteryPrice = 1000

@DisplayName(value = "구입금액으로 산 갯수를 계산한다")
@ParameterizedTest
@ValueSource(ints = [1000, 100000, 1000000])
fun calculateBuyNum(num: Int) {
assertThat(draw.calculateBuyNum(num)).isEqualTo(num / lotteryPrice)
}

@DisplayName(value = "로또 당첨 갯수를 계산한다")
@Test
fun calculateWin() {
val winNumbers = listOf(1, 2, 3, 4, 5, 6)
val lotteries = listOf(Lottery(listOf(1, 2, 3, 11, 12, 13)), Lottery(listOf(1, 2, 3, 4, 5, 6)))
val winLotteryResult = draw.calculateWin(winNumbers, lotteries)
assertThat(winLotteryResult.matchThree.matchNum).isEqualTo(1)
assertThat(winLotteryResult.matchSix.matchNum).isEqualTo(1)
}
}
Loading