프로그래밍 노트/Kotlin

[Kotlin] 널이 될 수 있는 타입, 안전한 호출 연산자(?.)

깡냉쓰 2020. 12. 15. 00:19
728x90
반응형

널 가능성

코틀린을 비롯한 최신 언어에서 null에 대한 접근 방법은 가능한 한 이 문제를 실행 시점에서 컴파일 시점으로 옮기는 것이다. 널이 될 수 있는지 여부를 타입 시스템에 추가함으로써 컴파일러가 여러 가지 오류를 커파일 시 미리 감지해서 실행 시점에 발생할 수 있는 예외의 가능성을 줄인다.

널이 될 수 있는 타입

코틀린과 자바의 차이점 중 하나는 코틀린 타입 시스템이 널이 될 수 있는 타입을 명시적으로 지원한다는 점이다. 코틀린은 null이 될 수 있는 변수를 메소드 파라미터로 받을 수 있으면 호출을 금지함으로써 많은 오류를 방지한다.

int strLen(String s){
    return s.length;
}

이 함수의 파라미터로 null이 들어오면 NullPointerException이 발생한다. 검사가 필요할지 여부는 이 함수를 사용하는 의도에 따라 달라진다.
널이 인자로 들어올 수 없다면 코틀린에서는 다음과 같이 함수를 구현한다.

fun strLen(s: String) = s.length

strLen에 null이거나 null이 될 수 있는 인자를 넘기는 것은 금지되며, 그런 값을 넘기면 컴파일 시 오류가 발생한다.

>>> strLen(null)

이 함수과 널과 문자열을 인자로 받을 수 있게 하려면 타입 이름 뒤에 물음표(?)를 명시해야 한다.

fun strLen(s: String?) = s.length

어떤 타입이든 타입 이름 뒤에 물음표를 붙이면 그 타입의 변수나 프로퍼티에 null 참조를 저장할 수 있다는 뜻이다. (모든 타입은 기본적으로 널이 될 수 없는 타입이다.)

널이 될 수 있는 타입의 변수가 있다면 그에 대해 수행할 수 있는 연산이 제한된다.

  1. 널이 될 수 있는 타입인 변수에 대해 변수.메소드() 처럼 메소드를 직접 호출할 수는 없다.
  2. 널이 될 수 없는 타입의 변수에 대입할 수 없다.
  3. 널이 될 수 있는 타입의 값을 널이 될 수 없는 파라미터를 받는 함수에 전달할 수 없다.
// 1. null 처리를 하지 않아 컴파일 오류남
>>> fun strLen(s: String?) = s.length
// 2. 널이 될 수 없는 타입에 대입 불가
>>> val x: String? = null
>>> val y: String = x 
// 3. 널이 될 수 있는 파라미터 전달 불가
>>> strLen(x)

null이 될 수 있는 파라미터를 받는다면, null 체크를 해줘야 컴파일러에서 오류가 나지 않는다.

fun strLenSafe(s: String?): Int =
    if(s != null) s.length else 0

안전한 호출 연산자: ?.

?.은 null 검사와 메소드 호출을 한 번의 연산으로 수행한다.
s?.toUpperCase() 는 훨씬 더 복잡한 if( s != null ) s.toUpperCase() else null 과 같다. 호출하려는 값이 null이 아니라면 ?.은 일반 메소드 호출처럼 작동한다. 호출하려는 값이 null이면 이 호출은 무시되고 null이 결과 값이 된다.
안전한 호출의 결과 타입도 널이 될 수 있는 타입이다. String.toUpperCase는 String 타입의 값을 반환하지만 s가 널이 될 수 있는 타입의 경우 s?.toUpperCase() 식의 결과 타입은 String?이다.

fun printAllCaps(s: String?){
    val allCaps: String? = s?.toUpperCase()
    println(allCaps)
}

>>> printAllCaps("abc")
ABC
>>> printAllCaps(null)
null

메소드 호출뿐 아니라 프로퍼티를 읽거나 쓸 때도 안전한 호출을 사용할 수 있다.

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

>>> 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"
}

>>> val person = Person("Corn", null)
>>> println(person.countryName())
Unknown

if (country != null) country else "Unknown" 이 코드도 더 간결하게 쓸 수 있다.
=> 다음 포스팅에서 더 살펴보자

val country = this.company?.address?.country
if (country != null) country else "Unknown"

val country = this.company?.addess?.country?: "Unknown"
728x90
반응형