240430 프로그래밍 심화 - 숫자 야구 만들기

노재원·2024년 4월 30일
0

내일배움캠프

목록 보기
29/90

어제부터 만들기 시작하긴 했지만 구조를 잡고 시작하기에 고민을 좀 하느라 시간이 질질 끌린 감이 있다. 앱에선 나름 큰 서비스라고 나눌만한 부분이 더 잘 보였지만 숫자 야구 게임처럼 작은 부분이 오히려 억지로 구조를 늘려서 가독성을 떨어트리는 게 아닐까? 생각이 들어 한 번 더 생각하게 되는 것 같다.

그래도 튜터님의 해설 과정에서 나오는 패키지 구조나 이전에 해오던 방식을 이리저리 생각해서 내 스타일대로 하는 방법이 나오긴 했고 차근차근 기능을 하나씩 완성해 나가고 있다.

그리고 Git convention도 지키려고 일단은 노력하고 있다. 원래 한 파일에서 작성 많이 하고 부분 커밋을 많이 하던지라 여기서도 고민이 은근 나오는 것 같다.

구조 정하기

  • Main (뷰, 컨트롤러, 출력부 담당)
  • NumberBaseballGame (게임 모델)
    • enum: NumberBaseballGameStatus
      • 게임 진행 상태 구분하려고 만든 enum class (Nothing, Progress, Correct)
    • enum: NumberBaseballDifficulty(var length)
      • 난이도와 난이도에 해당하는 정답 길이를 저장한 enum class (Normal, Hard, VeryHard)
    • data: NumberBaseballCount(val strike: Int, val ball: Int)
      • strike, ball을 Pair<Int, Int>로 넘겨주기 싫어서 typealias 를 썼다가 이것도 별로라 묶어서 반환하기 위해 만든 data class

출력부는 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로 밀고 나갔다.

난이도 조정 Setter

var difficulty: NumberBaseballDifficulty = NumberBaseballDifficulty.Normal
        set(value) {
            field = value
            this.resetGame()
        }

Main에서 난이도를 조정하면 게임 상태를 초기화하는 Setter로 시점도 명확하게 관리했다. 초기화 시점이 난이도에 달려있는게 통상적으로 맞나? 싶지만 현재 게임 진행상 반드시 거쳐가는 부분이니 나쁘지 않을 것 같다.



그 외에 남길만한 내용들은 Main 에서 While로 메뉴 선택지를 계속 반복시키는 데 자꾸 블록을 엉망으로 잡아서 이전 단계로 돌아갈 때마다 무한 반복되는 경우를 잡느라 시간을 허비한 것도 있었다.


Git 심화 특강

지난번에 기본적인 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를 사용할 필요가 없어 좋았다고 생각이 든다. 완전 탐색 방식 말고 순회 횟수를 줄일 방법이 없나 다른 풀이를 참고해봤지만 더 줄일 방법은 없어보였다.

withIndexforeachIndexed를 써서 순회했다면 조금 더 멋있지 않을까 싶긴 한데 until로 범위를 지정한 for문 두개도 썩 나쁘지만은 않으니 지금에 만족하기로 했다.

0개의 댓글