우아한테크코스 6기

우아한테크코스 6기(모바일 앱) 프리코스 3주차 공부 기록

Alsong 2023. 11. 10. 11:19

벌써 3주차군요! 지난 미션을 진행하는 동안의 시간이 느리게 느껴지기도 하고 빠르게 느껴지기도 하는 것 같아요. 그리고 벌써 프리코스가 3/4나 진행되었다니!! 벌써 프리코스도 한 주만 더 하면 끝나는군요.

이번에도 역시 3주차 미션과 함께 2주차 미션의 공통 피드백을 보내주셨습니다. 읽어보도록 하죠.

공통 피드백

1. 기능 목록을 업데이트한다.

기능 목록은 한 번 쓰고 마는 것이 아니라 계속해서 고쳐 나가야 하는 것입니다. 어떻게 보면 당연하지요. 문제를 보자마자 한 번에 완벽하게 기능구현 목록을 작성할 수 있는 사람이 어디 있겠어요! 저도 이번 미션에서는 코드 짜면서 README를 계속해서 업데이트하려고 노력했습니다.

 

2. 구현 순서도 코딩 컨벤션이다

'클래스는 프로퍼티, init 블록, 부 생성자, 메서드, 동반 객체 순으로 작성한다.' 라고 합니다. 그런데 동반 객체는 뭐지??라는 생각이 들어서 아래에서 자세히 알아보도록 했습니다.

 

3. 변수 이름에 자료형은 사용하지 않는다

2주차 미션을 하면서 변수의 타입을 변환해야 할 때가 있어서, 변환하기 전에는 변수 명에 Str, 변환하고 난 후에는 Int 붙이고 이랬었는데 좋지 않은 습관이군요. 어떻게 해야 할지 고민해 봐야겠습니다.

 

그리고 역시 이번에도 2주차에는 없었던 추가된 요구 사항이 생겼습니다. 한 번 읽어보겠습니다.

 

추가된 요구 사항

  • 함수(또는 메서드)의 길이가 15라인을 넘어가지 않도록 구현한다.
    • 함수(또는 메서드)가 한 가지 일만 잘하도록 구현한다.
  • else를 지양한다.
    • 힌트: if 조건절에서 값을 return하는 방식으로 구현하면 else를 사용하지 않아도 된다.
    • 때로는 if/else, when문을 사용하는 것이 더 깔끔해 보일 수 있다. 어느 경우에 쓰는 것이 적절할지 스스로 고민해 본다.
  • Enum 클래스를 적용해 프로그래밍을 구현한다.
  • 도메인 로직에 단위 테스트를 구현해야 한다. 단, UI(System.out, System.in, Scanner) 로직은 제외한다.
    • 핵심 로직을 구현하는 코드와 UI를 담당하는 로직을 분리해 구현한다.
    • 단위 테스트 작성이 익숙하지 않다면 test/kotlin/lotto/LottoTest를 참고하여 학습한 후 테스트를 구현한다.
    •  

하나하나 짚어보겠습니다!

 

1. 함수의 길이가 너무 길어도 안되는군요. 이는 각각의 함수가 한 가지의 일만 하도록 최대한 작게 쪼개라는 의미입니다. 공통 피드백 사항에도 있었죠! 하지만 함수의 길이가 15라인을 지키는 것이 쉽지는 않았습니다.. 몇 개는 20에 육박하기도 했군요..ㅠㅠ 하지만 오히려 너무 짧게 짜는 것이 가독성을 더 해치는 요인이 되는 경우도 있는 듯해서, 개발자가 융통성 있게 판단해야 한다고 생각합니다. (아니라면 죄송합니다 ㅠ)

 

2. else를 지양해야 하는 것은 1주차 때부터 유념해서 잘 지키고 있죠! 여기서 else 뿐만 아니라 else if 또한 지양해야 합니다. 하지만 그렇다고 해서 반드시 사용하지 않아야 하는 것은 아닙니다. 많은 분들이 else를 자주 사용하지 않아야 한다고 말씀하시지만, 그렇다고 해서 반드시 사용하지 않는 것은 더 위험하다고 말씀하십니다. 우테코에서도 때로는 else를 쓰는 것이 더 깔끔하므로, 프로그래머가 적절히 판단해야 한다고 하는군요.

 

3. Enum클래스에 대해서는 지금까지 들어보기는 자주 들어봤지만 사용할 일이 없어서 공부를 미루고 있었는데, 이제 공부해야 할 때가 온 것 같군요 ㅎㅎ.. 아래에서 자세히 알아보겠습니다.

 

