Kotlin의 null 안정성(null-safty)의 기능으로 인해 Java에서 자주 볼 수 있었던, NPE는 많이 줄어들게 되었습니다.
하지만 null-safty 메카니즘이 없는 다른 언어들(C, Java, ...)와 Kotlin을 연결하여 사용하게 되면, 이러한 예외가 발생할 수 있습니다. 따라서 최대한 안전하게 접근한다면 다른 언어에서 모든 것은 nullable로 가정하고 다루어야 합니다.
nullable과 관련하여 자주 문제가 되는 부분은 Java의 제네릭 타입입니다.
만약 Java API에서 List<Use>를 리턴하고 어노테이션을 따로 붙이지 않았다면, Kotlin에서는 모든 타입을 nullable로 다뤄야 하기 때문에 List내부의 객체들에 대해서 null체크를 수행해야 합니다. 만약 List<List<User>>를 리턴한다면 아래 처럼 복잡한 형태가 되게 됩니다.
val users: List<List<User>> = UserRepo().groupedUsers!!.map { it!!.filterNotNull() }
List는 적어도 map와 filterNotNull등의 메서드를 제공하지만, 다른 제네릭 타입이라면 null을 확인하는거 자체가 복잡한 일이 되버립니다. 그래서 Kotlin에서는 다른 언어에서 넘어온 타입들을 특수하게 플랫폼 타입이라고 부릅니다. 그리고 타입 이름 뒤에 ! 기호를 붙여서 표기합니다. (단, 이러한 어노테이션은 직접적으로 코드에 나타나지 않습니다.)
// Java
public class UserRepo {
public User getUser() {
// ...
}
}
// Kotlin
val repo = UserRepo()
val user1 = repo.user // user1의 타입은 User!
val user2: User = repo.user // user2의 타입은 User
val user3: User? = repo.user // user3의 타입은 User?
위와 같은 코드를 사용할 수 있으므로 이전에 언급했던 문제가 사라집니다.
val user: List<User> = USerRepo().users
val users: List<List<User>> = UserRepo().groupedUsers
하지만 여전히 null일 가능성은 남아 있습니다. 따라서 플랫폼 타입을 사용할 때는 주의를 기울여야 하고, 설계자가 명시적으로 어노테이션으로 표시하거나 주석으로 달아주지 않으면 나중에 언제든지 동작이 다른 개발자에 의해 변경될 수 있습니다.
즉, Java를 Kotlin과 같이 사용할 때, 가능하면 @Nullable과 @NotNull 어노테이션을 사용해야 합니다.
(안드로이드 main 개발 언어를 Kotlin으로 변경할 때, 가장 중요한 변경 사항입니다.)
아래의 Kotlin 코드에서는 NPE가 발생합니다.
// Java
public class JavaClass {
public String getValue() {
return null;
}
}
// Kotlin
fun statedType() {
val value: String = JavaClass().value // NPE
//...
println(value.length)
}
fun platformType() {
val value = JavaClass().value
// ...
println(value.length) // NPE
}
플랫폼 변수는 한 두번 안전하게 사용했다고 하더라도, 이후에 다른 사람이 사용할 때 NPE를 발생시킬 가능성이 존재합니다. 또한 이러한 문제는 타입 검사기로 검출해 주 수도 없습니다.
interface UserRepo {
fun getUserName() = JavaClass().value
}
class RepoImpl: UserRepo {
override fun getUserName(): String? {
return null
}
}
fun main() {
val repo: UserRepo = RepoImpl()
val text: String = repo.getUserName() // NPE
print("User name length is ${text.length}")
}
위와 같이 interface에서 inferred(추론된)타입이 플랫폼 타입이라면 구현부에서 nullable하게 구현했다면 NPE가 발생할 수 있습니다.
- 플랫폼 타입은 사용하는 부분만 위험할 뿐 아니라, 이를 활용하는 곳까지 영향을 주는 위험한 코드입니다.
- 가능하면 플랫폼 타입은 제거하는게 좋고, 그러기 힘들다면 어노테이션을 활용하는게 좋습니다.
'Kotlin' 카테고리의 다른 글
[Effective Kotlin] 7 - 결과 부족이 발생할 경우 null과 Failure를 사용하라 (0) | 2023.03.12 |
---|---|
[Effective Kotlin] 6 - 사용자 정의 오류보다는 표준 오류를 사용하라 (0) | 2023.03.05 |
[Effective Kotlin] 5 - 예외를 활용해 코드에 제한을 걸어라 (0) | 2023.02.25 |
[Effective Kotlin] 4 - inferred 타입으로 리턴하지 말라 (0) | 2023.02.19 |
[Effective Kotlin] 2 - 변수의 스코프를 최소화하라 (0) | 2023.02.04 |