반응형
Notice
Recent Posts
Recent Comments
Link
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
Tags
more
Archives
Today
Total
관리 메뉴

안드로이드 개발자 노트

[이펙티브 코틀린] Item3. 최대한 플랫폼 타입을 사용하지 말라 본문

Kotlin/이펙티브 코틀린

[이펙티브 코틀린] Item3. 최대한 플랫폼 타입을 사용하지 말라

어리둥절범고래 2023. 8. 27. 14:55
반응형

널 안정성(null-safety)은 코틀린의 주요 기능 중 하나입니다.

null-safety 메커니즘이 없는 자바, C등의 프로그래밍 언어와 코틀린을 연결해서 사용할 때는 널 포인터 예외(Null-Pointer Exception, NPE)가 발생할 수 있습니다.

 

@Nullable 어노테이션이나 @NotNull 어노테이션이 붙어있다면 코틀린에서 활용할때는 String?과 String으로 변경하면 됩니다.

자바에서 모든 것이 null일 수 있으므로 최대한 안전하게 접근한다면, 이를 nullable로 가정하고 다루어야 합니다.

하지만 메서드가 null을 리턴하지 않을 것이 확실한 경우 not-null 단정을 나타내는 !!를 붙입니다.

 

/ 자바
public class UserRepo{
    public List<User> getUsers() {
        //...
    }
}

// 코틀린
val users: List<User> = UserRepo().users!!.fillterNotNull()

// List<List<User>>를 리턴한다면? 더 복잡해진다.
val users: List<List<User>> = UserRepo().groupedUsers!!.map { filterNotNull() }

 

코틀린은 자바 등의 다른 프로그래밍 언어에서 넘어온 타입들을 특수하게 다루며, 이러한 타입을 플랫폼 타입(platform type)이라고 부릅니다.

플랫폼 타입이란, 다른 프로그래밍 언어에서 전달되어서 nullable인지 아닌지 알 수 없는 타입을 말합니다.

플랫폼 타입은 String! 처럼 타입 이름 뒤에 ! 기호를 표기합니다.

 

// 자바
public class UserRepo {
    public User getUser() {
        //...
    }
}

// 코틀린
val repo = UserRepo()
val user1 = repo.user        // user1의 타입은 User!
val user2: User = repo.user  // user2의 타입은 User
val user3: User? = repo.user // user3의 타입은 User?

val users: List<User> = UserRepo().users
val users: List<List<User>> = UserRepo().groupedUsers

 

그러나 여전히 null이 아니라고 생각되는 것이 null일 가능성이 있으므로 위험합니다.

자바 코드를 직접 조작할 수 있다면 가능한 @Nullable과 @NotNull 어노테이션을 붙여서 사용하면 좋습니다.

 

// 자바 
import org.jetbrains.annotations.NotNull;

public class UserRepo {
    public @NotNull User getUser() {
        //...
    }
}

 

결국에 플랫폼 타입은 안전하지 않으므로, 최대한 빨리 제거하는 것이 좋습니다.

왜 그런지 간단한 예로 statedType과 platformType의 동작을 살펴보겠습니다.

 

// 자바
public class JavaClass {
    public String getValue() {
        return null;
    }
}

// 코틀린
fun statedType() {
    val value: String = JavaClass().value // NPE
    // ...
    println(value.length)
}

fun platformType() {
    val value = JavaClass().value
    // ...
    println(value.length) // NPE
}

 

두 가지 모두 NPE(Null-Pointer Exception)가 발생합니다.

 

statedType에서는 자바에서 값을 가져오는 위치에서 NPE가 발생합니다.

이 위치에서 오류가 발생하면, null이 아니라고 예상을 했지만 null이 나온다는 것을 굉장히 쉽게 알 수 있습니다.


platformType에서는 값을 활용할 때 NPE가 발생합니다.

객체를 사용한다고 해서 NPE가 발생될 거라고 생각하지는 않으므로, 오류를 찾는데 굉장히 오랜 시간이 걸리게 될 것입니다.

 

 

추가적인 예로, 인터페이스에서 플랫폼 타입을 사용한다면 다음과 같은 문제가 발생합니다.

 

interface UserRepo {
    fun getUserName() = JavaClass().value // inferred 타입(추론된 타입)
}

 

이 경우 플랫폼 타입은 inferred 타입(추론된 타입)이 되며, 이는 누구나 nullable 여부를 지정할 수 있다는 것이 됩니다.

어떤 사람이 이를 활용해서 nullable을 리턴하게 했는데 사용하는 사람이 nullable이 아닐거라고 받아들였다면(not-null일거라 생각했다면), 문제가 됩니다.

 

class RepoImpl: UserRepo { // A: nullable을 리턴하게 해야지
    override fun getUserName(): String? {
        return null
    }
}

fun main() {
    val repo: UserRepo = RepoImpl()
    // B: not-null 이겠지?
    val text: String = repo.getUserName() // 런타임 NPE
    print("User name length is ${text.length}")
}

 

 

 

 


정리

  • 다른 프로그래밍 언어에서 와서 nullable 여부를 알 수 없는 타입을 플랫폼 타입이라고 부른다.
  • 이러한 플랫폼 타입을 사용하는 코드는 해당 부분만 위험할 뿐만 아니라, 이를 활용하는 곳까지 영향을 줄 수 있는 위험한 코드다.
  • 이런 코드를 사용하고 있다면 빨리 해당 코드를 제거하는 것이 좋다.
  • 연결되어 있는 자바 생성자, 메서드, 필드에 nullable 여부를 지정하는 어노테이션을 활용하는 것이 좋다.
반응형