[3 주차 코드 회고] 천외천, 산 넘어 산
“Nobody wants to show you the hours and hours of becoming.
They'd rather show the highlight of what they've become.”
by Anela Duckworth, grit
정말 이번 과제를 하면서 정말 주구장창 고민했던 부분들은 다음과 같다........ 정말 이런 부분들을 같이 해결하고 싶은....ㅠㅠ
1. 패키지 분리 기준 + 이름
2. 클래쓰 분리 기준 + 이름
3. 테스트 케이스 안 빼먹고 추가하기 방법
4. 테스트 케이스가 통과 안 할 때 통과하게 코드 변경하는 법 + 테스트 코드 이해하기 (runexception, throwerror 등)
5. 구글링 했을 때 원하는 답이 안 나올 때 문제 해결 법.....
6. 리드미 작성은 어떻게 해야하는가? (도저히 기능별로 리드미를 미리 작성하고, 함수 단위로 세부적으로 미리 나누는 게 어렵습니다ㅠㅠ)
7. 처음에 구조를 짤 때 어떻게 짜야하는지? 처음부터 꼼꼼하게 짜는 방법 + 변경 사항이 생겼을 때 이를 잘 반영하는 방법
그리고 추가적으로 학습한 내용 정리!!
fun printWinnings() {
println(StringResource.FIFTH.resource.replace(
StringResource.UNDERBAR.resource, this.winningCounter.getValue("3").toString()))
println(StringResource.FOURTH.resource.replace(
StringResource.UNDERBAR.resource, this.winningCounter.getValue("4").toString()))
println(StringResource.THIRD.resource.replace(
StringResource.UNDERBAR.resource, this.winningCounter.getValue("5_0").toString()))
println(StringResource.SECOND.resource.replace(
StringResource.UNDERBAR.resource, this.winningCounter.getValue("5_1").toString()))
println(StringResource.FIRST.resource.replace(
StringResource.UNDERBAR.resource, this.winningCounter.getValue("6").toString()))
}
이번 회고도 어김없이 다른 분들의 코드를 보며 시작!
그 중에서도 주로 참고한 분은 다음과 같다.
https://github.com/woowacourse-precourse/kotlin-lotto/pull/1
[로또] 신동원 미션 제출합니다. by ponopono0322 · Pull Request #1 · woowacourse-precourse/kotlin-lotto
로또 게임 3주차 미션 제출
github.com
1. enum 클라쓰를 처음 써보면서 막막했던 부분들 + 하드 코딩을 피하기 위해 미리 String을 만들 때 유동적으로 변경되어야 하는 텍스트 부분을 변경하기 위한 방법을 찾지 못하고 있었다.
참고했던 코드를 보면, 이 유동적으로 해결하기 위해서 임의의 변경 String을 만들었고 (UNDERBAR) 이를 replace함수를 통해 변경하는 방식으로 유동적으로 변경을 진행하셨다.
fun printWinnings() {
println(StringResource.FIFTH.resource.replace(
StringResource.UNDERBAR.resource, this.winningCounter.getValue("3").toString()))
println(StringResource.FOURTH.resource.replace(
StringResource.UNDERBAR.resource, this.winningCounter.getValue("4").toString()))
println(StringResource.THIRD.resource.replace(
StringResource.UNDERBAR.resource, this.winningCounter.getValue("5_0").toString()))
println(StringResource.SECOND.resource.replace(
StringResource.UNDERBAR.resource, this.winningCounter.getValue("5_1").toString()))
println(StringResource.FIRST.resource.replace(
StringResource.UNDERBAR.resource, this.winningCounter.getValue("6").toString()))
}
package data
import java.text.DecimalFormat
enum class StringResource(val resource: String) {
PURCHASE("구입 금액을 입력해주세요."),
LOTTO("당첨 번호를 입력해 주세요."),
BONUS("보너스 번호를 입력해 주세요."),
NUMBERERROR("[ERROR] 숫자가 아닙니다."),
REMAINDER("[ERROR] ${Price.STANDARD.price}원 단위로 입력하세요."),
DUPLICATE("[ERROR] 중복된 숫자를 입력할 수 없습니다."),
RANGEERROR("[ERROR] ${NumberRange.START.number}와 ${NumberRange.END.number} 사이의 숫자여야 합니다."),
LENGTHERROR("[ERROR] 로또 번호는 ${NumberRange.MAX.number}개가 필요합니다."),
TICKETS("_개를 구매했습니다."),
FIRST("6개 일치 (${DecimalFormat("#,###").format(WinningAmount.FIRST.amount)}원) - _개"),
SECOND("5개 일치, 보너스 볼 일치 (${DecimalFormat("#,###").format(WinningAmount.SECOND.amount)}원) - _개"),
THIRD("5개 일치 (${DecimalFormat("#,###").format(WinningAmount.THIRD.amount)}원) - _개"),
FOURTH("4개 일치 (${DecimalFormat("#,###").format(WinningAmount.FOURTH.amount)}원) - _개"),
FIFTH("3개 일치 (${DecimalFormat("#,###").format(WinningAmount.FIFTH.amount)}원) - _개"),
UNDERBAR("_"),
RATIO("총 수익률은 _%입니다.")
}
2. 리드미도 인상깊었는데, 리드미 작성 과정에서 고민했던 부분이 단위 테스트 목록 별로 나눠서 검증하는 것과 전체 함수들을 다 디테일하게 나열하는 것 중에서 무엇이 맞는지 고민을 많이했는데, 이 분의 경우 아예 다 깔끔하게 작성하는 것을 보여주셨다.
여기서 또 인상깊었던 것은 취소 줄! ~~ "text" ~~ 의 형태로 ~~를 활용하면 취소줄이 생기는 걸 처음 알았다!! 아주 유용한 기능일듯!
3. exception 부분을 해결하기 위한 노력들이 필요하다!
4. val과 const의 차이가 뭔가?
const는 컴파일 시간 상수입니다. 런타임에 수행 할 수있는 val과 달리 컴파일 시간 동안 값을 할당해야 함을 의미합니다. 즉, const는 함수 또는 클래스 생성자에 할당 할 수 없으며 문자열 또는 기본에만 할당 할 수 있습니다.
출처: https://myung6024.tistory.com/144
const val foo = complexFunctionCall() //Not okay
val fooVal = complexFunctionCall() //Okay
const val bar = "Hello world" //Also okay
5. string 다루기
https://www.androidhuman.com/2016-12-30-kotlin_string
코틀린으로 문자열(String) 쉽게 다루기
#Android, #Kotlin, and #Tesla
www.androidhuman.com
joinToString
trimToString
6. 정규식 다루기
https://yoon-dailylife.tistory.com/113
Kotlin) 정규 표현식 정리
정규 표현식 정규 표현식 또는 정규식은 특정한 규칙을 가진 문자열의 집합을 표현하기 위해 사용하는 형식 언어. 어떤 문자열에서 특정한 조건의 문자열을 찾고 싶을 때, 그 조건이 복잡한 경
yoon-dailylife.tistory.com
우선, 인풋을 체크하기 위한 정규식(regex)와 require 사용
참고 레퍼런스: https://github.com/woowacourse-precourse/kotlin-baseball/pull/86/
[숫자 야구 게임] 이해찬 미션 제출합니다. by dlgocks1 · Pull Request #86 · woowacourse-precourse/kotlin-basebal
숫자 야구 게임 이해찬 미션 제출합니다!!
github.com
package baseball
class InputChecker {
fun checkEndedNumber(userInput: String) {
val regex = Regex("[1-2]{1}") // 앞에 [#-#]이 regex에서 참고하려는 숫자의 범위이고, {#}는 참고하려는 숫자 개수
require(userInput.matches(regex)) { // require의 경우 자동으로 error코드 말함, userInput.matches(regex)로 일치하는지 체크
"1또는 2를 입력해 주세요."
}
}
fun checkBasballNumber(userNum: String) {
val regex = Regex("[0-9]{3}")
require(userNum.matches(regex)) {
"$userNum 3자리가 아니거나, 숫자가 아닌 값"
}
require(userNum.toCharArray().distinct().size == 3) { // 서로 다른지 수 + 크기 체크!
"$userNum 각각 다른 3자리 숫자가 아님"
}
}
}
7. 비즈니스/도메인 로직이란?
https://velog.io/@eddy_song/domain-logic
비즈니스 로직, 도메인 로직이 도대체 뭐지?
🙄 내 앱은 아직 비즈니스가 아닌데요...?
velog.io
송금 기능을 담당하는 코드가 있다.
이 코드를 자세히 뜯어보면 다음과 같은 일을 하는 코드로 이뤄져있다.
- 계좌 잔액이 충분한지 확인한다.
- 유효하다면 송금 버튼을 활성화하고, 유효하지 않다면 에러 메시지를 띄운다.
- 사용자의 멤버십 등급에 맞춰서 송금 수수료를 계산한다.
- 송금 수수료를 결제하도록 외부 결제 서비스에 요청한다.
- 사용자의 잔액을 감소시킨다.
- 사용자의 잔액을 DB에 저장한다.
이 코드들을 구분할 때, 해당 소프트웨어가 '송금'이라는 현실 문제에 대한 의사결정을 하는가를 생각해보자.
어떤 것이 도메인 로직이고, 어떤 것이 서비스 로직일까?
도메인 로직에 해당하는 것은 다음과 같다.
이 코드들은 '송금'에 대한 의사결정을 담당하고 있다.
- 계좌 잔액이 충분한지 확인 -> 송금이 가능한지에 대한 의사결정
- 송금 수수료를 계산 -> 송금에 드는 비용을 정책에 따라서 결정
- 사용자의 잔액을 감소시킨다 -> 송금이라는 서비스를 수행
어플리케이션 서비스 로직에 해당하는 것은 다음과 같다.
이 코드들은 도메인 로직이 의사결정을 할 수 있도록 입력을 제공하며, 결과를 외부 서비스/DB/UI에 업데이트하는 역할을 맡는다.
- 유효하지 않으면 에러 메시지를 띄운다 -> UI
- 송금 수수료를 결제하도록 외부 결제 서비스에 요청한다. -> 외부 서비스와의 네트워킹
- 잔액을 DB에 저장한다. -> Persistence
만약 어떤 코드가 명확하게 도메인 로직인지 아닌지 애매하다면, 해당 코드가 하는 일을 쪼개야 한다는 신호다. 도메인 로직이 분명한 부분과 서비스 로직이 섞여 있을 수도 있다.
현재 가지고 있는 원화가 미국 달러로 얼마인지 보여주는 함수가 있다고 해보자.
이 함수 하나만 가지고 생각하면 도메인 로직인지 아닌지 애매하다.
하지만 함수를 분리하고 코드를 쪼개보면 좀 더 명확해진다.
- 미국 달러 환율을 얼마로 할 것인가. 계산 공식은 얼마로 할 것인지 결정하는 코드. -> 도메인 로직
- 단위와 화폐 포맷을 바꿔서 UI에 업데이트를 하는 코드. -> 어플리케이션 서비스 로직
과제를 하면서 궁금했던 점들 해결하기
1. const val로 해놓은 message들을 어떻게 동적으로 변화를 줄 것인가?!
답은 %s로 주고 .format(주려는 숫자) 이렇게 하면 되는 것이었다!!
const val TOTAL_YIELD_RATE_MESSAGE = "총 수익률은 %s입니다."
print(TOTAL_YIELD_RATE_MESSAGE.format(earned.calculateYieldRate(investment)))
2. 잘하시는 분들을 보면 확장성을 고려해 상속 구조를 잘 쓰신다.
이 부분을 나도 활용할 수 있을지 체크해봐야한다.
2-1. 상속을 enum + error message print할 때 쓰기
enum class ErrorMaking {
OUT_OF_RANGE {
override fun getErrorMessage() = "숫자가 범위를 벗어났습니다."
},
NUMBER_DUPLICATED {
override fun getErrorMessage() = "중복되는 숫자가 존재합니다."
};
abstract fun getErrorMessage(): String
companion object {
fun makeError(errorMaking: ErrorMaking) {
throw IllegalArgumentException("[ERROR] ${errorMaking.getErrorMessage()}")
}
}
}
fun checkWinningsFormat(value: String?) {
try {
value!!.split(SEPARATOR).forEach { n -> n.trim().toInt() }
} catch (e: Exception) {
makeError(ErrorMaking.NUMBER_FORMAT_INCORRECT)
}
}