어제부터 만들기 시작하긴 했지만 구조를 잡고 시작하기에 고민을 좀 하느라 시간이 질질 끌린 감이 있다. 앱에선 나름 큰 서비스라고 나눌만한 부분이 더 잘 보였지만 숫자 야구 게임처럼 작은 부분이 오히려 억지로 구조를 늘려서 가독성을 떨어트리는 게 아닐까? 생각이 들어 한 번 더 생각하게 되는 것 같다.
그래도 튜터님의 해설 과정에서 나오는 패키지 구조나 이전에 해오던 방식을 이리저리 생각해서 내 스타일대로 하는 방법이 나오긴 했고 차근차근 기능을 하나씩 완성해 나가고 있다.
그리고 Git convention도 지키려고 일단은 노력하고 있다. 원래 한 파일에서 작성 많이 하고 부분 커밋을 많이 하던지라 여기서도 고민이 은근 나오는 것 같다.
출력부는 Main에서만 처리하기로 했다. 앱은 MVC 패턴 기준으로 봤을 때 구성을 나누기 쉬웠지만 쌩 클래스와 함수에서 출발하는 패키지 구조는 워낙 오랜만이기도 하고 아키텍쳐 패턴 이해도가 낮은 나로서는 출력부를 클래스에 넘기지 않는다는 것 이상으로 상상력을 끌어내기는 좀 어려웠다. 진짜 MVC처럼 하려면 Main 에서 Print 부분을 재차 분리해야 할텐데 단순 Print를 View처럼 진짜로 대하자니 영 느낌이 안와서 그냥 같이 두기로 했다.
그리고 NumberBaseballGame을 Model이라고 부르기엔 함수가 어째 컨트롤러 느낌도 좀 남아있는 것 같은데 이런 것도 나중에 리뷰가 필요할 것 같다. 사실 내가 짜던 모델의 느낌은 enum과 data class로 구분된 밑 3개의 클래스가 더 친숙하다.
여담으로 MVVM 패턴을 적용하려고 앱에서도 시도해본 적은 있지만 혼자 공부해서 그런지 MVVM 으로 구조만 짜여진 MVC 같다는 리뷰를 받은 적이 있어 아키텍쳐 패턴의 이해도는 아직도 MVC를 벗어나지 못한 것 같다. 이번엔 GPT한테 조금씩 물어보면서 했는데 기본적인 틀은 MVC 구조를 따라갔다고 표현은 할 수 있어 보인다.
Observer, Singleton 같은 디자인 패턴까진 어찌저찌 써보겠지만 MVC, MVP, MVVM 같은 아키텍쳐 패턴은 잘 설명된 레퍼런스를 봐도 구조에 맞춰 내 손으로 응용해보지 않으면 멋대로 해석하는 경향이 아직 강한 것 같다. 실무할 때 사수님이 없어서 계속 아쉬웠던 점중 하나다.
fun getCurrentGameCount(completion: (NumberBaseballCount) -> Unit) {
completion(NumberBaseballCount(this.strike, this.ball))
}
볼 카운트를 가져올 때는 정답도 Nothing도 아닐 때 뿐이라 모델에서 갖고 있던 값을 불러오기로 했는데 여기서 NumberBaseballCount
data class를 써먹었다. 결국은 이게 Strike 인지 Ball 인지 단박에 알아먹을 방법이 필요해서 작성했고 명시적이라 Main 에서 쓸 때 구분하기 좋아 마음에 들었다.
completion handler는 Swift에서 즐겨 쓰던 방식이라 데이터도 다양하게 넘겨주려고 일부러 작성했다.
fun checkInput(input: String): NumberBaseballGameStatus {
val inputCharList = Regex("""\d""").findAll(input).map { it.value.single() }.toList()
val answerLength = this.answer.length
if (inputCharList.count() != answerLength) { throw Exception("입력값이 올바르지 않습니다.") }
if (inputCharList.first() == '0') { throw Exception("첫 번째 숫자는 0이 될 수 없습니다.") }
if (inputCharList.distinct().count() < answerLength) { throw Exception("중복 값이 있습니다.") }
this.resetGameCount()
(0 until answerLength).forEach { index ->
if (this.answer[index] == inputCharList[index]) { this.strike++ }
else if (this.answer.contains(inputCharList[index])) { this.ball++ }
}
return this.validateAnswer()
}
사용자가 입력을 1 2 3 처럼 띄어썼을 수도 있으니 정규표현식으로 날리고 (그런데 정규표현식 아니어도 숫자만 저장할 방법은 있었을 것 같다) 예외처리도 하고 그냥 자릿수에 맞춰서 볼 카운트 관리를 해주는 방식을 취했다. 반환도 NumberBaseballGameStatus을 썼는데 밑에 서술할 validateAnswer 와 확실한 구분을 지을만한 건덕지가 안보인게 조금 아쉽다.
이 때 가장 고민이었던 건 볼 카운트를 모델이 계속 쥐고 있을 것인가 매개변수로 주고 받을 것인가였는데 매개변수를 쓰면 가독성이 확 박살날 느낌이라 그냥 쥐고 있기로 했다.
private fun validateAnswer(): NumberBaseballGameStatus {
return when {
this.strike == this.answer.length -> NumberBaseballGameStatus.Correct
this.strike > 0 || this.ball > 0 -> NumberBaseballGameStatus.Progress
else -> NumberBaseballGameStatus.Nothing
}
}
입력 값을 검증하고 나면 모델에서 갖고 있는 정보로 게임 상태를 계속 반환하는 함수로 쓰고 있다. 기왕 게임 상태 반환인거 private 함수 말고 조금 개방적으로 쓸 수 있게 설계했으면 좋았을 것 같기도 한데 Main 에서 추가로 필요할 일이 없는 것 같다.
private fun resetAnswer() {
this.answer = ""
while (this.answer.length < this.difficulty.length) {
// 첫번째 값은 0을 범위로 쓰지 않음
val randomNumber = ((if (this.answer.isEmpty()) 1 else 0)..9).random().toString().single()
if (!this.answer.contains(randomNumber)) this.answer += randomNumber
}
}
여기서 난이도에 따른 범위 조정을 위해 NumberBaseballDifficulty를 만들어 사용한건데 그럭저럭 나쁘지 않은 시도같다.
contains를 통한 체크 말고 map을 미리 만들어놓고 value를 Boolean 으로 관리할까도 생각했지만 그게 더 가독성이 떨어질 것 같아 contains로 밀고 나갔다.
var difficulty: NumberBaseballDifficulty = NumberBaseballDifficulty.Normal
set(value) {
field = value
this.resetGame()
}
Main에서 난이도를 조정하면 게임 상태를 초기화하는 Setter로 시점도 명확하게 관리했다. 초기화 시점이 난이도에 달려있는게 통상적으로 맞나? 싶지만 현재 게임 진행상 반드시 거쳐가는 부분이니 나쁘지 않을 것 같다.
그 외에 남길만한 내용들은 Main 에서 While로 메뉴 선택지를 계속 반복시키는 데 자꾸 블록을 엉망으로 잡아서 이전 단계로 돌아갈 때마다 무한 반복되는 경우를 잡느라 시간을 허비한 것도 있었다.
지난번에 기본적인 Git 명령어는 다 배웠지만 협업 과정에서 주로 쓰는 Branch, PR같은 건 배우지 않아서 이번에 2차로 특강이 진행됐다.
- git checkout 으로 브랜치를 이동했었지만 최근에 git switch가 추가됐다.
git checkout은 특정 커밋으로 이동도 가능한만큼 switch는 branch 이동에만 해당하는 명령어다.- Github Settings 에서 Default branch를 dev 같이 배포가 아닌 Branch로 설정하는게 사고 방지에 좋다.
- PR 코멘트는 리뷰 단위로 +를 눌러서 작성해주는게 좋다.
- Pending 상태의 코멘트가 달리면 해결한다.
- 승인 안된 리뷰가 있을 때 못 합치게 하려면 Rules를 따로 설정해야 한다.
- 올바른 협업을 위해 Head는 꾸준히 Pull로 맞춰줘야 한다.
- Open 한 상태에서 리뷰를 반영해 수정하면 Github 상에도 자동 업데이트 된다.
- gitignore.io 에서 기본적인 ignore를 관리하는게 조금 더 좋을 듯 싶다.
회사에서도 리뷰 진행하는 건 엿봤으니 PR의 개념은 알지만 Github이 PR에서 얼마만큼의 편의성을 제공하는지는 오늘 강의로 알게된 부분이 많았다. 슬랙과 연동이나 이메일 수시로 보내기같은 기능은 알아서 나중에 찾아 써먹어봐야겠다.
정수 배열 numbers가 주어집니다. numbers에서 서로 다른 인덱스에 있는 두 개의 수를 뽑아 더해서 만들 수 있는 모든 수를 배열에 오름차순으로 담아 return 하도록 solution 함수를 완성해주세요.
fun solution(numbers: IntArray): IntArray {
var answer = sortedSetOf<Int>()
numbers.count().let { count ->
for (i in 0 until count) {
if (i > count - 1) break
for (j in i + 1 until count) {
answer.add(numbers[i] + numbers[j])
}
}
}
return answer.toIntArray()
}
퍼포먼스 측면에서도 꽤 빨랐고 SortedSet
을 사용한 점도 복잡하게 distinct, sorted를 사용할 필요가 없어 좋았다고 생각이 든다. 완전 탐색 방식 말고 순회 횟수를 줄일 방법이 없나 다른 풀이를 참고해봤지만 더 줄일 방법은 없어보였다.
withIndex
나 foreachIndexed
를 써서 순회했다면 조금 더 멋있지 않을까 싶긴 한데 until로 범위를 지정한 for문 두개도 썩 나쁘지만은 않으니 지금에 만족하기로 했다.