Programming Languages

이상한 Kotlin의 세계 - Type inference

Sushi Yun 2020. 5. 17. 20:49

이거슨 사실 일종의 버그라 Kotlin이 이상하다고 하긴 애매하지만, 어쨌든 이것 때문에 스프링의 주요 기능이 동작하지 않고 있으니까 이상한걸로 치기로 했다. (링크 - p.19 WebTestClient Type Inference Issue in Kotlin)

수다쟁이 Java에 불만을 가진 사람들이 만든 JVM 기반의 언어들 중 Kotlin의 타입 추론은 참 이상하다는 느낌을 받았다.

// Java
class A {
	int a;
	char b;
}
    
<T> void inner() {
	System.out.println("(void)");
}
    
<T> void inner(T t) {
	System.out.println(t.toString());
}

void outer() {
	inner();
	inner(10);
	inner("ten");
	inner(new A());
}

위 Java 예제코드에서는 outer() 안에서 inner()를 호출할 때 파라미터 유무와 종류에 관계없이 제너릭을 잘 찾아낸다. (물론 레퍼런스에 매칭하기 위해서는 변수 타입 지정이 필요하다는 verbose함이 있긴 하다)

//Kotlin
internal class A {
	var a = 0
	var b = 0.toChar()
}

fun <T> inner() {
	println("(void)")
}

fun <T> inner(t: T) {
	println(t.toString())
}

fun outer() {
	inner() // error: Not enough information to infer type variable T
	inner<Any>() // inner<Unit>(), inner<Nothing>() ... are also OK
	inner(10)
	inner("ten")
	inner(A())
}

이번 코드는 위의 Java 코드를 그대로 IntelliJ에 복붙했을 때 자동으로 변경해주는 Kotlin 코드이다. 다른 부분은 충분히 짐작할 수 있는 방식으로 동작했고 (사실 A.b의 초기값을 0.toChar()로 잡은건 약간 의외긴 하다), inner()를 호출한 부분이 좀 눈에 띈다. 파라미터를 넘겼을 때는 파라미터 값의 타입을 통해 타입 추론이 가능했는데, 파라미터를 주지 않아서 타입 추론이 불가능했나보다(Not enough information to infer type variable T). 하지만 한편으로는 사용하지도 않는 타입을 무조건 넣어줘야 한다니, 좀 이상하다. inner<>() 형태로 사용하는 것도 안된다니... 그리고 (이건 좀 당연한 느낌이지만) 주석에 달아놓은 것 같이, Unit이나 Nothing 등의 타입을 넣어줘도 문제가 발생하진 않는다.

이런 현상이 '자바는 요구하지 않는 타입 추론을 코틀린에선 요구한다(Kotlin requires type inference where Java doesn't'는 이슈로 Jetbrains 이슈 관리 시스템에 등록되어있다. 6년 전에 등록된 이슈인데 아직도 해결되지 않고 있는걸 보면, 근본적인 문제가 있는거거나 별로 안중요한 이슈이거나 둘 중 하나인 것 같다.

근데 사실 내가 이 글을 쓴 것은, 이 현상을 Spring Boot + Kotlin으로 프로젝트를 구성하던 중 발견해서이다. Spring WebFlux 환경에서 WebTestClient를 사용해 ServerResponse(정확히는 Mono<ServerResponse>)를 리턴하는 RouterFunction을 테스트할 때, 응답 본문(Response Body)을 확인하기 위해 아래와 같은 코드를 사용했다.

client.get()
    .uri("/api/to/use")
    .exchange()
    .expectStatus().isOk
    .expectBody(ResultDto::class.java)
    .consumeWith<Nothing> {
        val responseBody = (it?.responseBody ?: fail("Null Response"))
        assertThat(responseBody.code).isEqualTo("HELLO")
        assertThat(responseBody.msg).isEqualTo("Hello message")
        assertThat(responseBody.data).isEqualTo("Hello")
    }

그런데 돌아오는건 consumeWith()에서 NPE가 발생했다는 테스트 실패 메시지가 아무것도 없이 그냥 NPE라는 이름뿐이었다. -_-

이것도 Spring 이슈 관리 시스템에 'Kotlin이 WebTestClient.BodySpec 타입을 상속받지 못한다(Kotlin unable to inherit type for WebTestClient#BodySpec)'라는 이름으로 등록되어있고, expectBody() 메소드를 사용하여 우회할 것을 권장하는 정도로 해결된 것으로 보인다.

val result = client.get()
	.uri("/api/to/call")
	.exchange()
	.expectStatus().isOk
	.expectBody(ResultDto::class.java)
	.returnResult()
	.responseBody!!

    assertThat(result.code).isEqualTo("HELLO")
    assertThat(result.msg).isEqualTo("Hello message")
    assertThat(result.data).isEqualTo("Hello")

이것도 뭐 나쁘지만은 않은데, 사실 consumeWith와 같이 넣어주는 제너릭이 왜 Nothing이어야 제대로 동작하는건지(적어도 컴파일은 된다) 모르겠다. 다른 타입으로 지정해주면 받는 함수의 파라미터 타입을 Nothing으로 인식한다.

이래저래 Kotlin 이상하다 -_-

'Programming Languages' 카테고리의 다른 글

이상한 Kotlin의 세계 - Iterable.map()  (0) 2020.01.17
Scala Option  (0) 2017.01.29
[추상자료형] 리스트(List)  (0) 2016.09.18