1편은 아래 글을 참고하자.
https://kth990303.tistory.com/312
이번 포스팅에선 1편에 이어서 java stream api와 유사한 코틀린의 Collections 함수들과 random, 물음표(null able operator)와 느낌표(not null operator), 코틀린 테스트 문법, 그리고 그 외 팁들에 대해 적어보려 한다. 아직 배우는 단계이므로 더 좋은 팁들이나 틀린 점들이 있다면 댓글로 마구마구 피드백! 부탁드려요~~
Java Stream과 유사한 기능을 해주는 Kotlin 컬렉션 함수들
굉장히 많아서 여기에는 내가 사용해본 몇 가지만 작성해보겠다.
코틀린 stream 구글링을 하면 더 많은 함수들을 확인할 수 있다.
- map
fun generateCars(userInput: String?): List<Car> {
val carNames: List<String> = separateCarNames(userInput)
return carNames.map { Car(it) }
}
코틀린 컬렉션 API 함수들은 괄호를 생략하여 중괄호만 사용할 수 있다는 점에 유의하자.
map({ Car(it) })로도 사용이 가능하다. 표현식이 마지막 인자로 사용되는 경우 괄호가 생략이 가능하지만, 람다 표현식이므로 중괄호는 필수적으로 들어가 있어야 한다는 점에 주의하자.
return carNames.map({ carName -> Car(carName) })
return carNames.map({ Car(it) })
return carNames.map { Car(it) }
return carNames.map(::Car)
코틀린에서는 it 키워드가 제공돼 별도로 car -> Car(car)와 같이 화살표 연산자를 작성할 필요 없이 it만으로 작성할 수 있다. 여기서 더 나아가 메서드 레퍼런스 기능을 활용하여 ::Car과 같이 작성해줄 수도 있다.
참고로 코틀린의 컬렉션 API 함수를 사용한 것이기 때문에 따로 toList()를 해줄 필요 없이 자동으로 새 List를 만들어서 반환해준다. (java에서 Collectors.toList()나 Collectors.toUnmodifiableList()에 너무 익숙해진 듯...)
- filter
fun findWinners(): List<Car> {
return cars
.filter { it.position == findMaxPosition() }
}
자동차의 위치가 최대인 자동차들만 걸러주는 함수이다.
it 키워드를 이용하여 car -> car.position이 아닌 it.position으로 바로 접근했다.
마찬가지로 컬렉션 API 함수이므로 toList()가 필요 없다.
- maxOf, minOf
private fun findMaxPosition(): Int {
return cars
.maxOf { it.position }
}
리스트에서 가장 최대/최소의 값을 리턴해준다.
cars는 List<Car> 타입이고, Car에는 position 속성이 포함돼있다.
리스트 안의 Car들 중 position 속성들 중 가장 큰 값을 리턴해준다.
- distinct
private fun isDuplicateNames(names: List<String>): Boolean {
return names.distinct().size != names.size
}
리스트 내의 원소들의 중복을 제거한 List를 리턴해준다.
- first, last
fun getFirstCar(): Car {
return cars.first()
}
fun getLastCar(): Car {
return cars.last()
}
List의 첫번째 원소, 마지막 원소를 리턴해준다.
- joinToString
fun getWinnerNames(winners: List<Car>): String {
return winners.joinToString(separator = ", ") { winner -> winner.name }
}
각 List<String> 원소들 사이에 separator를 넣어주어 하나의 String으로 반환해준다.
위 코드에서는 우승자의 이름들 사이에 ", "를 넣어주어 하나의 String을 반환해주는 걸 알 수 있다.
Math가 필요없는 Kotlin에서의 random값
java에서 랜덤값을 얻으려면 0~1 사이 난수를 반환하는 Math.random()를 이용해야 했다.
범위를 우리 마음대로 설정해주기 위해 end - start 값을 곱해준 후, start 값을 더해주곤 했다.
// JAVA
(int) (Math.random() * (end - start)) + start;
// KOTLIN
(start..end).random()
코틀린에서는 위 과정이 불필요하다.
random() 함수를 위와 같이 작성해주면 start ~ end 까지 해당되는 Int값들을 랜덤하게 리턴해주기 때문이다.
훨씬 코드가 간단해진 것을 확인할 수 있다.
참고로 kotlin에서 JAVA의 Math 기능을 사용하려 했는데 아래 사진과 같이 kotlin의 Math가 안떠서 당황할 수 있다.
kotlin에서 math 기능을 사용하기 위해선 아래와 같이 kotlin.math를 입력해주면 된다!
PI, 자연상수 e를 바로 사용할 수 있고, 절대값, 기울기를 구할 때 사용되는 atan2와 같은 삼각함수들도 사용할 수 있다.
Elvis Operator (엘비스 연산자)
코틀린에는 엘비스 연산자 ?: 가 존재한다. 특정 값이 null일 경우 ?: 뒤의 명령을 실행한다.
삼항연산자와 비슷하게 생겼지만, 엄밀히 따지면 삼항연산자와는 다르다고 한다. 삼항연산자는 참/거짓 분기를 타는 연산자라면, 엘비스연산자는 nullable 타입을 다루기 위한 연산자로 쓰인다. 물음표(?) 연산자, 느낌표(!!) 연산자, safe call(?.) 연산자와 세트메뉴처럼 불리는 데는 이유가 있는 법이었다.
여담으로 제이슨이 엘비스 연산자의 유래를 알려줬는데, 시계 방향으로 90도 뒤집으면 엘비스 프레슬리의 헤어스타일처럼 생겨서 이러한 이름이 붙여졌다고 한다.
- The name “Elvis operator” refers to the fact that when its common notation, ?:, is viewed sideways, it resembles an emoticon of Elvis Presley with his quiff, or in the other direction, his smirk.[1]: https://en.wikipedia.org/wiki/Elvis_operator)
// AS-IS
private fun toInt(number: String) : Int{
try {
return number.toInt()
} catch (e: NumberFormatException) {
throw IllegalArgumentException("[ERROR] 숫자를 입력하세요")
}
}
// TO-DO
number.toIntOrNull() ?: throw IllegalArgumentException()
출처: 우아한테크코스 4기 후니의 코틀린 레이싱카 PR, 우아한테크코스 4기 차리의 slack 스레드
엘비스 연산자를 사용하면 불필요한 indent를 줄이고 가독성을 높일 수 있다.
(내가 삼항연산자를 좋아하는 편이어서 그럴 수도 있긴 하다 ㅎㅎ... 중첩된 삼항연산자만 아니면 되지~)
예시를 함 보자.
fun test() {
a ?: return b
// ...
return c
}
a 값이 null이면 바로 b를 리턴시켜버리고, null이 아니면 함수를 실행해 c를 리턴한다.
Null Safety Operator
코틀린은 변수에 null을 넣지 못한다.
하지만 우리의 코틀린은 꼰대가 아니어서 null 허용 여부를 물음표(?)와 느낌표(!!)로 설정해줄 수 있다.
- 물음표(?) : null 허용 선언 : Nullable Operator
- 느낌표(!!) : null값이 절대 들어오지 않는다는 선언 : Not-Null Operator
- 물음표 + 온점 (?.) : null이 아닌 경우 프로퍼티, 메서드 호출. null일 경우 null 반환 : safe call Operator
val a:Int = null // 컴파일 에러
val b:Int? = null
b는 Int? 와 같이 물음표가 존재하므로 null을 허용한다.
// userInput null check
if (userInput != null) {
return userInput.split(",")
}
// userInput Null 아님이 확실할 경우
return userInput!!.split(",")
만약 절대로 null이 들어올 수 없도록 설계했을 경우 !! 를 사용해주어 코드를 간단하게 할 수 있다.
val a = "kth990303"
val b: String? = null
println(b?.length) // null
println(a?.length) // Unnecessary safe call
a는 null을 허용하지 않는다. 따라서 ?. safe call을 해줄 필요가 없다.
b는 null을 허용한다. b에는 null이 담길 수도 있고, 안담길 수도 있다. 만약 null이 아니라면 b.length를 출력하고, null일 경우 null을 반환한다. 따라서 b?.length의 타입은 Int? 가 된다.
입력받을 때엔 readLine과 readln
코틀린은 JAVA와 달리 Scanner로 입력받지 않고 바로 readLine, readln으로 입력받을 수 있다.
그런데 readLine()도 있고, readln()도 있고 readlnOrNull()도 있다. 무슨 차이일까?
바로 null 허용 여부에서 차이가 보인다.
코틀린의 물음표를 위에서 학습했다면, readln과 readLine중 어떤 것이 null을 허용해서 반환하는지 알 수 있을 것이다.
여담으로, readln이 readLine보다 비교적 최근에 탄생했다고 한다.
// readLine
fun getCarNamesByUser(): String? {
println(CAR_NAME_INPUT_MESSAGE)
return readLine()
}
// readln
fun getRoundByUser(): String {
println(ROUND_INPUT_MESSAGE)
return readln()
}
코틀린의 물음표(?) 는 null을 허용해준다.
readLine()은 null을 허용해서 입력받아 반환형이 String?이고, readln은 null을 허용하지 않고 입력받으므로 반환형이 String이다.
fun getNumber(): Int {
// readln
return readln().toInt()
// readLine (컴파일 에러)
return readLine().toInt()
// readLine과 !!
return readLine()!!.toInt()
// readLine과 ?., ?:
return readLine()?.toInt() ?: throw IllegalArgumentException("숫자 입력해")
}
위 네 가지 리턴 명령어들을 보면서 자신이 코틀린의 ?와 !!를 제대로 알고 있는지 확인해보자. (엘비스 연산자는 덤)
readln과 readLine을 제대로 활용하려면 물음표(?) 와 느낌표(!!)를 제대로 알고 있어야 한다.
readln은 null을 반환하지 않으므로 바로 toInt를 사용할 수 있다. 그러나, readLine은 null이 반환될 수도 있기 때문에 toInt()를 해주기 위해서는 null이 아니라는 느낌표(!!) 선언을 해준 후에 toInt()를 사용할 수 있다. safe call을 이용하여 readLine이 null이 아닐 경우 toInt()를 반환해주고, null일 경우 에러를 띄워주게 할 수도 있다.
io.kotest 테스트
코틀린 환경에서도 junit문법, AssertJ 문법을 사용할 수 있다. 그렇지만 이러한 문법들을 사용할 경우 Mocking이나 Assertion 과정에서 코틀린 DSL을 활용할 수 없다고 한다. (아직 나는 기초문법을 배우는 단계라 느끼진 못했지만)
이러한 단점을 kotest를 사용하면 보완할 수 있다고 한다.
환경설정 (build.gradle.kts)
dependencies {
testImplementation("io.kotest", "kotest-runner-junit5", "5.2.3")
}
tasks {
test {
useJUnitPlatform()
}
}
kotest 문법을 사용하기 위해 build.gradle.kts에 위 라이브러리를 추가해주자.
Assertions를 대신할 문법들
- shouldBe
@Test
@DisplayName("4 미만의 숫자를 넣으면 전진을 하지 않아야 합니다.")
fun not_proceed() {
val car = Car("k")
car.proceed(PROCEED_FLAG_NUMBER - 1)
// kotest 문법
car.position shouldBe 0
// Assertions 문법
assertThat(car.position).isEqualTo(0)
}
shouldBe를 사용하니까 상대적으로 조금 더 간단해졌다.
isEqualTo라 생각하면 될 듯하다.
shouldHaveSize, shouldContain, shouldContainAll
리스트에 관련된 테스트를 진행하고 싶다면 shouldHaveSize로 사이즈를 검증할 수 있다.
shouldContain, shouldContainAll로 특정 값이 포함돼있는지도 확인할 수 있다.
- shouldHaveSize, shouldContain
@Test
@DisplayName("우승자를 판단합니다.")
fun findWinners() {
val carA = Car("a")
val carB = Car("b")
val cars = Cars(listOf(carA, carB))
carA.proceed(PROCEED_FLAG_NUMBER)
cars.findWinners()
.shouldHaveSize(1)
.shouldContain(carA)
// assertThat(cars.findWinners())
// .hasSize(1)
// .contains(carA)
}
- shouldContainAll
@Test
@DisplayName("우승자가 중복일 경우 모두 판단합니다.")
fun findMultiWinners() {
val carA = Car("a")
val carB = Car("b")
val cars = Cars(listOf(carA, carB))
carA.proceed(PROCEED_FLAG_NUMBER)
carB.proceed(PROCEED_FLAG_NUMBER)
cars.findWinners()
.shouldHaveSize(2)
.shouldContainAll(carA, carB)
// assertThat(cars.findWinners())
// .hasSize(2)
// .contains(carA, carB)
}
참고로 shouldContain, shouldContainAll 대신 contain, containsAll을 써도 가능하다.
- shouldContainAll 과 containsAll
@Test
@DisplayName("올바른 입력일 때 자동차들을 정상적으로 생성해야 합니다.")
fun saveCars() {
// shouldContainAll (listOf로 컬렉션을 넣어주는 것도 가능함.)
generateCars("pobi,crong,honux")
.shouldHaveSize(3)
.shouldContainAll(Car("pobi"), Car("crong"), Car("honux"))
// containsAll (listOf로 컬렉션화해주어야 함.)
Assertions.assertThat(generateCars("pobi,crong,honux"))
.hasSize(3)
.containsAll(listOf(Car("pobi"), Car("neo"), Car("honux")));
// assertThat(generateCars("pobi,crong,honux"))
// .contains(Car("pobi"), Car("crong"), Car("honux"))
}
contains, containsAll은 SELF 타입을, should 관련 테스트함수들은 Unit 타입을 반환해준다. (List의 containsAll, junit의 containsAll, shouldContainAll 등 정말 다양한 containsAll 관련 함수들이 존재하므로 헷갈릴 수 있다.)
다만, junit 문법의 containsAll은 Assertions.assertThat으로 감싸주어서 확인해주어야 했는데, shouldContainAll은 바로 확인이 가능하다.
이외에도, 모든 원소들의 값이 순서 보장하여 완벽히 일치하는지 확인하는 shouldContainExactly와 순서를 보장할 필요 없는shouldContainExactlyInAnyOrder도 존재한다.
shouldThrow
예외처리 테스트를 하려고 코틀린에서 Assertions.assertThatThrownBy를 사용했다면 난감한 현상을 겪었을 것이다.
바로 아래 그림처럼 빨간줄이 뜨면서 작성이 되지 않기 때문이다.
사실 isInstanceOf(IllegalArgumentException::class.java)와 같이 작성하면 가능하긴 하지만, java의 class를 사용해야돼서 코틀린스럽지 못한 코드가 나온다고 한다.
다행히 코틀린에서 예외처리 테스트에는 다른 두 가지 좋은 방법이 존재한다.
1. junit의 assertThrows
2. kotest의 shouldThrow
@Test
@DisplayName("라운드에 숫자가 아닌 값이 입력될 경우 예외를 발생해야 합니다.")
fun validateRoundByNonNumber() {
// kotest
shouldThrow<IllegalArgumentException> {
validateRoundNumber("abc")
}
// junit
assertThrows<IllegalArgumentException> {
validateRoundNumber("abc")
}
}
두 방법 모두 validateRoundNumber("abc")를 실행했을 때 IllegalArgumentException가 발생하는지 테스트하는 코드이다.
도움된 글:
- null safety operator: https://kotlinlang.org/docs/null-safety.html#the-operator
- 코틀린에서의 stream: https://black-jin0427.tistory.com/214
- 엘비스 연산자: https://coco-log.tistory.com/158
- kotest 환경설정 및 간단한 문법목록: https://techblog.woowahan.com/5825/
java와 비슷한 듯 다른 kotlin 문법들이 생각보다 많다.
3탄 포스팅이 탄생할 수 있을 거 같기도 하고, 아니면 이제 문법 포스팅은 멈추고 개발 적용하는 포스팅이 올라올 수도 있을 듯하다.
배우면 배울수록 신기한 코틀린...
+) 22.05.05. 20시 경 추가
우테코 코치 제이슨이 꼼꼼하게 피드백해주셔서 포스팅의 퀄리티가 더욱 높아졌다!
어린이날 휴일임에도 불구하고 꼼꼼하게 개선점을 피드백해주신 갓 제이슨... 존경합니다
'Kotlin > Kotlin | Spring 학습기록' 카테고리의 다른 글
[Kotlin] Kotlin DSL + Spring REST Docs + MockMvc 적용기 (1) (2) | 2022.07.17 |
---|---|
[Kotlin] Sealed Class를 이용한 무분별한 상속 확장을 방지하기 (0) | 2022.06.04 |
[Kotlin] 코틀린에서의 상속을 위한 키워드 open, override, super (2) | 2022.05.19 |
[Kotlin] mutableList로 List에서 수정과 삭제를 해보자 (2) | 2022.05.13 |
[Kotlin] Java와 비슷하면서 다른 코틀린 문법 정리하기 (1) (6) | 2022.05.02 |