본문 바로가기
Kotlin

[Effective Kotlin] 3 - 최대한 플랫폼 타입을 사용하지 말라

by 매운돌 2023. 2. 12.

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가 발생할 수 있습니다.

 

  • 플랫폼 타입은 사용하는 부분만 위험할 뿐 아니라, 이를 활용하는 곳까지 영향을 주는 위험한 코드입니다.
  • 가능하면 플랫폼 타입은 제거하는게 좋고, 그러기 힘들다면 어노테이션을 활용하는게 좋습니다.