기타 메모

Kotlin이어도 null에러는 발생한다고..

Jonchann 2021. 12. 9. 16:47

java에 org.jsoup.Connection.Response라고 HTML response를 받는 오브젝트가 있다.
Response오브젝트에는 String형 body를 꺼낼 수 있는 body()나 Content-Type에서 String형 charset을 꺼내는 charset()이 있다.
그런데 이 charset()은 nullable한 String?이라는 건데. 언제 그러냐하면 UTF-8이 명시되어있지 않을 때 그렇다.

참고로 ?가 붙으면 nullable하다는 것이고 !가 붙으면 절대로 null일 가능성은 없다는 뜻이다.

이걸 모르고 아래와 같은 코드를 적었다.

import org.jsoup.Connection

private fun isValid(res: Connection.Response): Boolean {
    val charset = res.charset()
    return if (charset.uppercase() == "UTF-8") {
        true
    } else {
        LOGGER.info("not UTF-8 encoded. charset is $charset")
        false
    }
}

그랬더니 아니나다를까 릴리스 하자마자 에러를 뿜어냈다.

java.lang.NullPointerException: charset must not be null

듣기로 Kotlin은 null에 안전한 언어라지. 그래서 처음에 컴파일 할 때 경고라도 내 줬으면 좋았으려만.

어쨌든 다른 선배가 수정해서 PR을 냈다.

import org.jsoup.Connection

private fun isValid(res: Connection.Response): Boolean {
    val charset = res.charset()
    return if (charset?.uppercase() == "UTF-8") {
        true
    } else {
        LOGGER.info("not UTF-8 encoded. charset is $charset")
        false
    }
}

하지만 이 코드도 에러는 계속 났다.
에러 메세지로 봐서는 String!이 아니라 String?이라서 문제가 되는 것 같았다.
참고로 에러를 내고 있는 곳은 if 조건문이 적힌 곳인데, 여기서 charset이 하는 것은 .uppercase()하는 것 밖에 없다.

    return if (charset?.uppercase() == "UTF-8") {

uppercase()를 보아하니 String이라고만 적힌 것이 String?은 허용하지 않는 것 같았다(Kotlin에서 얼마나 null체크를 해주는 건지는 모르겠지만). 즉 null일 때는 uppercase()는 쓸 수 없지 않을까 하는 것.
하지만 다른 선배는 그 전 build에서 에러가 나서 이미 charset?.uppercase()로 해결되었던 것일지도 모른다고 했다. 확실히 에러에 적힌 build 시간은 그 전 릴리스에 의한 것이었지만 그렇다면 왜 그 다음 릴리스에서는 낫지 않고 몇 시간동안 계속된 것일까..

public actual inline fun String.uppercase(): String = (this as java.lang.String).toUpperCase(Locale.ROOT)

때문에 나는 아래와 같이 수정해서 PR을 냈다.
어차피 if조건문에서는 앞에 있는 조건을 먼저 체크하기 때문에 그 다음에 있는 charset은 null일리가 없다고 판단했기 때문이다. 하지만 이도 여전히 String?이 될 수도 있기 때문에 확실한 방법이라고 할 수 없다.

import org.jsoup.Connection

private fun isValid(res: Connection.Response): Boolean {
    val charset = res.charset()
    return if (!charset.isNullOrBlank() && charset.uppercase() == "UTF-8") {
        true
    } else {
        LOGGER.info("not UTF-8 encoded. charset is $charset")
        false
    }
}

결론적으로 디버그가 끝난 코드는 아래와 같다.
선배들 조언에 따르면 ==null 에 의해 Kotlin에서는 그 다음 행부터 !형이 된다고 한다. 따라서 가장 확실한 방법이긴 하다.

import org.jsoup.Connection

private fun isValid(res: Connection.Response): Boolean {
    val charset = res.charset()
    if (charset == null) {
        LOGGER.info("charset is null")
        return false
    }
    if (!charset.isNullOrBlank() && charset.uppercase() == "UTF-8") {
        return true
    }

    LOGGER.info("not UTF-8 encoded. charset is $charset")
    return false
}

로깅이 필요없다면 아래와 간결하게 같이 쓸 수 있다.

import org.jsoup.Connection

private fun isValid(res: Connection.Response): Boolean {
    val charset = res.charset() ?: return false
    return if (charset.uppercase() == "UTF-8") {
        true
    } else {
        LOGGER.info("not UTF-8 encoded. charset is $charset")
        false
    }
}

참고로, charset이 붙어있지 않은 경우는 대부분 default값으로 UTF-8이 지정되어 있는 경우인 것이 대부분이기 때문에 return true를 해도 된다.
그건 또 다른 PR로 리퀘스트 요청해야한다.