devseop08 님의 블로그

[OOP] 2. 널이 될 수 있는 값 본문

Language/Kotlin

[OOP] 2. 널이 될 수 있는 값

devseop08 2025. 6. 4. 09:27
  • 코드 가독성을 살려주는 코틀린 핵심 기능 : 널이 될 수 있는 타입 지원

7.1 NullpointerException을 피하고 값이 없는 경우 처리: 널 가능성

  • 코틀린을 포함한 최신 언어에서 null에 대한 접근 방법은 가능한 이 문제를 실행 시점에서 컴파일 시점으로 옮기는 것이다.
  • 널이 될 수 있는지 여부를 타입 시스템에 추가함 => 여러 가지 오류를 컴파일 시 미리 감지
  • 코틀린에서 null이 될 수 있는 값을 어떻게 표기하고 코틀린이 제공하는 도구가 그런 null이 될 수 있는 값을 어떻게 처리하는지 살펴보자
  • 널이 될 수 있는 타입 측면에서 코틀린과 자바 코드를 어떻게 함께 사용할 수 있는지 살펴보자

7.2 널이 될 수 있는 타입으로 널이 될 수 있는 변수 명시

  • 코틀린과 자바의 첫 번째이자 가장 중요한 차이는 코틀린 타입 시스템이 널이 될 수 있는 타입을 명시적으로 지원한다는 것이다.
  • 코틀린은 자바와 달리 null이 될 수 있는 타입과 null 될 수 없는 타입을 명시적으로 명확하게 구분하다.
  • 타입 이름 뒤에 물음표(?)가 붙지 않은 타입은 해당 타입이 절대 null이 될 수 없는 타입임을 명시적으로 알려준다
fun strLen(s: String) = s.length 
// s가 절대 null이 될 수 없는 타입의 값임을 명시했기 때문에 s에 참조 연산(.)이 가능하다. 
// 실행 시점에 s에 대한 참조 연산으로 인한 NullPointerException 에러가 발생하지 
// 않을 것임을 확신할 수 있다.
  • 타입 이름 뒤에 물음표(?)가 붙은 타입은 해당 타입이 null이 될 수도 있는 타입임을 명시적으로 알려준다