4. 도메인 로직에 단위 테스트를 구현해야 한다? 이 또한 아직은 무슨 말인지 모르겠군요 ㅠㅠ.. 아래에서 자세히 알아보겠습니다.

 

그럼 이번 주차에서도 배운 점들을 기록해 보겠습니다.

 

1. Enum 클래스

Enum이란 Enumeration의 약자로, '열거'를 의미합니다. 즉 어떤 값들을 열거하는 클래스를 의미하는 것이겠군요. 다음과 같이 쓰일 수 있습니다.

 

enum class People {
    Alsong, Dalsong
}

fun main() {
    val person1: People = People.Alsong
    val person2: People = People.Dalsong

    println("$person1, $person2")
}

 

People이라는 클래스에는 AlsongDalsong라는 두 가지 상수가 존재하고, 이를 사용할 때에는 People.Alsong처럼 사용하면 됩니다. 참고로 이때 person1person2는 모두 People이라는 타입을 가집니다. 

 

그리고 enum 클래스의 각 상수는 초기화 인자를 가질 수가 있습니다. 아래처럼요!

 

enum class People(val age: Int) {
    Alsong(17), Dalsong(35)
}

fun main() {
    val person1: Int = People.Alsong.age
    val person2: Int = People.Dalsong.age

    println("$person1, $person2")
}

위의 코드에서 Alsongage는 17, Dalsongage는 35인 것입니다. 이를 사용할 때에는 People.Alsong.age처럼 사용하죠. 이를 통해 상수에 대한 추가 데이터를 저장할 수가 있습니다.

 

사실 처음 공부했을 때에는 '이걸 왜 쓰는거지?' 라는 생각이 듭니다.. 그냥 상수 변수 선언해서 사용해도 될 것 같거든요. 하지만 enum 클래스를 사용하면 코드의 가독성을 향상시킬 수 있고, 중복도 줄이며 안전성 또한 향상시킬 수 있다고 합니다.

enum class WinningAmounts(val amounts: Int){
    threeMatch(5000),
    fourMatch(50000),
    fiveMatch(1500000),
    fiveAndBonus(30000000),
    sixMatch(2000000000)
}

enum class UnitAmount(val price: Int){
    unit(1000)
}

저는 이번 미션에서는 위와 같이 당첨 상금액과 로또 가격을 상수화 하는 데에 enum클래스를 사용했습니다. 다만 이렇게 하는 것이 최선인가? 라는 것은 아직 의문이기는 합니다..

 

 

2. Companion Object (동반 객체)

2주차 공통 피드백에서 '구현 순서도 코딩 컨벤션이다' 부분에 '동반 객체'라는 단어가 등장하더라구요! 게다가 마침 Enum클래스를 공부하면서 동반 객체에 대한 개념이 등장하길래, 이것 역시 공부해 보았습니다.

 

참고로 Companion Object (동반 객체)에 대해서 정말 이해가 쉽게 설명이 되어있는 블로그가 있어서 여기서 도움을 많이 받았습니다! 이 글을 읽고 계신 분들 중에 아직 동반 객체에 대해 개념이 잘 잡히지 않으신 분이 계시다면 한 번 들어가 읽어보시는 걸 추천드릴게요.

 

https://www.bsidesoft.com/8187

 

[kotlin] Companion Object (1) - 자바의 static과 같은 것인가? - Bsidesoft co.

개요 코틀린(Kotlin)의 Companion object는 단순히 자바(Java)의 static 키워드를 대체하기 위해서 탄생했을까요? 이 갑작스러운 질문은 코틀린에서 왜 static을 안 쓰게 되었는지 이해하는 데 큰 도움이 될

www.bsidesoft.com

Companion Object를 공부하기에 앞서 먼저 Object에 대해 짚고 넘어가자면, Object란 자바에는 없는 독특한 기능으로, 인스턴스가 하나만 있는 클래스 선언 방법입니다. class 대신 object 키워드를 사용하면 됩니다.

 

object Alsong {
    val age = 17
    fun prtName(): String {
        return "alsong"
    }
}

fun main() {
    println(Alsong.age)
    println(Alsong.prtName())
}
// 실행 결과
17
alsong

이렇게 말이죠. 참고로 이렇게 인스턴스가 하나밖에 없는 클래스를 싱글턴이라고 합니다.

 

그리고 Companion Object는 클래스 내에 선언된 Object입니다.

 

class Alsong{
    companion object{
        val age = 17
        val name = "Alsong"
    }
}

