Next step에서 진행하는 ‘TDD, 클린 코드 with Kotlin 7기’ 강의를 들은지 벌써 2달이 지났다. 후기를 작성을 빨리 하고 싶었지만, 업무를 병행하면서 미션을 모두 진행했다는 뿌듯함에 너무 오랫동안 미루게 되었다. 이제야 부랴부랴 작성해본다… (강의 링크: https://edu.nextstep.camp/c/Z9QeJlCi/)

코틀린은 백엔드 개발쪽에서도 자바와 비교하여 대체할만한 언어로 몇 년간 화두에 올랐었습니다. 그래서 계속 관심을 갖고는 있었지만 딱히 공부를 하지는 않았었습니다. 첫 째는 자바도 충분히 빠르게 발전하고 있었다고 생각하였고, 다니고 있던 회사에서도 코틀린은 거의 사용하지 않아서 동기부여가 되지 않았었습니다. 그러다 회사에서 이 교육에 지원할 사람을 모집하는 글을 보게 되었습니다. 예전부터 관심이 있던 강의였던지라 신청을 하였고, 운이 좋게도 선정이 되어 듣게 되었습니다.

강의 기간은 2023년 10월 24일에서 2023년 12월 4일까지 약 두 달간 진행되었습니다. 미션은 총 4가지가 있었습니다.

  1. 자동차 경주 게임
  2. 로또 게임
  3. 블랙잭 게임
  4. 지뢰 찾기 게임

미션은 1주에 하나씩 시작되었고, 미션 피드백과 코틀린에 대해서 강의도 한 주에 1개씩 진행하였다. 미션은 오픈 소스에 PR을 반영하는 방식과 거의 유사하게 진행되기 때문에 PR을 경험해볼 수 있다는 것도 큰 장점인 것 같다.

미션은 기본적으로 1주에 하나씩 시작하였고, 주마다 코틀린에 대한 설명 및 미션 피드백에 대한 강의가 있었습니다. 미션을 진행은 단계별로 주어진 요구사항을 구현하고 PR을 보내 담당 리뷰어님에게 피드백을 받고 이를 반영하는 방식입니다. 가장 큰 도움이 되고 빠르게 배울 수 있었던 점이 바로 이러한 피드백 시스템이었습니다. 미션 자체도 게임이다보니 흥미가 있었고, 궁금한 부분에 대해서도 빠르게 피드백을 받을 수 있는 점이 정말 좋았습니다.

매주 진행되는 제이슨님의 강의도 큰 도움이 되었습니다. 단순히 개념에 대해서 알려주는 딱딱한 방식이 아닌 미션이나 실제 현업에서 사용되는 코드를 기반으로 설명을 해주셨고, 채팅으로 궁금한 점에 대해서도 바로바로 답변을 주셨습니다. 전반적으로 매우 매우 만족스러운 강의였습니다.

다행스럽게도 강의가 끝나는 주에 미션을 모두 완료하여 정말 뿌듯했습니다. 코틀린에 대해서도 빠르게 익숙해질 수 있어서 작은 프로젝트에 바로 도입을 해보아야겠다고 다짐했습니다.

2월인 지금 아직 못지키고 있는게 아쉽지만… 올 해내에는 꼭 코틀린으로 개발하는 프로젝트 1개는 만들어볼 계획입니다!

개인적인 미션 결과는 이 링크에서 확인해볼 수 있습니다.

기억에 남는 미션 피드백

미션을 진행하면서 남기면 좋을 것 같은 피드백을 모아보았습니다. 사실 강의가 코틀린 언어 자체도 중요하지만 TDD(테스트), 클린 코드가 어떻게 보면 더 중요할 수도 있을 것 같습니다. 그러다보니 이에 대한 피드백이 더 기억에 남았던 것 같고, 클린 코드와 테스트를 코틀린에서 어떻게 잘 작성할까에 대한 피드백도 좋았었습니다.

VO(Value Object)는 언제 사용해야할까?

VO는 ‘값’을 객체로 분리하여 좀 더 유연한 코드를 만들 수 있다는 장점이 있습니다. 게다가 불변으로 유지해야하기 때문에 관리가 용이합니다. 이를 미션에 최대한 활용을 해보고 싶어졌지만, 어디에 그리고 얼만큼 이를 활용하는게 좋을지 감이 잡히지 않아 리뷰어님에게 질문을 드렸습니다.

img1

감사하게도 리뷰어님만의 기준을 말씀해주셨고, 이에 공감이 가기도 했고 저만의 기준이 필요하다고 느껴졌습니다. 미션은 학습을 위한 것이다보니 최대한 VO를 사용할 수 있는 곳은 사용해보고자 하였습니다. 그러다보면 자연스럽게 나만의 기준이 생기지 않을까도 기대해보았습니다. (답변 링크)

객체를 나누는 기준

로또 게임은 자동차 경주 게임보다는 복잡한 요구사항을 가지고 있었습니다. 그러다보니 객체를 어떻게 나눠야할지가 큰 고민거리였습니다. 어떤 객체에 역할과 책임을 부여할지가 최대 관심사였던 기억이 납니다.

img2

이에 대해서도 리뷰어님에게 질문을 드렸고, 객체 지향에 대해서 다시 한 번 생각해보는 계기가 되었습니다. 객체 지향이 객체에 적절한 역할과 책임을 부여하고 이러한 객체들 사이의 관계가 중요하다고는 알고 있었습니다. 하지만 이를 어떻게 할까에 대한 고민은 깊게 하지 못했던 것 같습니다. 결국 객체지향은 깨끗한 코드를 유지하여 유지보수를 높이는 궁극적인 목표가 있습니다. 이를 위해서는 여러 개발자들이 보편적으로 떠올리는 생각들이 있을 것입니다. 이에 대해서 짚어주는 답변이었고, 크게 공감을 하였습니다. (답변 링크)

코틀린이 제공하는 다양한 객체 선언 문법

코틀린에서는 객체를 선언하는 다양한 방법을 제공하고 있습니다. 특히, 값을 표현하기 위한 객체 선언을 data class와 value class 두 가지 방법으로 사용할 수 있다보니, 상황에 따라 적절히 사용하는 것이 필요하였습니다. 특히, value class에 대해서는 피드백을 통해 처음 알게 되기도 하여 이에 대해 찾아보고 비교를 해보았습니다.

img3

답변 링크

img4

답변 링크

코틀린을 써보면서…

코틀린을 사용해보면서 느낀 장점은 생산성과 안정성이었습니다. 세미콜론과 같은 불필요한 문법을 제거하고 직관적인 이름으로 편리한 기능을 제공하는 API를 통해 생산성을 높일 수 있었습니다. 그리고 기본적으로 null을 허용하지 않고 불변으로 선언을 하기 때문에 실수를 줄여주었습니다. 미션을 통해서 이를 느낄 수 있었고 그에 대한 예제를 간단히 나열해보았습니다.

세미콜론(;)이 필요없다.

이는 아래 예제에서 모두 살펴볼 수 있습니다.

직관적이고 다양한 기능의 API

유효성 검사

require(namesOfRacingCar.isNotEmpty()) { "자동차 이름은 1개 이상 존재해야 합니다." }
require(numOfAttempts >= 1) { "시도 횟수는 1 이상이어야 합니다." }
require(name.isNotBlank()) { "자동차 이름은 공백일 수 없습니다." }
require(name.length in 1..5) { "자동차 이름은 1~5자만 가능합니다." }

반복문

repeat(numOfAttempts) {
    val movedRacingCars: MutableList<RacingCar> = mutableListOf()
    resultOfRacingCars.forEach {
        it.moveForward()
        movedRacingCars.add(it.copy())
    }

    racingCarGameRound.add(RacingCarGameRound(round++, movedRacingCars))
}

for (row in 0 until height) {
    for (col in 0 until width) {
        if (openedCoordinate.contains(Coordinate(row, col))) {
            print("${countingBoard.countAroundMine(row, col)} ")
            continue
        }
        print("C ")
    }
    println()
}

컬렉션

컬렉션 내에서 편의를 위한 다양한 API를 제공해줍니다.

// map(): 다른 객체로 변환해주는 메서드
val resultOfRacingCars = namesOfRacingCar.map { RacingCar(it, racingGameMoveRule) }

// intersect(): 두 집합의 교집합을 구하는 메서드
val matchCount = lottoNumbers.values.intersect(winningLottoTicket.lottoNumbers.values.toSet()).size

// shuffled(): 랜덤하게 컬렉션 내 요소를 섞어주는 메서드
LottoNumbers.lottoNumbersCandidates.shuffled().take(6)

문자열 처리가 편리하다.

문자열 내에서 변수를 사용할 수 있습니다.

override fun toString(): String {
    return "RacingCar(name='$name', position=$position)"
}

문자열 내에서 표현식을 사용할 수 있습니다.

println("${winners.racingCars.joinToString(", ") { it.name }}가 최종 우승했습니다.")

참고로, 위와 같은 기능을 코틀린에서는 문자열 템플릿(String Template) 기능이라고 합니다.

불변으로 다루기가 쉽다.

불변 변수를 선언하는 val 이라는 키워드를 제공합니다. 거기다 기본적인 컬렉션 네이밍은 모두 불변으로 제공합니다.

// val 키워드로 선언한 변수는 모두 불변
val names = ConsoleInput.inputNamesOfPlayer()
val scoringRule = DefaultScoringRule()

// 'mutable' 이라는 키워드를 포함하지 않는 기본 네이밍의 컬렉션은 모두 불변
val allPlayers = listOf(dealer, *participants.toTypedArray())
val cards: List<Card> get() = _cards.toList()

불변이 아닌 가변으로 변수를 선언하고 싶은 경우는 var 키워드로 변수를 선언해야 하며, 컬렉션인 경우는 mutable 이라는 키워드가 붙은 메서드 또는 객체를 사용해야합니다.

var input: String

private val _cards: MutableList<Card> = mutableListOf()

null 안전성을 쉽게 보장해준다.

코틀린에서 선언하는 모든 변수는 기본적으로 null을 허용하지 않습니다.

단, null을 사용하기 위해서는 ? 키워드를 사용할 수 있습니다.

  • 널 가능 타입(Nullable Types): null을 허용하는 변수 선언
  • 안전 호출 연산자(Safe Call Operator): null이 아닐 경우에만 호출
  • 엘비스 연산자(Elvis Operator): null인 경우 반환할 값(또는 예외) 선언 (우항)
// 널 가능 타입(Nullable Types)
var name: String? = "John"
name = null // 널 할당 가능

// 안전 호출 연산자(Safe Call Operator)
println(name?.length)

// 엘비스 연산자(Elvis Operator)
val length = name?.length ?: 0  // null인 경우 0 반환
rows[Coordinate(row, col)] ?: throw IllegalArgumentException("존재하지 않는 좌표입니다.")  // null인 경우 예외 반환

물론, 장점만 존재하지는 않았습니다. 강의에서 진행한 미션은 순수한 코틀린으로도 충분하기 때문에 느끼지는 못했지만, 예전부터 자바에서 제공하던 라이브러리나 프레임워크에 대해서는 아직 사용이 불편한 점들이 있었습니다.

  • JUnit에 비해 Kotest에서는 제공하지 않거나 사용하기 위해서 추가적인 설정 또는 우회가 필요하다거나
  • JPA, QueryDSL를 사용하기 위해서 복잡한 설정을 추가해야한다거나

또, 코틀린 DSL, 확장 함수, 연산자 오버로딩 등 다양한 구문을 제공하다보니 한 가지 기능을 구현하는데도 개발자마다 코드가 확연히 달라질 수 있습니다. 이는 여러 개발자가 같이 구현하기 위해서는 이를 위한 컨벤션이 필요할 것이고 이 또한 비용일 수 있습니다.

개인적으로 코틀린을 사용해보면서 자바보다 확실히 생산성이 좋았습니다. 자바에서 불편했던 부분을 많이 개선했다는 느낌이 강하게 들었습니다. 코드량도 눈에 띄게 줄다보니 피로감도 낮았습니다. 단, 강의에서 진행한 미션에서는 스프링과 같은 복잡한 프레임워크나 라이브러리 없이 단순히 콘솔에서 동작하는 프로그램을 만드는 것이다보니 이러한 환경에서는 또 어떠할지는 모르겠습니다. 이미 경험한 분들의 말에 따르면 생각보다 불편하다는 것을 듣기는 하였습니다. (강의에서도 이 부분은 언급해주었고 실제로 필요한 설정들을 보니 사용 허들이 조금 높겠다고 느꼈습니다.)

자바만을 주로 사용하다가 코틀린을 사용하니 재미도 있었고, 최근 서버에서 사용하는 언어 중 코틀린도 꾸준히 증가하고 있는 것을 보면 강의 이후에도 꾸준히 공부하고 사용해야겠다고 느꼈습니다.