BottleH Blog

Kotlin in Action - 7장 널이 될 수 있는 값

    Tags

  • Kotlin
Kotlin in Action - 7장 널이 될 수 있는 값 thumbnail

📖 7.1 NullPointerException을 피하고 값이 없는 경우 처리: 널 가능성

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

📖 7.2 널이 될 수 있는 타입으로 널이 될 수 있는 변수 명시

  • 코틀린과 자바의 첫 번째이자 가장 중요한 차이는 코틀린 타입 시스템이 널이 될 수 있는 타입을 명시적으로 지원한다는 점이다.
fun strLen(s: String) = s.length
  • null이 인자로 들어올 수 없다면 위와 같이 정의 가능
fun main() { strLen(null) // error }
  • 컴파일 시, 오류가 발생한다.
fun strLenSafe(s: String?) = s.length() // error
  • 타입 이름 뒤에 물음표를 붙이면 그 타입의 변수나 프로퍼티에 null 참조를 저장할 수 있다.
  • 널이 될 수 있는 타입 값의 메서드를 직접 호출할 수는 없다.
fun main() { val x: String? = null var y: String = x // error }
  • 널이 될 수 있는 값을 널이 될 수 없는 타입의 변수에 대입할 수 없다.
fun main() { val x: String? = null strLen(x) // error }
  • 널이 될 수 있는 타입의 값을 null이 아닌 타입의 파라미터를 받는 함수에 전달할 수 없다.
fun strLenSafe(s: String?): Int = if (s != null) s.length else 0
  • null과 비교하고 나면 컴파일러는 그 사실을 기억하고 null이 아님이 확실한 영역에서는 해당 값을 null이 아닌 타입의 값처럼 사용할 수 있다.

📖 7.3 타입의 의미 자세히 살펴보기

  • 타입
    • 가능한 값의 집합과 그런 값들에 대해 수행할 수 있는 연산의 집합
  • 자바의 타입 시스템은 null을 제대로 다루지 못한다.
    • ex. String 타입의 변수에는 null, String 모두 들어갈 수 있지만 완전히 다르다. 심지어, instanceof 연산자도 null이 String이 아니라고 답한다.
  • 코틀린의 널이 될 수 있는 타입은 이런 문제에 대해 종합적인 해법을 제공한다.

📖 7.4 안전한 호출 연산자로 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)) println(managerName(ceo)) }
  • Employee로 프로퍼티 접근 시 안전한 호출을 사용하는 방법
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" } fun main() { val person = Person("Dmitry", null) println(person.countryName()) // Unknown }
  • null 검사를 한줄로 연쇄적으로 할 수 있다!

📖 7.5 엘비스 연산자로 null에 대한 기본값 제공: ?:

  • 엘비스 연산자
    • null 대신 사용할 기본값을 지정할 때 편리하게 사용할 수 있는 연산자
    • null 복합 연산자라고도 부름
fun greet(name: String?) { val recipient: String = name ?: "unnamed" println("Hello, $recipient") } fun strLenSafe(s: String?): Int = s?.length ?: 0
  • 한줄로 줄여쓸 수 있다.
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") } }
  • 코드가 매우 간결해진다!

📖 7.6 예외를 발생시키지 않고 안전하게 타입을 캐스트하기: as?

  • 코틀린에서 대상 값을 as로 지정한 타입으로 바꿀 수 없으면 ClassCastException이 발생
  • as를 사용할 때마다 is로 변환 가능한 타입인지 검사할 수 있지만 너무 귀찮다.
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() }
  • as? 연산자는 어떤 값을 지정한 타입으로 변환한다. 변활할 수 없으면 null을 반환한다.
  • 하나의 식으로 해결 가능해진다.

📖 7.7 널 아님 단언: !!

  • 널 아님 단언은 코틀린에서 널이 될 수 있는 타입의 값을 다룰 때 사용할 수 있는 도구 중에서 가장 단순하면서도 무딘 도구다.
  • !!을 사용하면 어떤 값이든 널이 아닌 타입으로 바꿀 수 있다.
  • null에 대해 !!를 사용하면 NPE 발생
