두번째 미션부터는 2주를 투자하게 되었다.
나는 리뷰어님께서 목요일 저녁에 머지를 해주셔서..
금요일부터 월요일까지 더 깊게 공부해볼까 했지만, 엄마 생신이셔서 마침 시간이 나니까 그냥 광주에 다녀왔다.
뭔가 배운게 많은 것 같기도, 적은 것 같기도 한, 로또 미션 피드백 가보자고 !
1단계
2단계
기타
2단계에서는 다음과 같은 추가 요구사항이 주어졌다.
예외 처리를 통해 에러가 발생하지 않도록 한다.
참고로 코틀린에서는 아래와 같이 예외 처리를 한다. 장기적으로는 아래와 같이 예외 처리하는 걸 연습해 본다.
- 논리적인 오류일 때만 예외를 던진다.
- 논리적인 오류가 아니면 예외를 던지지 말고 null을 반환한다.
- 실패하는 경우가 복잡해서 null로 처리할 수 없으면 sealed class를 반환한다.
- 일반적인 코틀린 코드에서 try-catch를 사용하지 않는다.
그에 따라 리뷰어님도 나에게 다음과 같은 리뷰를 남겨주셨다.
private fun readInputMoney(): Int {
return try {
OutputView.printInputMoneyPrompt()
InputView.readInputMoney()
} catch (e: IllegalArgumentException) {
println("[ERROR] ${e.message}")
readInputMoney()
}
}
코틀린에서 예외를 처리하는 방법은 크게 두 종류가 있습니다.
- 오류가 예상되는 경우
-> null 또는 기본값을 리턴- 오류가 예상되지 않는 경우
-> Exception을 던지고 이 Exception을 개발자에게 알려서 문제를 빠르게 파악하도록 한다.
Exception은 반드시 예외적인 상황에서만 사용하는 것이 좋습니다.
위 경우는 어떤 경우일까요?
나의 대답은 오류가 예상되는 경우였다.
잘못된 입력이 들어올 것이라고 내가 예상하였기 때문이다.
이런 경우에는 오류를 던지지 말고 null로 간주하고 프로그램을 종료하도록 했다.
만약 유효한 값을 입력할 때까지 계속해서 반복하고 싶다면, 입력된 값이 null인 경우 함수를 다시 호출하도록 구현할 수 있다.
private fun readInputMoney(): Money {
OutputView.printInputMoneyPrompt()
val input = InputView.readInputMoney()
Money.validateInputMoney(input)
return Money(input.toInt())
}
return try {
values().last { rank -> rank.countOfMatch == countOfMatch }
// values.lastOrNull { rank -> rank.countOfMatch == countOfMatch } ?: throw IllegalException
} catch (e: IllegalException) {
MISS
}
try-catch를 사용하는 것도 좋지만, OrNull 계열의 함수를 사용하는 방법도 있습니다.
엘비스 연산자(?:)를 활용하면 간단하게 null 대신 기본값을 리턴할 수 있습니다.
한번에 정리하고 싶어서 코틀린 인 액션을 그냥 읽었다. 6-1. 널 가능성 참고
let은 mutable하면서 nullable한 경우에만 사용하는 것을 권장하며, 단순히 null을 회피하기 위해 사용하는 것은 권장하지 않습니다. 관련 자료
InputView는 "입력"에 관련된 책임을 가질 뿐, "변환"에 대한 책임을 가지지 않습니다.
그렇다면, 변환은 어디서 해야할까요? 🤔 (View와 Domain 둘 다 접근이 가능한 곳)
InputView에서는 입력된 값만 전달해주고, 실질적인 값에 대한 유효성 체크는 관련 domain에서 하는 것이 좋습니다.
만약 요구사항이 변경되어 domain이 변경된다고 가정해봅시다.
InputView에서 "변환"과 "검증"까지 다 하게 되는 구조라면, 요구사항 변경 시 view와 domain 둘 다 변경을 해야 합니다.
심지어는 controller도 변경할 가능성도 높아집니다.
해서, domain에서 검증을 하도록 하면, 요구사항이 변경되어 domain이 변경된다 하더라도 domain만 변경하면 됩니다. 😄
정리해보자면,
InputView
입력을 InputView에서 받는 경우, InputView는 입력에 대한 책임만을 갖는다.
Domain
입력값을 받아왔다면, 실질적인 값에 대한 유효성 검증은 해당 domain에서 하는 것이 바람직하다.
tip. 부생성자나 팩토리 메서드를 활용하면 좋다.
Controller
변환은 View와 Domain이 모두 접근 가능한 곳인 controller에서 이루어지는 것이 바람직하다.
class UserLotto {
val count: Int
private val lottoNumbers: List<Lotto>
constructor(count: Int) {
this.count = count
this.lottoNumbers = listOf()
}
constructor(count: Int, lottoNumbers: List<Lotto>) {
this.count = count
this.lottoNumbers = lottoNumbers
}
}
코틀린에서는 default value를 지원합니다.
default value를 활용하면, 부생성자를 없앨 수 있어요.class userLotto( val count: Int, private val lottoNumbers: List<Lotto> = emptyList() )
함수명 명명 규칙
테스트 함수를 작성할 때, 내용과 함수명이 일치하게 작성하도록 한다.
함수명에는 구입 금액이라고 명시되어 있지만, 내부 로직은 개수를 입력하도록 되어있네요.
테스트하고자 하는 것과 함수명을 일치시켜보면 어떨까요?
함수명만 봤을 때에는 마치 숫자1부터 45까지를 테스트하는 것으로 보이나, 실질적으로는 범위 외의 숫자를 입력한 결괏값을 테스트하고 있네요.
이 부분도 마찬가지로 테스트하고자 하는 것과 함수명을 일치시켜보면 어떨까요?
domain에서 Controller 접근 금지
domain에서 Controller에 접근하고 있네요.
doomain은 어떤 것과도 의존성을 가지지 않도록 코드를 개선해보면 어떨까요?
!! 함부로 사용 금지
(readInputMoney().value?.div(Money.MONEY_UNIT))이 null이 아님을 보장할 수 있을까요?
대체로 커스텀 함수보다는 시스템 함수가 가독성이 더 좋다
fun readInputManualLottoCount() = readInputNumber()
fun readInputBonusNumber() = readInputNumber()
private fun readInputNumber(): String {
return readln()
}
대체로 커스텀 함수보다는 시스템 함수가 더 가독성이 좋습니다.
단순히 함수 내부에서 시스템 함수만 호출한다면, 시스템 함수를 호출하는 것을 권장합니다.
괜히 커스텀 함수를 사용하기보다 겹치는 코드가 많아도 가독성을 위해 일일이 작성하도록 하자.
생각하고 고민하는 과정은 GOOD but 과도한 걱정은 BAD
🐯 : 리팩터링하는 부분에서 더 코드가 복잡해진 것 같은데.. 조언해주시면 감사하겠습니다.!
🤔 : 어떤 코드가 복잡해진것 같으신가요?
🐯 : 뭔가.. 제가 확신이 없어서 그런지 코드를 복잡하다고 느끼는 것 같습니다.. 😥
코드를 수정하다보니, Controller 에서 너무 많은 일을 담당하고 있는 것 같다는 생각도 들었습니다.
🤔 : 충분히 잘 하고 계셔서, 자신감을 가지시면 좋을 것 같아요!
Controller에서 너무 많은 일을 담당하고 있다는 점을 아시는 것만으로도 대단한 것 같아요!
Controller가 많은 일을 하는 점은 MVC 패턴의 단점이기도 합니다. 😄
아마.. 내가 가장 좋아할 것 같은 페어 베리와 함께한 2주가 막을 내렸다. (보고 싶을거야 😥)
베리와 정말 잘 맞아서 1단계도 수월하게 끝냈고,
커트 리뷰어는 요구사항에서 크게 벗어나는 피드백은 주시지 않아서 여유로운 미션을 진행할 수 있었다.
오히려 수업을 전혀 못따라가서 빨리 정리해야하는데.. ㅜㅜ 생각만해도 멀었다..
언제 다 정리하지..
내일부터는 블랙잭 미션.. 새로운 페어가 누구일지 기대된다. 🤗