fun strLen(s: String?) = s.length
// ERROR: Null cant not be a value of a non-null type String
// s가 null이 될 수도 있는 타입의 값임을 명시했기 때문애 s에 참조 연산이 불가능함을
// 컴파일 타임에 확인할 수 있다.
  • 물음표가 없는 타입은 어떤 변수가 null 참조를 저장할 수 없다는 뜻이다.
  • 모든 타입은 기본적으로 null이 될 수 없는 타입이다. 뒤에 명시적으로 ?가 붙어야 null이 될 수 있다.
  • 널이 될 수 있는 타입 값에 대한 수행 가능 연산은 제한된다.
    • 널이 될 수 있는 타입 값의 메서드를 직접 호출할 수 없다.
    • fun strLenSafe(s: String?) = s.length() // ERROR : only safe (?.) or non-null asserted (!!.) calls are allowed // on a nullable receiver of type kotlin.String?
    • 널이 될 수도 있는 타입 값을 널이 될 수 없는 타입의 변수에 저장할 수 없다
    • fun main(){ val x: String? = null val y: String = x // ERROR: Type mismatch: inferred type is String? but String was expected }
    • 널이 될 수도 있는 타입의 값을 null이 될 수 없는 타입의 파라미터를 받는 함수의 인자로 전달할 수 없다.
    • fun main(){ val x: String? = null strLen(x) // ERROR : Type mismatch: inffered type is String? // but String was expected }
  • 널이 될 수 있는 타입의 값으로는 할 수 있는 중요한 일은 null과 비교하는 것이다.
  • 널이 될 수 있는 타입의 값을 null과 비교하고 나면 컴파일러는 그 사실을 기억하고 null이 아님이 확실한 영역에서는 해당 값을 널이 될 수 없는 타입의 값처럼 사용할 수 있다.
fun strLenSafe(s: String?): Int =
    if(s != null) s.length else 0 // String? 타입의 값을 String 타입의 값처럼 사용

fun main(){
    val x: String? = null
    println(x)
    // 0
    println(strLenSafe("abc")) 
    // 3
}

7.3 타입의 의미 자세히 살펴보기

  • 널 가능성을 위해 사용할 수 있는 도구가 if 검사 뿐이라면 코드가 금방 번잡해질 것이다
  • 널이 될 수도 있는 값을 다룰 때 도움이 되는 코틀린이 제공하는 여러 도구를 알아보기 전에 널 가능성과 변수 타입의 의미를 확인해보자.
  • 타입이란 무엇이고 왜 변수에 타입을 지정해야 하는가
  • 타입은 가능한 값의 집합과 그런 값들에 대해 수행할 수 있는 연산의 집합이라 정의된다.
    • 자바에서 String 타입의 변수에는 String이나 null이라는 2가지 종류의 값이 들어갈 수 있다.
    • 이 두 종류의 값의 서로 완전히 다르다.
    • 자바 자체의 instanceof 연산자도 null이 String이 아니라고 답한다.
    • 두 종류의 값에 대해 실행할 수 있는 연산도 완전히 다르다.
    • 이는 자바의 타입 시스템이 null을 제대로 다루지 못한다는 뜻이다.
  • 코틀린은 null을 제대로 다루지 못하는 자바의 타입 시스템 문제를 위한 해법으로 null이 될 수도 있는 타입을 제공한다.
    • 널이 될 수 있는 타입과 널이 될 수 없는 타입을 구분하면 각 타입의 값에 대해 어떤 연산이 가능할 지 명확히 이해할 수 있고, 실행 시점에 예외를 발생시킬 수 있는 연산을 판단할 수 있다.
    • 그런 연산들을 애초에 컴파일 타임에 아예 금지시킬 수 있다.
    • 실행 시점에는 널이 될 수 있는 타입이나, 널이 될 수 없는 타입이나 똑같은 타입의 객체이다.
    • 널이 될 수 있는 타입은 널이 될 수 없는 타입을 감싼 래퍼 타입이 절대 아니다.

7.4 안전한 호출 연산자로 null 검사와 메서드 호출 합치기: ?.

  • str?.uppercase()는 훨씬 더 복잡한 if(str != null) str.uppercase() else null과 같다.
  • 안전한 호출의 결과 타입도 널이 될 수 있는 타입이라는 사실이 중요하다.
fun printAllCaps(str: String?){
    val allCaps: String? = str?.uppercase()
    println(allCaps)
}

fun main(){
    printAllCaps("abc")
    // ABC
    printAllCaps(null)
    // null
}
  • 메서드 호출뿐 아니라 프로퍼티를 읽거나 쓸 때도 안전한 호출을 사용할 수 있다.
class Employee(val name: String, val manager: Employee?)

fun managerName(employee: Employee): String? = employee.manager?.name

fun main(){
    val ceo = Employee("Da Boss", null)
    val developer = Employee("Bob Smith", ceo)
    println(managerName(developer))
    // Da Boss
    println(ceo)
    // null
}
  • 객체 그래프에서 널이 될 수 있는 중간 객체가 여럿 있다면 한 식 안에서 안전한 호출을 연쇄해서 함께 사용할 수 있다.
class Address(val streetAddress: String, val zipCode: Int,
              val city: String, val country: String)

class Company(val name: String, val address: Address?)

class Person(val name: String, val company: Company?)

fun Person.countryName(): String{
    val country = this.company?.address?.country // String?으로 타입 추론
    return if (country != null) country else "Unknown"
}

fun main(){
    val person = Person("Dmitry", null)
    print(person.countryName())
    // Unknown
}

7.5 엘비스 연산자로 null에 대한 기본값 제공 ?:

  • 코틀린은 null 대신 사용할 기본값을 지정할 때 편리하게 사용할 수 있는 엘비스 연산자(?:)를 제공한다.
fun greet(name: String?){
    val recipient: String = name ?: "unnamed"
    println("Hello, $recipient!") 
}
fun strLenSafe(s: String?): Int = s?.length ?: 0

fun main(){
    println(strLenSafe("abc"))
    // 3
    println(strLenSafe(null))
    // 0
}
fun Person.countryName(): String = company?.address?.country ?: "Unknown
  • 코틀린에서는 return이나 throw 등도 식이기 때문에 엘비스 연산자의 오른쪽에 return, throw 등을 넣을 수 있다.
class Address(val streetAddress: String, val zipCode: Int,
              val city: String, val country: String)

class Company(val name: String, val address: Address?)

class Person(val name: String, val company: Company?)

fun main(){
    val address = Address("Elsestr. 47", 80687, "Munich", "Germany")
    val jetbrains = Company("JetBrains", address)
    val person = Person("Dmitry", jetbrains)

    printShippingLabel(person)

    printShippingLable(Person("Alexey", null))
}

fun printShippingLabel(person: Person){
    val address = person.company?.address 
        ?: throw IllegalArgumentException("No address") // 주소 없으면 예외 발생
    with(address){
        println(streetAddress)
        println("$zipCode $city, $country")
    }
}

7.6 예외를 발생시키지 않고 안전하게 타입 캐스트하기: as?

  • as? 연산자는 어떤 값을 지정한 타입으로 변환한다.
  • as?는 값을 대상 타입으로 변환할 수 없으면 null을 반환한다.
  • 안전한 캐스트 연산자 as?
    • 안전한 캐스트를 사용할 때 일반적인 패턴은 캐스트를 수행한 뒤에 엘비스 연산자를 사용하는 것이다.
    • equals 메서드를 구현할 때 이런 패턴이 유용하다.
    • class Person(val firstName: String, val lastName: String){ override fun equals(o: Any?){ val otherPerson = o as? Person ?: return false // 타입이 일치하면 Person 타입 객체로 타입 캐스트 // 안전한 캐스트-> 스마트 캐스트 // 타입이 일치하지 않으면 false 반환 return otherPerson.firstName == firstName && otherPerson.lastName == lastName } override fun hashCode(): Int { firstName.hashCode() * 37 + lastName.hashCode() } } fun main(){ val p1 = Person("Dmitry", "Jemerov") val p2 = Person("Dmitry", "Jemerov") println(p1==null) // equals 메서드 호출 => 타입이 다르기 때문에 false println(p1==p2) // equals 메서드 호출 => 같은 타입, 같은 내용 -> true println(p1.equals(42)) // 다른 타입이기 때문에 false }

7.7 널 아님 단언: !!

  • 때로는 코틀린의 null 처리 지원을 쓰는 대신, 직접 컴파일러에 게 어떤 값이 실제로는 null이 아니라는 사실을 알려주고 싶은 경우가 있다.
  • 널 아님 단언은 코틀린에서 널이 될 수 있는 타입의 값을 다룰 때 사용할 수 있는 도구 중에서 가장 단순하면서도 무딘 도구다.
  • 느낌표를 이중(!!)으로 사용하면 어떤 값이든 널이 아닌 타입으로 바꿀 수 있다.
  • 실제 null에 대해 !!를 적용하면 NullPointerException 에러가 발생한다.
fun ignoreNulls(str: String?) {
    val strNotNull: String = str!! // 예외는 이 지점을 가리킨다.
    println(strNotNull.length)     
}

fun main() {
    ignoreNulls(null)
    // Exception in thread "main" kotln.KotlinNullPointerException
    // . at <...>.ignoreNulls(07_NotnullAssersions.kt:2)
}
  • 근본적으로 !!는 컴파일러에게 "나는 이 값이 null 아님을 잘 알고 있고 내가 잘못 생각했다면 예외가 발생해도 감수하겠다"라고 말하는 것이다.
  • 호출된 함수가 언제난 다른 함수에서 null 아닌 값을 전달받는 사실이 분명하다면 굳이 null 검사를 다시 수행하고 싶지 않을 것이다. 이럴 때 널 아님 단언문을 사용할 수 있다.
  • 널 아님 단언문을 사용하면 좋은 대표적인 예: UI 프레임워크에 있는 액션 클래스
  • 액션 클래스 안에는 해당 액션의 상태를 변경하는 메서드와 실제 액션을 실행하는 메서드가 있는데, update 메서드 안에서 검사하는 조건을 만족하지 않는 경우 execute 메서드는 호출될 수 없다.
class SelectableTextList {
    val contents: List<String>,
    var selectedIndex: Int? = null,
}

class CopyRowAction(val list: SelectableTextList) {
    fun isActionEnabled(): Boolean =
        list.selectedIndex != null
    fun executeCopyRow() { // isActionEnabled가 true인 경우에만 호출된다.
        val index = list.selectedIndex!! 
        val value = list.contents[index]
        // value를 클립보드에 복사한다.
    }
}
  • !!를 null에 대해 사용해서 발생하는 예외의 스택 트레이스에는 어떤 파일의 몇 번째 줄인지에 대한 정보는 들어있지만 어떤 식에서 예외가 발생했는지에 대한 정보는 들어있지 않다.
  • 여러 !! 단언문을 한 줄에 함께 쓰는 일은 지양하라
person.company!!.address!!.country // 이런 식의 단언문 사용은 하지마라

7.8 let 함수

  • let 함수를 사용하면 널이 될 수 있는 식을 더 쉽게 다룰 수 있다.
  • let을 사용하는 가장 흔한 용례는 널이 될 수 있는 값을 널이 아닌 값만 인자로 받는 함수에 넘기는 경우이다.
fun sendEmilTo(email: String) { /*...*/ } // 널이 아닌 값만 인자로 받는다.

fun main(){
    val email: String? = "foo@bar.com"
    sendEmailTo(email)
    // ERROR: Type mismatch : inferred type is String? but String was expected
}
  • 이런 경우엔 원래 인자를 넘기기 전에 주어진 값이 null이 아닌지 검사 후 인자를 넘겨야 한다.
if (email != null) sendEmailTo(email)
  • 하지만 let 함수를 통해 주어진 인자가 null이 아닌지 따로 검사하는 문자 없이 인자를 전달할 수도 있다.
  • let 함수는 자신의 수신 객체를 자신의 인자인 전달받은 람다에 그 람다의 인자로 넘긴다.
  • 널이 될 수도 있는 값에 대해 안전한 호출 구문(?.)을 사용해 let을 호출하되 널이 아닌 타입을 인자로 받는 받는 람다를 let에 전달 => 널이 될 수 있는 타입의 값을 널이 될 수 없는 타입의 값으로 바꿔 람다에 전달하게 된다.
email?.let{ email -> sendEmailTo(email) }
email?.let{ it -> sendEmailTo(it) }

7.9 직접 초기화하지 않는 널이 아닌 타입: 지연 초기화 프로퍼티 lateinit 변경자

  • 실제로는 널이 아닌 프로퍼티인데 생성자 안에서 널이 아닌 값으로 초기화할 방법이 없는 경우가 있다.
  • 일반적으로 코틀린 클래스 안에 쓰이는 프로퍼티는 프로퍼티 선언과 동시에 초기화 되거나 그 프로퍼티가 속하는 해당 클래스의 생성자 안에서 초기화되도록 해당 클래스에서 구현되어야 한다.
  • 코틀린에서 클래스 안의 널이 아닌 프로퍼티를 그 해당 클래스 생성자 안에서 초기화하지 않고 클래스 안의 다른 특별한 메서드 안에서 초기화할 수는 없다.
  • 프로퍼티 타입이 널이 될 수 없는 타입이라면 반드시 널이 아닌 값으로 그 프로퍼티를 초기화하거나 그런 초기화 값을 제공할 수 없으면 널이 될 수 있는 타입을 사용하여 널로 초기화를 해줘야 한다.
  • 널이 될 수도 있는 타입을 사용하여 프로퍼티를 널로 초기화해주는 경우엔 해당 프로퍼티에 대한 모든 접근에 null 검사를 추가하거나, !! 연산자, ?. 연산자를 사용해야 한다. => 보기 좋지 못한 코드가 나온다
class MyService {
    fun performAction(): String = "Action Done!"
}

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class MyTest {
    private var myService: MyService? = null
    // mySerivce 프로퍼티를 생성자에서 초기화 하지않고 프로퍼티 선언과 동시에 해주기로 했다
    // 초기화 시 널이 아닌 값을 줄 수 없는 상황이기에
    // 널이 될 수도 있는 타입을 사용하여 우선 null로 초기화를 해주었다.
    // 차후 다른 메서드, 다른 영역에서 myService 프로퍼티 접근 시 ?.나 !! 사용 필수 

    @Before 
    fun setUp(){
        myService = MyService() // myService var 선언 => 초기화 후 얼마든지 변경 가능
    }

    @Test 
    fun testAction(){
        assertEquals("Action Done!", myService!!.performAction())
        // MyService?(널이 될 수 있는 타입)이기 때문에 널 가능성 체크를 위한 연산자 필요
    }
}
  • 초기화 시 어쩔 수 없이 널이 될 수도 있는 타입으로 선언하여 null로 초기화한 프로퍼티에 lateinit 변경자를 사용하면, 해당 프로퍼티를 널이 될 수 없는 타입으로 선언하면서도 초기화를 해주지 않아도 된다.
  • lateinit 변경자를 사용한 프로퍼티는 해당 프로퍼티를 선언과 동시에 초기화 해주지 않아도 되고, 생성자에서 초기화하는 코드를 써주지 않아도 되며, 널이 될 수도 있는 타입을 사용하지 않아도 된다는 뜻이다
  • 단, val로 선언된 프로퍼티는 final로 컴파일 되고 생성자에서 반드시 초기화가 돼야 하기 때문에 lateinit 변경자를 사용해 지연 초기화를 적용하려는 프로퍼티는 반드시 var로 선언돼야 한다.
  • 만약 lateinit 변경자로 선언한 프로퍼티가 포함된 클래스의 객체 생성 후 lateinit 변경자를 사용한 프로퍼티가 초기화되지 않았는데 접근하면,
  • 즉 지연 초기화가 완료되지 않았는데 해당 프로퍼티에 접근하면 NullPointerException 에러가 아닌 UninitializedPropertyAccessException 에러가 발생한다.
  • 이 경우 에러 메시지가 UninitializedPropertyAccessException: lateinit property myService has not been initialized이기 때문에 단순 NPE가 발생하는 경우보다 훨씬 오류를 수정하기 용이하다.
class MyService {
    fun performAction(): String = "Action Done!"
}

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class MyTest {
    private lateinit var myService: MyService
    // lateinit을 사용한 var 프로퍼티 myService
    // 프로퍼티 선언에서 초기화 필요하지 않으며, 생성자에서도 초기화 필요하지 않다
    // 널이 될 수 없는 타입으로 선언했지만 lateinit으로 선언했기에 초기화 하지 않아도 된다

    @Before 
    fun setUp(){
        myService = MyService() // 이 때 myService가 초기화된다(지연 초기화)
    }

    @Test 
    fun testAction(){
        assertEquals("Action Done!", myService.performAction())
        // MyService(널이 될 수 없는 타입)이기 때문에 널 가능성 체크 연산자 필요없다.
    }
}

7.10 안전한 호출 연산자(?.)없이 타입 확장: 널이 될 수 있는 타입에 대한 확장

  • 널이 될 수 있는 타입에 대한 확장 함수를 정의하면 null 값을 다루는 강력한 도구로 활용할 수 있다.
  • 어떤 메서드를 호출하기 전에 수신 객체 역할을 하는 변수가 null이 될 수 없다고 보장하는 것 대신,
  • 메서드 호출이 null을 수신 객체로 받는 것을 허용하고 내부에서 null을 처리하게 할 수 있는데, 이런 처리는 확장 함수에서만 가능하다.
  • 일반 멤버 호출은 객체 인스턴스를 통해 디스패치되므로 그 인스턴스가 null인지 여부를 검사하지 않는다.
  • 실제로 String? 타입(널이 될 수 있는 타입)의 수신 객체에 대해 호출할 수 있는 isEmptyOrNull이나 isBlankOrNull 확장 함수가 있다.
fun verifyUserInput(input: String?){
    if(input.isNullOrBlank()){
        println("Please fill in the required fields");
    }
}

fun main(){
    verifyUserInput(" ");
    // Please fill in the required fields
    verifyUserInput(null);
    // Please fill in the required fields
}
  • 위와 같이 안전한 호출 연산자를 사용하지 않고도 널이 될 수 있는 수신 객체 타입에 대해 선언된 확장 함수를 호출 가능하다.
// isNullOrBlank 확장 함수 선언(구현)

fun String?.isNullOrBlank() : Boolean =  // 널이 될 수 있는 String의 확장
    this == null || this.isBlank() // 두 번째 this에는 스마트 캐스트 적용
  • 널이 될 수 있는 타입에 대한 확장을 선언하면 널이 될 수 있는 값에 대해 그 확장 함수를 호출할 수 있다.
  • 그 확장 함수의 내부에서 this는 null이 될 수 있기 때문에 명시적으로 null 여부를 검사해야 한다.
  • 자바에서는 메서드 안의 this는 그 메서드가 호출된 수신 객체를 가리키므로 항상 null이 아니지만 코틀린에서는 널이 될 수 있는 타입의 확장 함수 안에서는 this가 null이 될 수 있다.
  • let 함수도 널이 될 수 있는 타입의 값에 대해 호출은 할 수 있지만 this가 null인지는 검사하지 않는다. 그렇기 때문에 널이 될 수 있는 타입의 값에 대해 let 함수 호출은 가능할지라도 let 함수의 인자로 전달되는 람다가 널이 될 수 없는 타입의 값을 인자로 받게 선언돼있으면 오류가 발생한다.
fun sendEmail(email: String){ // 널이 될 수 없는 String 타입입
    println("Sending email to $email") 
}

fun main(){
    val recipient: String? = null
    recipient.let{ sendEmail(it) } // let 함수 호출은 가능하나, 람다에서 ERROR'
    // ERROR: Type mismatch:
    // inferred type is String? but String was expected 
}
  • let을 사용할 때 수신 객체가 null이 아닌지 검사하고 싶다면
    recepient?.let { sendEmail(it)}처럼 반드시 안전한 호출 연산자 ?.를 사용해야 한다.

7.11 타입 파라미터의 널 가능성

  • 코틀린에서는 함수나 클래스의 모든 타입 파라미터는 기본적으로 널이 될 수 있다.
  • 널이 될 수 있는 타입을 포함하는 어떤 타입이라도 타입 파라미터를 대신할 수 있다.
  • 타입 파라미터 T를 클래스나 함수 안에서 타입 이름으로 사용하면 이름 끝에 물음표가 없더라도 T가 널이 될 수 있는 타입이다.
fun <T> printHashCode(t: T){
    println(t?.hashCode())
}

fun main(){
    printHashCode(null); // T는 Any?로 추론된다.
    // null
}
  • 타입 파라미터가 널이 아님을 확실히 하려면 널이 될 수 없는 타입 상계를 지정해야 한다.
fun <T : Any> printHashCode(t: T){ // T는 널이 될 수 없는 타입이다.
    println(t.hashCode())
}

fun main(){
    printHashCode(null); 
    // ERROR: Type parameter bound for 'T' is not satisfied
    printHashCode(42)
    // 42
}

널 가능성과 자바

  • 자바 타입 시스템은 널 가능성을 지원하지 않는다.
  • 자바 소스 파일과 코틀린 소스 파일을 조합해 프로그램을 만드는 경우 자바 타입이 코틀린에서 어떻게 해석되느냐를 따져봐야 한다.
  • 자바 타입 시스템 자체는 널 가능성을 지원하지 않지만 어노테이션으로 널 가능성 정보를 표시할 수 있다.
  • 자바의 @Nullable String은 코틀린 쪽에서 볼 때 String?와 같고,
  • 자바의 @NotNull String은 코틀린 쪽에서 볼때 String과 같다.
  • 만약 널 가능성 정보 표시를 위한 어노테이션이 사용되지 않은 경우 자바의 타입은 코틀린에서 어떻게 해석될까?
  • 그런 경우엔 자바의 타입은 바로 코틀린의 플랫폼 타입이 된다.

플랫폼 타입

  • 플랫폼 타입은 코틀린이 널 관련 정보를 알 수 없는 타입을 말한다.
  • 그 타입을 널이 될 수 있는 타입으로 처리해도 되고 널이 될 수 없는 타입으로 처리해도 되는데, 이는 곧 자바와 마찬가지로 플랫폼 타입에 대해 수행하는 모든 연산에 대한 책임이 온전히 사용자에게 있다는 의미다.
/*자바*/
public class Person {
    private final String name; 
    // 코틀린 컴파일러는 String을 플랫폼 타입(String!)으로 컴파일

    public Person(String name){
        this.name = name;
    }

    public String getName(){
        return name;
    }
}
// 1. 코틀린 코드에서 null 검사 없이 자바 클래스 접근
fun yellAt(person: Person){ // Person은 널이 될 수 없는 타입
    println(person.name.uppercase() + "!!!")
    // name이 플랫폼 타입이기 때문에 널이 될 수 있고 
    // name이 널이라면 플랫폼 타입이기 때문에 컴파일 타임이 아닌 런타임에 NPE 발생 
}

fun main(){
    yellAt(Person(null))
    // 런타임에 에러 발생
    // java.lang.NullPointerException: person.name must not be null
}
// 2. 코틀린 코드에서 null 검사를 통해 자바 클래스 접근
fun yellAt(person: Person){ // Person은 널이 될 수 없는 타입
    println((person.name ?: "Anyone").uppercase() + "!!!")
    // name이 플랫폼 타입이기 때문에 널이 될 수 있고 
    // 엘비스 연산자로 널 가능성에 대해서 처리를 해준다.
    // name이 런타임에 실제 null인 경우에 런타임 NPE 발생하지 않게 해준다.
}

fun main(){
    yellAt(Person(null))
    // ANYONE!!!
}

상속

  • 코틀린에서 자바 메서드를 오버라이드할 때 그 메서드의 파라미터와 반환 타입을 널이 될 수 있는 타입으로 선언할 지 널이 될 수 없는 타입으로 선언할지 결정해야 한다.
/*자바*/

interface StringProcessor{
    void process(String value);
}
class StringPrinter: StringProcessor {
    override fun process(value: String){
        println(value)
    }
}

class NullableStringPrinter: StringProcessor {
    override fun process(value: String?){
        println(value)
    }
}
  • 구현 메서드를 다른 코틀린 코드가 호출할 수 있으므로 코틀린 컴파일러는 널이 될 수 없는 타입으로 선언한 모든 파라미터에 대해 널이 아님을 검사하는 단언문을 만들어주는데,
  • 코틀린 코드에서와 마찬가지로 자바 코드에서 그 메서드에게 null을 넘기면 단언문이 발동돼 예외가 발생한다. 이는 파라미터를 메서드 안에서 결코 사용하지 않는 경우에도 유효하며 이런 예외를 피할 수 없다.