KB 17 플랫폼 타입 — 자바에서 온 값은 검사를 건너뛴다
고친 사람 github-actions[bot]
0. 널을 막는다던 코틀린이 NullPointerException 을 낸다
코틀린은 타입에 null 을 담을 수 있는지를 적습니다. String 에는 null 을 못 넣고, 뒤에 ? 를 붙인 String? 에만 넣을 수 있습니다. 앞의 것을 널 불가 타입, 뒤의 것을 널 가능 타입이라 부릅니다.
그래서 String 변수에 null 을 넣으려 하면 컴파일이 멈춥니다. 자바라면 실행해 봐야 터질 일을 컴파일러가 먼저 잡는 셈입니다. 그런데 자바 메서드가 돌려준 값이면 이야기가 달라집니다.
fun main() {
val home = System.getenv("NO_SUCH_VAR")
println("got it")
println(home.length)
}
val home = ...— 다시 대입할 수 없는 변수home을 선언합니다. 타입을 안 적으면 오른쪽 값에서 타입을 정합니다System.getenv(이름)— 자바 표준 라이브러리의 메서드입니다. 환경 변수 값을 돌려주고, 그런 이름의 변수가 없으면null을 돌려줍니다home.length— 코틀린에서 문자열 길이는length()메서드가 아니라 괄호 없는length프로퍼티입니다
NO_SUCH_VAR 는 없는 환경 변수라 getenv 가 null 을 돌려줍니다. 이 파일을 First.kt 로 저장해 코틀린 컴파일러 kotlinc 로 컴파일하고 kotlin 명령으로 돌렸습니다. -d out 은 결과물을 out 폴더에 쓰라는 옵션입니다.
$ kotlinc First.kt -d out
$ kotlin -cp out FirstKt
got it
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "home" is null
at FirstKt.main(First.kt:4)
at FirstKt.main(First.kt)
컴파일은 경고 하나 없이 지나갔습니다. 실행하니 got it 이 찍히고, 4번째 줄 home.length 에서 NullPointerException 이 났습니다. 자바에서 null 에 length() 를 부를 때 나는 바로 그 예외입니다.
String 에 null 을 못 넣게 막는다던 컴파일러가 이번에는 아무 말이 없었습니다. 자바 메서드는 반환값이 null 일 수 있는지를 타입에 적지 않기 때문입니다. 컴파일러는 모르는 것을 모르는 채로 통과시켰습니다.
이 편은 그 「모르는 채로」가 어떤 타입인지부터 봅니다. 1절이 그 타입의 이름, 2절이 자바 쪽에서 알려 주는 방법, 3절이 반대 방향, 4절이 실무에서 받는 규칙, 5절이 컴파일러가 넣는 검사의 비용입니다. 출력은 전부 Kotlin 2.4.10, JDK 21 에서 직접 돌린 결과입니다.
1. String! — 컴파일러에게 타입을 물어보는 법
home 에 타입을 안 적었으니 컴파일러가 무슨 타입으로 정했는지 소스만 봐서는 안 보입니다. 이 절에서는 컴파일러에게 직접 물어봅니다. 방법은 일부러 틀린 타입을 적는 것입니다. 그러면 에러 문구에 컴파일러가 생각하는 타입이 찍힙니다.
fun main() {
val x: Int = System.getenv("HOME")
}
val x: Int— 변수 이름 뒤 콜론 다음이 타입입니다. 문자열이 올 곳에 정수 타입Int를 적었습니다
$ kotlinc Type.kt -d out
Type.kt:2:16: error: initializer type mismatch: expected 'Int', actual 'String!'.
val x: Int = System.getenv("HOME")
^
에러는 초깃값의 타입이 선언한 타입과 다르다는 뜻입니다. 볼 것은 뒤쪽 actual 'String!' 입니다. String 도 String? 도 아닌, 느낌표가 붙은 String! 입니다.
! 는 「널일 수도 있고 아닐 수도 있다」는 표시입니다. 자바에서 넘어와 널 여부를 모르는 타입을 플랫폼 타입(platform type)이라 부르고, 컴파일러는 에러 문구에서 이를 T! 로 적습니다(공식 문서). KB 04 코틀린에서 자바 부르기 — 게터가 프로퍼티로 보인다 에서 자바 리스트가 (Mutable)List<String!>! 로 찍혔던 것과 같은 표시입니다.
String! 을 코드에 직접 적을 수는 없습니다. 에러 문구에만 나오는 표기입니다. 그래서 이 편처럼 틀린 타입을 적어 에러로 확인하는 것이 소스에서 플랫폼 타입을 알아보는 가장 빠른 방법입니다.
String! 은 String 에도 String? 에도 들어간다
플랫폼 타입의 쓰임새는 받는 쪽이 고를 수 있다는 데 있습니다. 같은 null 값을 String? 변수와 String 변수에 차례로 담았습니다.
fun main() {
val a: String? = System.getenv("NO_SUCH_VAR")
println(a)
val b: String = System.getenv("NO_SUCH_VAR")
println(b)
}
둘 다 에러 없이 컴파일됐습니다. 코틀린끼리라면 String? 값을 String 에 넣는 4번째 줄은 컴파일 에러입니다. 플랫폼 타입이라서 양쪽 다 통과했습니다.
$ kotlinc Both.kt -d out
$ kotlin -cp out BothKt
null
Exception in thread "main" java.lang.NullPointerException: getenv(...) must not be null
at BothKt.main(Both.kt:4)
at BothKt.main(Both.kt)
String? 에 담은 a 는 null 을 찍었습니다. String 에 담은 b 는 담는 4번째 줄에서 멈췄습니다. 쓰기도 전입니다.
메시지도 0절과 다릅니다. getenv(...) must not be null 은 「getenv 가 돌려준 값이 null 이면 안 된다」는 뜻입니다. 플랫폼 값을 널 불가 타입에 담으면 컴파일러가 그 대입 줄에 널 검사를 넣습니다. 공식 문서도 「If you choose a non-nullable type, the compiler will emit an assertion upon assignment」라고 적습니다.
받는 방식에 따라 null 이 들어왔을 때 멈추는 줄이 이렇게 갈립니다.
| 받는 방식 | 변수 타입 | null 이 들어오면 |
|---|---|---|
타입을 안 적는다 (val home = ...) |
String! |
쓰는 줄에서 Cannot invoke "String.length()" |
String 으로 적는다 |
String |
담는 줄에서 getenv(...) must not be null |
String? 로 적는다 |
String? |
멈추지 않는다. 대신 쓸 때 컴파일러가 널 처리를 요구한다 |
첫 줄이 가장 나쁩니다. null 이 변수를 타고 멀리 흘러간 다음 엉뚱한 곳에서 터질 수 있습니다. 둘째 줄은 적어도 값이 들어온 줄에서 멈추고, 메시지에 어느 메서드의 반환값인지가 찍힙니다.
정리하면, 자바 메서드가 돌려준 값은 String! 이라는 플랫폼 타입이 되고, 받는 쪽이 String 이든 String? 든 골라 담을 수 있습니다. 고르지 않으면 코틀린의 널 검사가 통째로 빠집니다.
2. 자바가 @Nullable·@NotNull 을 붙이면 컴파일 에러로 바뀐다
플랫폼 타입이 생기는 까닭은 자바 쪽에 정보가 없어서입니다. 그러면 자바 쪽이 정보를 주면 됩니다. 이 절에서는 자바 메서드에 널 여부를 알리는 표식을 붙였을 때 코틀린 컴파일러가 무엇을 바꾸는지 봅니다.
애노테이션은 @이름 꼴로 코드에 붙이는 표식입니다. 널 애노테이션은 「이 값은 null 일 수 있다」(@Nullable)나 「절대 null 이 아니다」(@NotNull)를 적어 두는 표식입니다. 자바 컴파일러는 이것을 검사하지 않고, 읽는 도구가 따로 있습니다. 코틀린 컴파일러가 그 도구 가운데 하나입니다.
자바 클래스 Users 에 같은 일을 하는 메서드 셋을 만들었습니다. 애노테이션만 다릅니다.
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class Users {
public static String plain(int id) {
return id == 1 ? "kim" : null;
}
public static @Nullable String nickname(int id) {
return id == 1 ? "kim" : null;
}
public static @NotNull String name(int id) {
return "user" + id;
}
}
org.jetbrains.annotations— 코틀린을 만든 JetBrains 의 애노테이션 패키지입니다. 코틀린 배포판에annotations-13.0.jar로 들어 있어 따로 받지 않았습니다plain·nickname—id가 1 이면"kim", 아니면null을 돌려줍니다.nickname에만@Nullable이 붙었습니다name— 언제나"user" + id를 돌려주고@NotNull이 붙었습니다
코틀린에서 세 메서드의 반환값에 바로 .length 를 불렀습니다. 4번째 줄은 nickname 의 값을 String 에 담습니다.
fun main() {
println(Users.plain(2).length)
println(Users.nickname(2).length)
val n: String = Users.nickname(1)
println(Users.name(2).length)
}
$ javac -cp annotations-13.0.jar -d out Users.java
$ kotlinc -cp out Use.kt -d kout
Use.kt:3:30: error: only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type 'String?'.
println(Users.nickname(2).length)
^
Use.kt:4:19: error: initializer type mismatch: expected 'String', actual 'String?'.
val n: String = Users.nickname(1)
^
에러는 3·4번째 줄, @Nullable 이 붙은 nickname 쪽에서만 났습니다. 애노테이션이 없는 plain 은 0절처럼 통과했고, @NotNull 이 붙은 name 도 통과했습니다.
에러 문구의 타입을 보면 까닭이 보입니다. nickname 의 반환 타입이 String! 이 아니라 String? 입니다. @Nullable 하나로 플랫폼 타입이 코틀린의 널 가능 타입이 됐습니다. 그래서 1절에서 통과하던 String 대입도 이제는 막힙니다.
3번째 줄의 에러는 널 가능 타입에 . 으로 멤버를 부를 수 없다는 뜻입니다. 대신 쓰라는 ?. 와 !!. 가운데 ?. 를 써서 고쳤습니다.
fun main() {
println(Users.nickname(1)?.length) // 3
println(Users.nickname(2)?.length) // null
println(Users.name(2).length) // 5
}
?.— 안전 호출입니다. 왼쪽이null이면 멤버를 부르지 않고 식 전체가null이 됩니다. 자바의x == null ? null : x.length()를 줄인 것입니다!!.— 「null이 아니라고 내가 보장한다」는 단언입니다.null이면 그 줄에서NullPointerException이 납니다. 널 안전성 편(KB 16)에서 자세히 다룹니다
$ kotlinc -cp out Fixed.kt -d out
$ kotlin -cp out FixedKt
찍힌 값은 오른쪽 주석에 붙여 뒀습니다. null 이 들어온 두 번째 줄도 예외 없이 null 을 찍었습니다. 실행에서 터질 일이 컴파일 에러로 앞당겨졌고, 그 에러를 고치는 과정에서 널 처리가 들어갔습니다. 이것이 애노테이션이 주는 이득입니다.
어느 애노테이션을 읽나
@Nullable 이라는 이름은 여러 라이브러리에 있습니다. 코틀린 컴파일러가 알아보는 패키지는 공식 문서에 목록으로 있습니다(공식 문서).
| 애노테이션 | 패키지 | 기본 처리 |
|---|---|---|
| JetBrains | org.jetbrains.annotations |
에러 |
| JSpecify | org.jspecify.annotations |
에러 (-Xjspecify-annotations=strict 가 기본값) |
| JSR-305 | javax.annotation |
기본 -Xjsr305=warn. 단 @Nonnull·@Nullable·@CheckForNull 은 설정과 상관없이 적용 |
| 그 밖 | Android · FindBugs · Eclipse · Lombok · RxJava 3 · Vert.x | 공식 문서 목록 참고 |
위 Users 와 같은 모양으로 JSpecify 1.0.0 의 @Nullable, JSR-305 3.0.2 의 @Nullable·@CheckForNull 을 붙인 메서드도 만들어 봤습니다. 셋 다 .length 에서 위와 같은 only safe (?.) or non-null asserted (!!.) calls are allowed 에러를 냈습니다.
백엔드에서 가장 자주 만나는 것은 스프링입니다. 스프링 프레임워크 문서는 API 전체에 JSpecify 애노테이션을 달아 두었다고 적습니다(스프링 문서). 그래서 스프링 API 는 대부분 플랫폼 타입이 아니라 String 이나 String? 로 보입니다.
정리하면, 자바 쪽 널 애노테이션은 플랫폼 타입을 코틀린의 String 이나 String? 로 바꿔 줍니다. @Nullable 이 붙은 값을 검사 없이 쓰면 실행 대신 컴파일에서 멈춥니다.
3. 반대 방향은 안전하다 — 자바가 null 을 넘기면 들어오는 순간 멈춘다
지금까지는 코틀린이 자바 값을 받는 방향이었습니다. 이번에는 거꾸로 자바가 코틀린 함수를 부르면서 null 을 넘깁니다. 자바에는 널 불가 타입이 없으니 javac 는 이것을 막을 방법이 없습니다.
코틀린 쪽에 함수 둘을 만들었습니다. greet 는 널 불가 String 을, greetOrNot 은 널 가능 String? 을 받습니다.
fun greet(name: String) {
println("in greet")
println("hi $name")
}
fun greetOrNot(name: String?) {
println("hi $name")
}
fun greet(name: String)— 함수 선언입니다. 매개변수도 이름 뒤에 콜론, 그 뒤에 타입을 씁니다"hi $name"— 문자열 템플릿입니다.$이름에 그 변수의 값이 끼워집니다.null이면null이라는 글자가 들어갑니다
파일 바로 아래에 쓴 함수는 파일 이름 + Kt 라는 클래스의 static 메서드가 됩니다. 그래서 자바에서는 Greet.kt 의 함수를 GreetKt.greet(...) 로 부릅니다.
public class CallJ {
public static void main(String[] args) {
GreetKt.greetOrNot(null);
GreetKt.greet(null);
}
}
$ kotlinc Greet.kt -d out
$ javac -cp out -d out CallJ.java
$ kotlin -cp out CallJ
hi null
Exception in thread "main" java.lang.NullPointerException: Parameter specified as non-null is null: method GreetKt.greet, parameter name
at GreetKt.greet(Greet.kt)
at CallJ.main(CallJ.java:4)
javac 는 두 호출 모두 에러 없이 컴파일했습니다. 실행 결과는 갈렸습니다.
greetOrNot(null) 은 받겠다고 한 대로 hi null 을 찍었습니다. greet(null) 은 in greet 도 찍지 못하고 멈췄습니다. 함수 본문의 첫 줄보다 먼저 검사가 돈 것입니다. 예외가 지나온 호출 경로를 찍은 목록, 곧 스택 트레이스의 at GreetKt.greet(Greet.kt) 에도 본문의 줄 번호가 찍히지 않았습니다.
메시지 Parameter specified as non-null is null: method GreetKt.greet, parameter name 은 어느 함수의 어느 매개변수에 null 이 들어왔는지를 이름으로 알려 줍니다. 공식 문서는 이것을 「Kotlin generates runtime checks for all public functions that expect non-nulls」라고 설명합니다(공식 문서). 자바에서 부를 수 있는 함수라면 널 불가 매개변수마다 입구에 검사를 둔다는 뜻입니다.
정리하면, 이 방향은 코틀린이 스스로 지킵니다. 자바가 null 을 넘기면 함수 본문이 돌기 전에 멈추고, 메시지가 매개변수 이름까지 짚어 줍니다. 구멍은 1·2절의 방향, 코틀린이 자바 값을 받는 쪽에만 있습니다.
4. 실무 규칙 — 경계에서 한 번 받아 널 가능 타입으로 좁힌다
플랫폼 타입의 위험은 1절 표의 첫 줄, 타입을 안 적어서 String! 이 흘러 다니는 것이었습니다. 이 절에서는 그것을 막는 규칙 하나와, 그 규칙이 없을 때 생기는 일을 봅니다.
규칙은 이렇습니다. 자바 값이 코틀린으로 들어오는 곳, 곧 경계에서 한 번 타입을 적어 받는다. 애노테이션이 없고 null 이 올 수 있는 값이면 String? 로 받습니다.
타입을 안 적으면 함수 밖으로 번진다
타입을 안 적은 변수는 한 함수 안에 머뭅니다. 그런데 함수의 반환 타입을 안 적으면 플랫폼 타입이 함수 밖으로 나갑니다.
fun home() = System.getenv("HOME")
fun main() {
val x: Int = home()
}
fun home() = ...— 등호 뒤의 값을 바로 돌려주는 한 줄 함수입니다. 반환 타입을 안 적으면 그 값에서 정합니다
1절의 방법대로 틀린 타입 Int 를 적어 물었습니다.
$ kotlinc Leak.kt -d out
Leak.kt:4:16: error: initializer type mismatch: expected 'Int', actual 'String!'.
val x: Int = home()
^
home() 은 코틀린 함수인데 반환 타입이 String! 입니다. 코틀린 함수가 플랫폼 타입을 돌려주고 있습니다. 이 함수를 부르는 곳마다 자바 메서드를 직접 부른 것과 같은 상태가 되고, 어디서 null 을 걸러야 하는지가 흐려집니다.
고치는 법은 반환 타입 한 단어입니다. fun home(): String? = System.getenv("HOME") 이라고 적으면 부르는 쪽은 처음부터 String? 을 받고, 널 처리를 안 하면 컴파일러가 막습니다.
받자마자 기본값으로 좁힌다
널 가능 타입으로 받은 값은 대개 곧바로 널이 아닌 값으로 바꿔서 넘깁니다. 환경 변수로 포트 번호를 받는 함수를 예로 들었습니다. 변수가 없거나 숫자가 아니면 8080 을 씁니다.
fun port(): Int {
val raw: String? = System.getenv("APP_PORT")
return raw?.toIntOrNull() ?: 8080
}
fun main() {
println(port())
}
: Int— 괄호 뒤 콜론 다음은 반환 타입입니다. 이 함수 밖으로는 널 불가Int만 나갑니다toIntOrNull()— 문자열을 정수로 바꾸고, 숫자가 아니면 예외 대신null을 돌려줍니다?:— 엘비스 연산자입니다. 왼쪽이null이면 오른쪽 값을 씁니다. 자바의x != null ? x : 8080에 해당합니다
환경 변수를 없는 채로, 숫자로, 숫자 아닌 글자로 바꿔 가며 세 번 돌렸습니다.
$ kotlinc Config.kt -d out
$ kotlin -cp out ConfigKt
8080
$ APP_PORT=9090 kotlin -cp out ConfigKt
9090
$ APP_PORT=abc kotlin -cp out ConfigKt
8080
없을 때와 abc 일 때 모두 예외 없이 8080 이 나왔습니다. null 을 다루는 코드는 port() 안의 한 줄뿐이고, 이 함수를 부르는 쪽은 Int 만 봅니다. 자바 값의 불확실함을 경계의 함수 하나에 가둔 것입니다.
어떤 타입으로 받을지 고르는 순서
자바 메서드 하나를 코틀린에서 처음 부를 때 따라갈 순서를 그렸습니다.
flowchart TD
A[자바 메서드의 반환값] --> B{"에러 문구에<br/>String! 이 찍히나"}
B -- "아니다: String 이나 String?" --> C[애노테이션이 정해 준 타입을 그대로 쓴다]
B -- "그렇다: 플랫폼 타입" --> D{"null 이 올 수 있나<br/>문서·소스로 확인"}
D -- "올 수 있다·모른다" --> E["String? 로 받고<br/>?. ?: 로 바로 좁힌다"]
D -- "절대 안 온다" --> F["String 으로 받는다<br/>틀리면 대입 줄에서 멈춘다"]
모를 때는 String? 쪽이 기본입니다. 틀려도 컴파일러가 널 처리를 하나 더 요구하는 것이 전부입니다. 반대로 String 으로 받았는데 null 이 오면 1절에서 본 대로 실행 중에 멈춥니다. 그래도 그때는 타입을 안 적은 경우와 달리 값이 들어온 줄에서 멈춥니다.
정리하면, 자바 값은 들어오는 곳에서 한 번 타입을 적어 받고, 함수의 반환 타입도 적어서 플랫폼 타입이 밖으로 새지 않게 합니다. 널이 올 수 있으면 String? 로 받아 그 함수 안에서 좁힙니다.
5. 성능 — 컴파일러가 넣는 널 검사는 신경 쓰지 않아도 된다
1절과 3절에서 컴파일러가 몰래 넣은 널 검사를 두 번 만났습니다. 같은 검사가 한 군데 더 들어갑니다. 이 절에서는 그 검사가 어디에 들어가는지 모으고, 비용이 얼마나 되는지, 끌 수 있는지를 봅니다.
| 검사가 들어가는 곳 | 본 절 | 멈출 때 메시지 |
|---|---|---|
| 플랫폼 값을 널 불가 타입에 담는 줄 | 1절 | getenv(...) must not be null |
@NotNull 이 붙은 자바 메서드의 반환값 |
아래 | name(...) must not be null |
| 자바에서 부를 수 있는 함수의 널 불가 매개변수 | 3절 | Parameter specified as non-null is null |
둘째 줄은 아직 보지 않았습니다. 자바가 @NotNull 을 붙여 놓고 실제로는 null 을 돌려주면 어떻게 될까요.
import org.jetbrains.annotations.NotNull;
public class Liar {
public static @NotNull String name() {
return null;
}
}
fun main() {
val n = Liar.name()
println("got it")
println(n.length)
}
$ kotlinc -cp out UseLiar.kt -d out
$ kotlin -cp out UseLiarKt
Exception in thread "main" java.lang.NullPointerException: name(...) must not be null
at UseLiarKt.main(UseLiar.kt:2)
at UseLiarKt.main(UseLiar.kt)
got it 이 찍히지 않았습니다. Liar.name() 을 부른 2번째 줄에서 바로 멈췄습니다. 코틀린은 @NotNull 을 믿고 n 을 String 으로 다루지만, 값을 받는 줄에서 한 번 확인합니다. 애노테이션이 틀렸어도 null 이 코틀린 코드 안쪽으로 흘러 들어가지 않습니다.
검사 하나는 비교 한 번이다
세 검사는 모두 같은 일을 합니다. 값이 null 인지 한 번 비교하고, null 이면 예외를 던집니다. KB 06 숫자 타입 — Int 하나 뒤의 int 와 Integer 에서 디컴파일한 결과에 매개변수 검사가 Intrinsics.checkNotNullParameter(xs, "xs") 라는 한 줄로 찍혀 있었습니다. 새 객체를 만들지도 않고 값을 복사하지도 않습니다.
그래서 신경 쓰지 않아도 됩니다. 함수 하나가 도는 동안 하는 일, 곧 문자열을 만들거나 컬렉션을 돌거나 DB 를 부르는 일에 비하면 비교 한 번은 보이지 않는 크기입니다. 이 편에서는 따로 재지 않았고, 숫자는 싣지 않습니다.
끌 수는 있지만 끄면 잃는 것이 크다
kotlinc 에는 이 검사를 빼는 옵션이 있습니다. kotlinc -X 로 목록을 보면 이렇게 나옵니다.
| 옵션 | 설명 (kotlinc -X 원문) |
|---|---|
-Xno-param-assertions |
Don't generate not-null assertions on parameters of methods accessible from Java. |
-Xno-call-assertions |
Don't generate not-null assertions for arguments of platform types. |
3절의 Greet.kt 를 -Xno-param-assertions 로 다시 컴파일하고, 같은 CallJ 로 null 을 넘겼습니다.
$ kotlinc -Xno-param-assertions Greet.kt -d out2
$ javac -cp out2 -d out2 CallJ.java
$ kotlin -cp out2 CallJ
hi null
in greet
hi null
예외가 사라졌습니다. greet 는 본문으로 들어가 in greet 를 찍고, 널 불가라고 적은 name 에 담긴 null 로 hi null 을 찍었습니다. String 이라고 적은 변수에 null 이 들어 있는데 아무도 알려 주지 않습니다. 이 값이 .length 까지 가면 그때 가서 name 과 상관없어 보이는 줄에서 터집니다.
끄면 얻는 것은 호출마다 비교 한 번이고, 잃는 것은 null 이 들어온 함수와 매개변수 이름을 짚어 주는 메시지입니다. 대부분의 코드에서 이 교환은 손해입니다. 옵션은 있다는 것만 알아 두면 됩니다.
정리하면, 컴파일러가 넣는 널 검사는 대입 줄·자바 반환값·매개변수 입구에 들어가는 null 비교 한 번이고, 객체를 만들지 않아 비용을 따질 일이 없습니다. 끄는 옵션이 있지만 끄면 null 이 조용히 코틀린 코드 안으로 들어옵니다.
6. 한 장 요약
자바 메서드가 돌려준 값은 널 여부를 모르는 플랫폼 타입(
String!)이 된다. 컴파일러는 이 값을 검사 없이 통과시키므로, 타입을 안 적고 쓰면 자바와 같은 곳에서NullPointerException이 난다. 자바 쪽에@Nullable·@NotNull이 있으면String?·String이 되어 컴파일 에러로 잡힌다. 반대로 자바가 코틀린 함수에null을 넘기면 함수 입구에서 바로 멈춘다. 실무에서는 자바 값이 들어오는 경계에서 한 번 타입을 적어 받고, 모르면String?로 받아 곧바로 좁힌다. 컴파일러가 넣는 널 검사는 비교 한 번이라 비용을 신경 쓸 필요가 없다.
| 상황 | 코틀린에서 보이는 타입 | null 이 오면 |
확인한 방법 |
|---|---|---|---|
| 애노테이션 없는 자바 반환값을 타입 없이 받음 | String! |
쓰는 줄에서 Cannot invoke ... |
실행 · 일부러 틀린 타입의 에러 문구 |
같은 값을 String 으로 받음 |
String |
담는 줄에서 ...(...) must not be null |
실행 |
같은 값을 String? 로 받음 |
String? |
멈추지 않음. 쓸 때 ?.·?: 를 요구 |
컴파일 · 실행 |
자바에 @Nullable |
String? |
검사 없이 쓰면 컴파일 에러 | kotlinc 에러 |
자바에 @NotNull |
String |
받는 줄에서 ...(...) must not be null |
실행 |
자바가 코틀린 String 매개변수에 null |
— | 함수 입구에서 Parameter specified as non-null is null |
실행 |
| 반환 타입을 안 적은 코틀린 함수가 자바 값을 돌려줌 | String! 이 밖으로 샌다 |
부르는 쪽마다 0절과 같은 상태 | kotlinc 에러 문구 |
-Xno-param-assertions |
— | 매개변수 검사가 빠져 null 이 본문으로 들어간다 |
실행 |
관련 항목
플랫폼 타입과 맞세워지는 코틀린의 널 타입
널 불가 타입 · 널 가능 타입 · 널 안전성 · 플랫폼 타입 · 타입 추론
플랫폼 타입을 다루는 연산자
안전 호출 연산자 · 엘비스 연산자 · 널 아님 단언 · 스마트 캐스트
플랫폼 타입을 코틀린 타입으로 바꾸는 널 애노테이션
널 애노테이션 · 애노테이션 · JetBrains Annotations · JSpecify · JSR-305 · Lombok
널 검사가 실패할 때 던지는 예외와 그 흔적
NullPointerException · 예외 · 스택 트레이스 · Helpful NullPointerExceptions
코틀린과 자바가 서로 부르는 상호 운용 경계
자바 상호 운용성 · 톱레벨 함수 · Java · Kotlin · JVM · 스프링 프레임워크