널 가능성
코틀린을 비롯한 최신 언어에서 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. 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"
'프로그래밍 노트 > Kotlin' 카테고리의 다른 글
[Kotlin] 코틀린 데이터 타입 (0) | 2023.03.27 |
---|---|
[kotlin] 코틀린 타입 시스템 (0) | 2023.03.25 |
[Kotlin] 수신 객체 지정 람다: with와 apply (0) | 2020.12.14 |
[Kotlin] 코틀린 지연 계산(lazy) 컬렉션 연산 (0) | 2020.12.13 |
[Kotlin] 코틀린 컬렉션 함수 API(filter, map ...) (0) | 2020.12.06 |