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

유우선·2026년 2월 11일

Kotlin Study📚

목록 보기
16/32

1. null 검사와 호출 합치기

안전한 호출 연산자 ( ?. )

  • null 검사와 메서드 호출을 한 연산으로 수행
  • str?.uppercase() == if (str != null) str.uppercase() else null
  • 호출하려는 값이 null이 아니라면 일반 메서드 호출처럼 작동
  • 호출하려는 값이 null이라면 null 반환
  • 안전한 호출의 결과 → 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(managerName(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
		return if (country != null) country else "Unknown"
		// country에 대한 null 검사 반복
		// 엘비스 연산자를 통해 개선 가능
}

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

2. null에 대한 기본값 제공 (엘비스 연산자)

  • 코틀린 → 엘비스 연산자( ?: )를 통해 null에 대한 기본값을 간단하게 지정할 수 있음

사용 방법

fun greet(name: String?) {
		val recipient: String = name ?: "unnamed"
		println("Hello, ${recipient}!")
}
  • 2개의 값을 받음
    1. null이 아닐 경우의 결과값
    2. null일 경우의 결과값

안전한 호출 연산자와 엘비스 연산자 동시사용

fun strLenSafe(s: String?): Int = s?.Length ?: 0

fun main() {
		println(strLenSafe("abc")
		// 3
		println(strLenSafe(null))
		// 0
}
  • 객체가 null인 경우에 대비해 값을 지정할 수 있음
  • 안전한 호출 연산자가 null을 반환할 때 기본값을 지정할 수 있음

엘비스 연산자 오른쪽에 throw, return등 사용가능

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 printShippingLabel(person: Person) {
    val address = person.company?.address ?: throw IllegalArgumentException("No address")
    with (address) {
        println(streetAddress)
        println("$zipCode $city, $country")
    }
}

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

    printShippingLabel(person)
    // Elsestr. 47
    // 80687 Munich, Germany
    printShippingLabel(Person("Alexey", null))
    // java.lang.IllegalArgumentException: No address
}
  • printShippingLabel 함수 → 모든 정보가 있으면 주소를 출력, 주소가 없으면 throw문 실행

3. 안전한 타입 캐스트 ( as )

  • 어떤 값을 지정한 타입으로 변환
  • 대상 타입으로 변환할 수 없으면 null 반환
class Person(val firstName: String, val lastName: String){
    override fun equals(o: Any?): Boolean {
        val otherPerson = o as? Person ?: return 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)
    // false
    println(p1 == p2)
    // true
    println(p1.equals(42))
    // false
}
  • 엘비스 연산자와 함께 사용하여 쉽게 타입을 검사하고 캐스트 할 수 있음
    • 타입이 안맞으면 false 반환

4. null 아님 단언 (!!)

  • 느낌표를 이중으로 사용 ( !! )
  • null이 아닌 null이 될 수 있는 값을 null이 될 수 없는 값으로 강제 변환

null이 될 수 있는 인자를 null이 될 수 없는 타입으로 변환하기

fun ignoreNulls(str: String?) {
    val strNotNull: String = str!!
    println(strNotNull.length)
}

fun main() {
    ignoreNulls(null)
    // Exception in thread "main" java.lang.NullPointerException
	  // at MainKt.ignoreNulls(Main.kt:2)
	  // at MainKt.main(Main.kt:8)
	  // at MainKt.main(Main.kt)

}
  • null에 사용할 시 NPE가 발생함
  • 단언문을 선언한 곳에서 오류가 발생함
    • ignoreNulls 함수는 null이 될 수 있는 값을 인자로 받기 때문에 오류가 발생하지 않음
  • 코틀린 설계자들은 컴파일러가 검증할 수 없는 단언 대신 다른 좋은 방법을 사용하도록 설계함

단언문이 좋은 해결책인 경우

  • 어떤 함수에서 null에 대한 검증을 했더라고 다른 함수에선 이를 인식할 수 없음
  • 다른 함수에서 이미 검증했음에도 다른 함수에서도 검증하는 건 비효율적임
  • null이 아닌 값을 전달받는 것이 확실한 상황에서는 단언문이 좋은 선택지가 될 수 있음

액션 클레스에서 단언문 사용하기

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를 클립보드에 복사
    }
}
  • 단언문을 사용하지 않으려면 val value = list.selectedValue ?: return 등을 사용해 null이 아닌 값을 획득해야 함
  • 단언문을 사용해서 발생한 예외의 스택 트레이스는 파일의 몇 번째 줄에서 오류가 발생했는지는 알려주지만 어떤 식에서 발생했는지는 안알려주기 때문에 단언문을 한줄에 여러개 사용하는 것은 지양해야 함
    • person.company!!.address!!.country 같은 형식 지양

5. let 함수

null이 아닌 값만 인자로 받는 함수에 null이 될 수 있는 값을 넘기려면?

  • 코틀린 컴파일러는 이를 허용하지 않음
  • 하지만 표준 라이브러리에 이를 도와주는 함수 let이 있음
  • 안전한 호출과 함께 사용하면 null 검사와 그 결과를 다른 함수에 넘기는 작업을 간단하게 처리할 수 있음

null이 될 수 있는 값을 null이 아닌 값을 받는 함수에 넘기기

null 검사를 통해 인자를 넘기기

fun sendMailTo(email: String) {
    println("Sending email to $email")
}

fun main() {
    val email: String? = "foo@bar.com"
    sendMailTo(email)
    // Argument type mismatch: actual type is 'String?', but 'String' was expected.
}
  • 이 경우 null 검사를 하지 않았기 때문에 에러가 발생함
  • if (email != unll) sendEmailTo(email) 를 사용하면 오류가 발생하지 않음

let 함수로 null 검사 생략

fun sendMailTo(email: String) {
    println("Sending email to $email")
}

fun main() {
    var email: String? = "foo@bar.com"
    email?.let { sendMailTo(it) }
    // Sending email to foo@bar.com

    email = null
    email?.let { sendMailTo(it) }
    //
}
  • let은 자신의 수신 객체를 인자로 전달 받은 람다에 넘김
  • 수신 객체가 null이 아닌 경우에만 람다를 호출함
  • 여러 값이 null인지 검사해야 하는 경우 let을 사용하기 보단 if문으로 null 검사를 수행하는 것이 가독성이 더 좋음

0개의 댓글