728x90
반응형
✅ 널 가능성
- 널 가능성(nullability)은 NullPointer Exception(NPE)를 피할 수 있게 돕기 위한 코틀린 타입 시스템의 특성이다.
- 코틀린을 비롯한 최신 언어에서 null에 대한 접근 방법은 가능한 한 이 문제를 실행 시점에서 컴파일 시점으로 옮기는 것이다.
- 널이 될 수 있는지 여부를 타입 시스템에 추가함으로써 컴파일러가 여러 가지 오류를 컴파일 시 미리 감지해 실행 시점에 발생할 수 있는 예외의 가능성을 줄일 수 있다.
📌 널이 될 수 있는 타입
- 널이 될 수 있는 타입은 프로그램 안의 프로퍼티나 변수에 null을 허용하게 만드는 방법이다.
- 어떤 변수가 널이 될 수 있다면 그 변수에 대해 메서드를 호출하면 NPE가 발생할 수 있기 때문에 안전하지 않다.
- 코틀린은 그런 메서드 호출을 금지함으로써 많은 오류를 방지한다.
/* 자바 */
int strLen(String s) {
return s.length();
}
- 위 함수가 안전하다고 했을 때 우리는 안전하지 못하다는 결론을 내릴 것이다.
- 왜냐하면 이 함수에 null을 넘길 경우 NPE가 발생하기 때문이다.
- 그렇다면 코틀린에서 위의 함수를 구현한 후 Null을 넣어보자.
fun strLen(s: String) = s.length
- 코틀린에서는 아래와 같은 오류를 발생하는 것을 볼 수 있다.
- 그렇다면 위의 함수가 널과 문자열을 인자로 받을 수 있게 하려면 어떻게 해야할까?
- 그 방법은 바로 타입 이름 뒤에 물음표(?)를 명시해주면 된다.
- 어떤 타입이든 타입 이름 뒤에 물음표를 붙이면 그 타입의 변수나 프로퍼티에 null 참조를 저장할 수 있게 된다.
fun strLenSafe(s: String?) = ...
📌 타입의 의미
- 타입이란 무엇이고 왜 변수에 타입을 지정해야하는 걸까?
- 타입은 분류로 어떤 값들이 가능한지와 그 타입에 대해 수행할 수 있는 연산의 종류를 결정한다.
- String 타입의 변수와 null을 비교해보자.
- 자바에서 String 타입의 변수에는 String이나 null이라는 두 가지 종류의 값이 들어갈 수 있다.
- 이 두 종류의 값은 서로 완전히 다르며 자바 자체의 instanceof 연산자도 null이 String이 아니라고 답한다.
- 그에 따른 실행할 수 있는 연산도 완전히 다르다.
- String이 들어있는 변수에 대해 String 클래스에 정의된 모든 메서드를 호출할 수 있지만 null일 경우에는 사용할 수 있는 메서드가 많지 않다.
- 이는 자바의 타입 시스템이 널을 제대로 다루지 못한다는 뜻이다.
- 변수에 선언된 타입이 있어 널 여부를 추가로 검사하기 전에는 그 변수에 대해 어떤 연산을 수행할 수 있을지 알 수 없다.
- 물론 자바에도 NullPointerException 문제를 해결하는 데 도움을 주는 도구가 있다.
- @Nullable이나 @NotNull 등의 애너테이션이다.
- 하지만 코드가 더 지저분해지고 래퍼가 추가됨에 따라 실행 시점에 성능이 저하되며 전체 시스템에서 일관성있게 활용하기는 어렵다 단점이 있다.
- 하지만 코틀린의 널이 될 수 있는 타입은 이러한 문제에 대한 해법을 제공한다.
- 널이 될 수 있는 타입과 널이 될 수 없는 타입을 구분하면 각 타입의 값에 대해 어떤 연산이 가능할지 명확히 이해할 수 있다.
- 뿐만 아니라 실행 시점에 예외를 발생시킬 수 있는 연산을 판단할 수도 있어, 문제가 되는 연산을 아예 금지시킬 수 있다.
📌 안전한 호출 연산자: ?.
- 코틀린이 제공하는 가장 유용한 도구 중 하나가 안전한 호출 연산자인 ?. 이다.
- ?. 은 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
- ?. 연산자를 사용하면 다른 추가 검사 없이 Employee의 manager 프로퍼티 값을 간편하게 초기화할 수 있다.
- 그렇다면 초기화하는 값이 null일 경우 기본 값을 지정하는 방법은 없을까?
- 이에 대한 방법으로 엘비스 연산자인 ?: 를 활용하면 된다.
📌 엘비스 연산자: ?:
- 코틀린은 null 대신 사용할 디폴트 값을 지정할 때 편리하게 사용할 수 있는 연산자를 제공한다.
- 그 연산자는 엘비스 연산자( ?: ) 라고 한다.
- 이 연산자는 이항 연산자로 좌항을 계산한 값이 널인지 검사한다. 좌항 값이 널이 아니면 좌항 값을 결과로, 좌항 값이 널이면 우항 값을 결과로 한다.
- 예시 코드는 아래와 같다.
fun foo(s: String?) {
val t: String = s ?: ""
}
- 엘비스 연산자를 더 잘 활용하는 방법이 있다.
- 코틀린에서는 return이나 throw 등의 연산도 모두 식이기 때문에 엘비스 연산자의 우항에 return, throw 등의 연산을 넣으면 엘비스 연산자를 더욱 더 유용하게 활용할 수 있다.
/* 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 printShippingLabel(person: Person) {
val address = person.company?.address
?: throw IllegalArgumentException("No address") // 주소가 없으면 예외를 발생시킨다.
with (address) { // "address"는 널이 아니다.
println(streetAddress)
println("$zipCode $city, $country")
}
}
fun main(args: Array<String>) {
val address = Address("Elsestr. 47", 80687, "Munich", "Germany")
val jetbrains = Company("JetBrains", address)
val person = Person("Dmitry", jetbrains)
printShippingLabel(person)
printShippingLabel(Person("Alexey", null))
}
📌 안전한 캐스트 : as?
- as? 연산자는 어떤 값을 지정한 타입으로 캐스트한다.
- 값을 대상 타입으로 변환할 수 있으면 해당 타입을 반환하고, 변환할 수 없으면 null을 반환한다.
/* 안전한 연산자를 사용해 equals 구현하기 */
class Person(val firstName: String, val lastName: String) {
override fun equals(o: Any?): Boolean {
val otherPerson = o as? Person ?: return false // 타입이 서로 일치하지 않으면 false를 반환
return otherPerson.firstName == firstName && // 안전한 캐스트를 하고 나면 otehrPerson이 Person 타입으로 스마트 캐스트됨
otherPerson.lastName == lastName
}
override fun hashCode(): Int =
firstName.hashCode() * 37 + lastName.hashCode()
}
val p1 = Person("Dmitry", "Jemerov")
val p2 = Person("Dmitry", "Jemerov")
println(p1 == p2) // == 연산자는 "equals" 메서드를 호출
// true
println(p1.equals(42))
// false
- 이러한 패턴을 사용하면 파라미터로 받은 값이 원하는 타입인지 쉽게 검사하고 캐스트할 수 있고, 타입이 맞지 않으면 쉽게 false를 반환할 수 있다.
📌 널 아님 단언 : !!
- 널 아님 단언은 코틀린에서 널이 될 수 있는 타입의 값을 다룰 때 사용할 수 있는 도구 중에서 가장 단순한 도구이다.
- 느낌표를 이중(!!)으로 사용하면 어떤 값이든 널이 될 수 없는 타입으로 바꿀 수 있다.
/* 널 아님 단언 사용하기 */
fun ignoreNulls(s: String?) {
val sNotNull: String = s!! // 예외는 이 지점을 가리킨다.
println(sNotNull.length)
}
ignoreNulls(null)
// Exception in thread "main" kotlin.KotlinNullPointerException
// at <...>.ignoreNulls(...)
- 여기서 한 가지 유의해야 할 점은 !!을 통해 컴파일러에게 null 값이 아니라고 했지만 null 값이 들어갈 경우
- 그로 인해 발생하는 예외는 null 값을 사용하는 코드가 아니라 단언문이 위치한 곳을 가리킨다.
- 근본적으로 !! 는 컴파일러에게 "나는 이 값이 null이 아님을 잘 알고 있다. 내가 잘못 생각했다면 예외가 발생해도 감수하겠다" 라고 말하는 것이다.
- 또한 !! 에는 한 가지 함정이 더 있다.
- !! 를 널에 대해 사용해서 발생하는 예외의 스택 트레이스에는 어떤 파일의 몇 번째 줄인지에 대한 정보는 들어있지만 어떤 식에서 예외가 발생했는지에 대한 정보는 들어있지 않다.
- 그로 인해 어떤 값이 널이었는지 확실히 하기 위해 아래와 같이 여러 !! 단언문을 한 줄에 함께 사용하는 일은 피해야 한다.
person.company!!.address!!.country // 이런 식의 코드는 작성 X
728x90
반응형
'Studying > Kotlin' 카테고리의 다른 글
[Kotlin In Action] 6장. 코틀린 타입 시스템(3) - 코틀린의 원시 타입 (0) | 2023.05.31 |
---|---|
[Kotlin In Action] 6장. 코틀린 타입 시스템(2) - 널 가능성[2] (0) | 2023.05.30 |
[Kotlin In Action] 5장. 람다로 프로그래밍(5) - 수신 객체 지정 람다 : with와 apply (0) | 2023.05.23 |
[Kotlin In Action] 5장. 람다로 프로그래밍(4) - 자바 함수형 인터페이스 활용 (0) | 2023.05.23 |
[Kotlin In Action] 5장. 람다로 프로그래밍(3) - 지연 계산(lazy) 컬렉션 연산 (0) | 2023.05.22 |