null이 될 수 있는 타입 다루기 (2)

유우선·2026년 2월 11일

Kotlin Study📚

목록 보기
17/32

1. 지연 초기화 프로퍼티

  • 객체를 일단 생성한 후 나중에 초기화하는 프레임워크들이 많음
    • 안드로이드, JUnit 등등…
  • 코틀린에는 클래스안의 null이 될 수 없는 프로퍼티를 생성자가 아닌 특별한 메서드에서 초기화할 방법이 없음
  • 프로퍼티를 null이 될 수 있는 타입으로 선언하면 모든 프로퍼티 접근에 null 검사를 넣거나 단언문( !! )을 사용해야 함
class MyService() {
    fun performAction(): String = "Action Done!"
}

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class MyTest {
    private var myService: MyService? = null // null 사용을 위해 null이 될 수 있는 타입으로 선언
    
    @BeforeAll fun setUp(){
        myService = MyService() // 메서드안에서 초기화
    }
    
    @Test fun testAction(){
        assertEquals("Action Done!", myService!!.performAction()) // null 가능성을 검증해야함
    }
}

지연 초기화 : lateinit

lateinit 변경자를 통해 프로퍼티를 나중에 초기화할 수 있음

class MyService() {
    fun performAction(): String = "Action Done!"
}

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class MyTest {
    private lateinit var myService: MyService

    @BeforeAll fun setUp(){
        myService = MyService()
    }

    @Test fun testAction(){
        assertEquals("Action Done!", myService.performAction())
    }
}
  • val 프로퍼티로 선언하면 파이널 필드로 컴파일 되어 생성자 안에서 변경해줘야 함
    • 지연 초기화 프로퍼티는 항상 var로 선언 되어야 함
  • 지연 초기화 프로퍼티는 null이 될 수 없는 타입이라고 해도 생성자 안에서 초기화 할 필요가 없음
  • 초기화 전에 지연 초기화 프로퍼티에 접근하면 다음과 같은 오류가 발생함
    kotiln.UninitializedPropertyAccessException: 
    		latainit property myService has not been initialized
    • 단순히 NPE가 발생하는 것보다 문제를 파악하기 더 좋음

자바 의존관계 주입 프레임워크와의 사용

  • 자바 프레임워크와의 호환성을 위해 lateinit이 지정된 프로퍼티와 가시성이 똑같은 필드를 생성
    • 지연 초기화 프로퍼티가 public이라면 코틀린이 생성한 필드도 public

📌 지역 변수와 최상위 프로퍼티도 지연 초기화 가눙


2. null이 될 수 있는 타입에 대한 확장

null이 될 수 있는 타입에 대한 확장 함수를 정의하면 null 값을 다루는 강력한 도구가 될 수 있음

  • 메서드 호출이 null을 수신 객체로 받고 내부에서 null을 처리할게 할 수 있음
  • String? 클래스의 isEmptyOrNull 이나 isBlankOrNull이 null이 될 수 있는 타입에 대한 확장임
    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 → 입력값이 null인 경우 true를 반환하고 null이 아닐 경우 isBlank를 호출함
      fun String?.isNullOrBlank(): Boolean =
      		this == null || this.isBlank()

null이 될 수 있는 타입에 대한 확장을 정의하면 null이 될 수 있는 값에 대해 그 확장 함수를 호출할 수 있음

  • 코틀린에서는 null이 될 수 있는 타입의 확장 함수 안에서 this가 null이 될 수 있음

let 함수도 null이 될 수 있는 타입의 값에 호출할 수 있지만 this가 null인지 검사하지 않음

  • 안전한 호출을 사용하지 않고 let을 호출하면 람다의 인자는 null이 될 수 있는 값으로 추론됨
    fun sendMailTo(email: String) {
        println("Sending email to $email")
    }
    
    fun main() {
        val recipient: String? = null
        recipient.let { sendMailTo(it) } // 안전한 호출을 사용하지 않으면 it은 null이 될 수 있는 타입으로 추론됨
    }
    
    //type mismatch: actual type is 'String?', but 'String' was expected.
    • 수신 객체가 null인지 검사하고 싶다면 안전한 호출을 사용해야 함

3. 타입 파라미터의 null 가능성

코틀린에서 함수나 프로퍼티 → 기본적으로 null이 될 수 있음

  • null이 될 수 있는 타입을 포함한 모든 타입이 타입 파라미터를 대신할 수 있음
  • 타입 파라미터를 타입 이름으로 사용하면 물음표가 없더라도 null이 될 수 있는 타입임
    fun <T> printHashCode(t: T) {
        println(t?.hashCode())
    }
    
    fun main() {
        printHashCode(null)
        // null
    }
    • 타입 파라미터 T에 대해 추론한 타입은 null이 될 수 있는 Any? 타입임
    • 타입 파라미터에 물음표가 붙어있지 않지만 null을 받을 수 있음

타입 파라미터가 null이 아님을 확실히 하려면 타입 상계(upper bound)를 지정해줘야 함

fun <T: Any> printHashCode(t: T) {
    println(t.hashCode())
}

fun main() {
    printHashCode(null)
    printHashCode(42)
}
  • null이 될 수 없는 타입 상계를 지정하면 null이 될 수 있는 값을 거부함
  • 위 코드는 컴파일 되지 않음
    • null이 될 수 없는 타입의 파라미터에 null을 넘겼기 때문

0개의 댓글