fun ignoreNulls(str: String?) { val strNotNull: String = str!! // 예외는 이 지점을 가리킨다. println(strNotNull.length) }
  • 예외가 가리키는 지점은 단언문이 위치한 곳을 가리킨다.
  • 결국 !!는 컴파일러에게 null이 아님을 알고 있으며, 예외가 발생해도 감수하겠다는 것을 말하는 것이다.
  • 굳이 느낌표 두개를 선택한 것은 더 나은 방법을 찾아보라는 코틀린 설계자들의 의도이다.
    • 기호가 못생기고 무례함
class SelectableTextList( val contents: List<String>, var selectedIndex: Int? = null ) class CopyRowAction(val list: SelectableTextList) { fun isActionEnabled(): Boolean = list.selectedIndex != null fun executeCopyRow() { val index = list.selectedIndex!! val value = list.contents[index] } }
  • !!를 사용하여 발생하는 에러 스택 트레이스에는 몇번째 줄인지에 대한 정보가 들어있지 않다.
    • 여러 !! 단언문을 한 줄에 함께 쓰는 일을 피하는 것이 좋다.

📖 7.8 let 함수

fun sendEmailTo(email: String) { println("Sending email to $email") } fun main() { var email: String? = "yole@gmail.com" email.let { sendEmailTo(it) } email = null email?.let { sendEmailTo(it) } }
  • let을 사용하는 가장 흔한 용례는 널이 될 수 있는 값을 널이 아닌 값만 인자로 받는 함수에 넘기는 경우이다.
  • let 함수는 자신의 수신 객체를 인자로 전달받은 람다에 넘긴다.
  • 아주 긴 식이 있고 그 값이 null이 아닐 때 수행해야 하는 로직이 있을 때 let을 쓰면 훨씬 더 편하다.

📖 7.9 직접 초기화하지 않는 널이 아닌 타입: 지연 초기화 프로퍼티

class MyService { fun performAction(): String = "Action Done!" } class MyTest { private var myService: MyService? = null @BeforeAll fun setUp() { myService = MyService() } @Test fun testAction() { assertEquals("Action Done!", myService!!.performAction()) } }
  • 이 코드는 보기 나쁘다.
  • 코틀린에서는 클래스 안의 널이 아닌 프로퍼티를 생성자 안에서 초기화하지 않고 특별한 메서드 안에서 초기화할 수는 없다. 일반적으로 생성자에서 모든 프로퍼티를 초기화해야 한다.
  • 프로퍼티 타입이 널이 될 수 없는 타입이라면 반드시 널이 아닌 값으로 초기화해야 한다.
class MyTest { private lateinit var myService: MyService @BeforeAll fun setUp() { myService = MyService() } @Test fun testAction() { assertEquals("Action Done!", myService.performAction()) } }
  • lateinit 변경자를 붙이면 프로퍼티를 나중에 초기화할 수 있다.
  • 지연 초기화 프로퍼티는 항상 var여야한다.

📖 7.10 안전한 호출 연산자 없이 타입 확장: 널이 될 수 있는 타입에 대한 확장

fun verifyUserInput(input: String?) { if (input.isNullOrBlank()) { println("Please fill in the required fields") } } fun main() { verifyUserInput(" ") verifyUserInput(null) // 예외 발생하지 않음. }
  • 메서드 호출이 null을 수신 객체로 받고 내부에서 null을 처리하게 할 수 있다.
    • 확장 함수에서만 가능
    • 일반 멤버 호출은 객체 인스턴스를 통해 디스패치되므로 그 인스턴스가 null인지 여부를 검사하지 않는다.
  • 널이 될 수 있는 타입의 확장 함수는 자신의 수신 객체가 null일 때 어떻게 해야 하는지 스스로 안다.
    • 안전한 호출 없이도 호출 가능
fun sendEmailTo(email: String) { println("Sending email to $email") } fun main() { val recipient: String? = null recipient.let { sendEmailTo(it) } // 안전한 호출을 사용하지 않아 널이 될 수 있는 타입으로 취금 }
  • let은 this가 null인지 검사하지 않는다.
  • let을 사용할 때 수신 객체가 null이 아닌지 검사하고 싶다면 안전한 호출 연산인 ?.를 사용해야 한다.

📖 7.11 타입 파라미터의 널 가능성

  • 타입 파라미터 T를 클래스나 함수 안에서 타입 이름으로 사용하면 이름 끝에 물음표가 없더라도 T가 널이 될 수 있는 타입이다.
fun <T> printHashCode(t: T) { println(t?.hashCode()) // 안전한 호출 사용 } fun main() { printHashCode(null) // null }
  • printHashCode 호출에서 타입 파라미터 T에 대해 추론한 타입은 널이 될 수 있는 Any? 타입이다.
  • 타입 파라미터가 널이 아님을 확실히 하려면 널이 될 수 없는 타입 상계를 지정해야 한다.
fun <T: Any> printHashCode(t: T) { println(t.hashCode()) // 안전한 호출 사용 } fun main() { printHashCode(null) // error }
  • 타입 파라미터는 널이 될 수 있는 타입을 표시하려면 반드시 물음표를 타입 이름 뒤에 붙여야 한다는 규칙의 유일한 예외이다.

📖 7.12 널 가능성과 자바

🔖 7.12.1 플랫폼 타입

  • 플랫폼 타입은 코틀린이 널 관련 정보를 알 수 없는 타입
    • 널이 될 수 있는 타입 or 널이 될 수 없는 타입으로 처리해도 된다.
    • 컴파일러는 모든 연산을 허용
  • 자바 API를 다룰 때는 조심해야 한다.
    • 대부분의 라이브러리는 널 관련 어노테이션을 쓰지 않는다.
  • 코틀린에서 플랫폼 타입을 선언할 수는 없다.
  • 자바에서 가져온 널 값을 널이 될 수 없는 코틀린 변수에 대입하면 실행 시점에 대입이 이뤄질 때 예외가 발생

🔖 7.12.2 상속

  • 코틀린에서 자바 메서드를 오버라이드할 때 그 메서드의 파라미터와 반환 타입을 널이 될 수 있는 타입으로 선언할지 널이 될 수 없는 타입으로 선언할지 결정해야 한다.
  • 자바 클래스나 인터페이스를 코틀린에서 구현할 경우 널 가능성을 제대로 처리하는 것이 중요
    • 코틀린 컴파일러는 널이 될 수 없는 타입으로 선언한 모든 파라미터에 대해 널이 아님을 검사하는 단언문을 만들어준다.
Written by@BottleH
Back-End Developer

GitHub