fun main(){
    println(Alsong.Companion.age)
    println(Alsong.Companion.name)
}
// 실행 결과
17
Alsong

이렇게 클래스 내에 companion object 키워드를 사용해 Object를 만들어 주면 동반 객체가 생성된 것입니다. 오.. 클래스 내에서 선언된 Object인데도 인스턴스를 만들지 않고 변수를 사용할 수가 있군요! 뭔가 단순해서 마음에 듭니다 ㅋㅋㅋ

 

참고로 Alsong.Companion.age 부분에서 `Companion`은 IDEA에서 입력해 보면 회색 처리가 되면서 생략이 가능하다고 알려줍니다. 그래서 다음과 같이 쓰는 것이 가능합니다.

 

println(Alsong.age)
println(Alsong.name)

이렇게 Companion Object라는 것을 명시하지 않아도 되는 것이죠. 이러한 사용 방법이 마치 자바에서의 Static과 비슷하다고 하는데, 저는 자바를 몰라 거기까지는 모르겠습니다 허허.. 어쨌든 이런 기능이 있다는 것을 알았으니 앞으로 유용하게 써먹을 수 있을 것 같습니다.

 

 

3. 단위 테스트

이번 미션의 요구 사항에 다음과 같은 문구가 추가되었죠.

 

도메인 로직에 단위 테스트를 구현해야 한다.

무슨 말인지 몰라서 알아봤습니다. 일단 단위 테스트는 알겠는데, 도메인 로직은 무엇이지??

 

알아본 바로는 이렇습니다. 도메인 로직이란 '현실 세상의 문제를 프로그래밍으로 해결하는 코드'라는 것입니다. 이번 미션을 예로 들다면 '로또를 어떻게 랜덤하게 발행받을 것인지', '당첨 여부는 어떻게 판단할 것인지', '수익률은 어떻게 계산할 것인지' 등등을 코드로 어떻게 구현하는지를 말합니다. 단어만 어려울 뿐이지 말 자체는 어렵지 않네요. 그러니까 각각의 메서드들마다 단위 테스트를 구현하라는 말입니다.

 

class LottoGameTest {
    private fun provideInput(input: String) {
        System.setIn(ByteArrayInputStream(input.toByteArray()))
    }

    @Test
    fun readWinningNumberTest() {
        val lottoGame = LottoGame()

        // 가짜 입력을 설정
        provideInput("1,2,3,4,5,6")

        val result = lottoGame.readWinningNumber()

        // 테스트 단언문 작성
        assertEquals(listOf(1,2,3,4,5,6), result)
    }
}

코틀린으로 단위 테스트를 하는 것이 서툴러서 Chat GPT의 도움을 받았습니다. 참고로 단위 테스트 하려고 하는 메서드는 public으로 선언되어 있어야 한다고 합니다.

 

위 코드는 당첨 로또 번호를 입력받는 readWinningNumber() 메서드를 테스트하는 코드입니다. provideInput() 함수를 통해 가짜 입력값을 넣어줄 수가 있고, 그렇게 해서 받은 입력값을 통해 무엇을 리턴하는지를 테스트하고 있습니다.

 

assertEquals() 함수는 넣어준 인수들이 같다는 것을 *단언*합니다. assert는 '단언하다'라는 뜻이죠. 즉 저는 listOf(1,2,3,4,5,6)과 result가 같다는 것을 '단언'하는 것입니다. 만약 정말로 같다면 테스트가 통과하며, 같지 않다면 저의 단언이 틀린 것이기 때문에 오류가 발생합니다.

 

테스트 성공!

저의 단언이 옳았군요 ^^ 이런 방법으로 단위 테스트를 진행하면 되겠습니다.

 

 

4. 예외처리

커뮤니티를 보다가 어떤 분께서 팁을 공유하셨더라고요!

 

https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/require.html

 

require - Kotlin Programming Language

 

kotlinlang.org

require(value)

굳이 throw IllegalArgumentException을 쓰지 않아도 value가 False라면 IllegalArgumentException를 발생시킨다고 합니다! 정말 대단한 꿀팁..! 이제 저걸 사용하면 코드 길이도 줄고 가독성도 크게 향상되겠네요. 공유해 주신 Lumi님 감사합니다 (꾸벅)

 

 

5. 기능 구현시 어려웠던 점

이번 미션을 하면서 가장 어려웠던 점 중 하나입니다. 바로 기능 구현에 우테코에서 제공한 Lotto.kt 클래스를 사용해야 하는데, 이 클래스는 프로퍼티가 numbers만 존재했습니다.

 

class Lotto(private val numbers: List<Int>) {
    init {
        require(numbers.size == 6)
    }

    // TODO: 추가 기능 구현
}

이름이 numbers라서, 저는 numbers가 발행된 로또 번호라고 생각했습니다. 하지만 발행받은 번호를 다루는 클래스라면 로또번호가 당첨인지 아닌지를 판단하는 메서드를 정의하는 것이 일반적인 생각인데, 그러려면 당첨번호를 리스트 또한 이 클래스에서 프로퍼티로 갖고 있어야 합니다. 그런데 이 클래스는 그게 없으니 어떻게 해야 하는지를 상당히 오래 고민했습니다. 지금 생각해 보면 numbers가 당첨 번호라고 생각하는 편이 자연스러울 것 같긴 합니다만.. ㅋㅋ 그때는 왠지 그게 생각이 안나더군요.. 그래서 저는 그냥 이 클래스에 프로퍼티를 추가하는 특단의 조치를 취하기로 했습니다.

 

class Lotto(
    private val numbers: List<Int>,
    private val winnigNumber: List<Int>,
    private val bonusNumber: Int
) {

    init {
        require(numbers.size == 6) { "[ERROR] 로또 번호는 총 6개여야 합니다." }
    }

    fun winningJudge(): Int {
        var numberOfSameNumber = 0
        for (i in numbers) {
            if (i in winnigNumber) {
                numberOfSameNumber++
            }
        }
        // n개 일치하면 n을 리턴. 만약 5개 일치하면 보너스 있는지 확인하고 있으면 -1리턴, 없으면 5 리턴
        if (numberOfSameNumber != 5) {
            return numberOfSameNumber
        }
        if (bonusNumber in numbers) {
            return -1
        }
        return numberOfSameNumber
    }

}

이 클래스에서는 발행번호와 함께 당첨번호, 보너스 번호를 프로퍼티로 가집니다. 그래서 당첨 여부를 판단하는 winningJudge() 함수가 일치 번호 개수를 리턴하는 방식이죠. 요구 사항에서도 필드를 추가하면 안 된다는 말은 있었지만 프로퍼티를 추가하면 안 된다는 말은 없었기에.. 이렇게 했습니다 허허.. 하지만 확실히 부자연스럽다는 느낌은 들긴 합니다 ㅠㅠ 다른 분들의 코드를 참고해봐야겠네요..!!

 

 

느낀점

3주차에서는 enum 클래스와 단위 테스트를 처음 사용해 보았습니다. 프로젝트 전체를 테스트하는 것은 우테코 측에서 제공해 주신 테스트코드를 보고 2주차부터 적용을 해봤지만, 메서드 단위로 테스트 하는 것은 방법을 몰라 chat gpt의 도움을 받아 작성해 보았습니다. chat gpt에 모르는 것을 질문하니 정말 알기 쉽게 답을 얻을 수 있어서 다음부터도 프로그래밍을 공부할 때 chat gpt를 적극 활용해야겠다고 생각했습니다. 또 TDD가 정말 강력한 개발 방법이라는 것도 느꼈습니다.

 

또 기존에는 기능 구현 목록을 완벽하게 작성한 후 기능구현에 들어가야겠다고 생각했는데, 완벽하지 않아도 괜찮고 추후에 업데이트 하는 게 더 좋다는 사실을 알게 되어 이를 실천해 보았습니다. 직접 해보니 이 방법이 더 효율적이라고 느껴졌습니다.

 

또 아직 저의 리팩토링이 많이 부족하다고 느낍니다. 특히 함수를 15라인이 넘지 않도록 최대한 분리하려 노력했는데, 잘 되지가 않아 아직 멀었다는 것이 느껴집니다.

 

이제 마지막 주차만 남은 만큼 지금까지 배운 내용을 최대한 적용하는데 힘쓰려고 합니다. 또 4주동안의 프리코스 만으로도 엄청나게 성장했다는 것이 느껴집니다. 마지막까지 모두 화이팅!! ^^

 

https://github.com/woowacourse-precourse/kotlin-lotto-6/pull/36

 

[로또] 송제욱 미션 제출합니다. by songpink · Pull Request #36 · woowacourse-precourse/kotlin-lotto-6

기능 구현 목록 1. 로또번호가 당첨인지 여부를 판단하는 클래스 Lotto를 정의한다. winningJudge()함수 : n개 일치하면 n을 리턴한다. 만약 5개 일치하면 보너스 있는지 확인하고 있으면 0리턴, 없으면

github.com