관리 메뉴

나만을 위한 블로그

[Kotlin] null 처리 방법 정리 본문

개인 공부/Kotlin

[Kotlin] null 처리 방법 정리

참깨빵위에참깨빵_ 2022. 2. 20. 16:23
728x90
반응형

자바와 코틀린을 비교했을 때 장점 하나를 꼽아보라면 빠지지 않고 나오는 것이 null 처리에 관한 내용이다.

자바에선 주로 if와 &&, || 등의 연산자들을 통해 null이 아닌 경우에 처리할 로직을 작성했다.

이 방식은 규모가 작다면 별 문제되지 않는 처리 방식이지만 depth가 깊은 로직일 경우 null 처리로만 몇 줄을 잡아먹는 경우도 있어서 가독성까지 해칠 수 있다. 코드에서 심미론을 찾지는 않지만 내가 보기 거슬릴 때도 간혹 있다.

이에 비해서 코틀린은 적은 코드로 자바보다 훨씬 간단하게 null을 처리할 수 있기 때문에 이 방법들에 대해 정리하는 포스팅을 쓰려고 한다.

먼저 코틀린 공식문서 중 Null safety 문서부터 읽고 가자. 모든 시작은 공식문서부터다.

https://kotlinlang.org/docs/null-safety.html

 

Null safety | Kotlin

 

kotlinlang.org

타입 뒤에 명시적으로 ?을 붙임

 

첫 번째 방법은 타입 뒤에 ?을 붙여 nullable하게 만드는 방법이다.

fun main() {
    val nullableString: String? = null
    val nonNullString: String = "foo"

    val name: String
//    val address: String = null  // 에러 : null을 허용하지 않는 값에 null을 대입할 수 없음
}

주석친 코드를 주석 해제하면 null 부분에 빨간 줄이 뜨면서 컴파일 에러가 발생한다. 이유는 address의 타입이 String?이 아닌데 null을 대입하려 해서 컴파일러가 이렇게 쓰면 안된다고 알려주기 때문이다.

이 규칙은 함수의 파라미터와 리턴값에도 적용된다. 변수와 똑같이 파라미터나 리턴값에 맞는 타입을 쓰지 않으면 컴파일 에러가 발생한다.

 

fun main() {
    // 인자 line2에는 null을 사용할 수 있게 정의
    fun formatAddress(line1: String, line2: String?, city: String): String {
        return ""
    }
    // 인자 line1은 null값을 허용하지 않기 때문에 첫 번째 null 밑에 빨간 줄 생성
    formatAddress(null, null, "샌프란시스코")

    // 입력한 주소에 해당하는 우편번호를 리턴하지만 검색 결과가 없으면 null을 리턴하는 함수
    fun findPostalCode(address: String): PostalCode? {
        //
    }
    // 에러 : postalCode는 null값을 허용하지 않지만 findPostalCode()는 null값을 반환할 수 있어서 에러가 발생한다
    val postal: PostalCode = findPostalCode("1000")
}

class PostalCode(address: String)  // 예시 코드 작동을 위해 만든 가짜 클래스

 

엘비스 연산자

 

엘비스 연산자는 ?: 형태로 쓰는데, 이것을 오른쪽으로 90도 돌리면 엘비스처럼 보인다고 해서 엘비스 연산자라는 이름이 붙은 연산자다. 코틀린 공식문서에선 아래와 같이 설명한다.

nullable 참조 b가 있을 때 "b가 null이 아니면 사용하고, 그렇지 않으면 null이 아닌 값을 사용해라" 라고 말할 수 있다. 완전한 if 표현식을 쓰는 대신 엘비스 연산자(?:)를 써서 이를 표현할 수도 있다.
val l = b?.length ?: -1
?:의 왼쪽에 있는 표현식이 null이 아니면 엘비스 연산자가 이를 반환하고, 그렇지 않으면 오른쪽에 있는 표현식을 반환한다. 오른쪽 표현식은 왼쪽이 null인 경우에만 평가된다. throw 및 return은 코틀린 표현식이므로 엘비스 연산자의 오른쪽에서도 사용할 수 있다.

이 연산자는 null이 아니면 어떤 값, null이면 어떤 값을 리턴하라는 코드를 작성할 때 사용하며 사용법은 아래와 같다.

 

fun main() {
    // foo가 null이 아니면 foo, null이면 bar 리턴
    foo ?: bar
}

 

난 이 연산자를 보고 : 연산자가 떠올랐다. 안드로이드 리사이클러뷰 어댑터에서 getItemCount()를 재정의할 때 사용하곤 하는 연산자다.

이 엘비스 연산자를 쓰면 함수가 null을 리턴할 때 대신 사용할 값을 지정해줄 수 있다.

 

// 함수가 null값을 리턴하는 경우 PostalCode.NONE 값 대입
val postal: PostalCode = findPostalCode("샌프란시스코") ?: PostalCode.NONE

 

값 반환 뿐 아니라 예외를 발생시키게도 할 수 있다.

 

fun generateMapWithAddress(address: String): Image {
    // 우편번호 검색 결과가 없으면 IllegalStateException 발생
    val postal = findPostalCode(address) ?: throw IllegalStateException()
}

 

예외명 우측의 () 안에 ""으로 어떤 문자열을 쓰게 할 수도 있으니 좀 더 가독성 좋게 에러를 발생시킬 수 있겠다.

 

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

 

이 연산자는 null값의 확인과 그 처리를 동시에 하는 연산자다. 코틀린 공식문서에선 아래와 같이 설명한다.

nullable 변수의 프로퍼티에 접근하기 위한 다른 옵션은 safe call operator(?.)를 쓰는 것이다.
val a = "Kotlin"
val b: String? = null
println(b?.length)
println(a?.length) // Unnecessary safe call
b가 null이 아니면 b.length를 반환하고 그렇지 않으면 null을 리턴한다. 이 표현식의 타입은 Int?다. safe call은 체인에서 유용하다. 예를 들어 Bob은 부서에 배정되거나 배정되지 않을 수 있는 직원이다. 해당 부서에는 다른 직원이 부서장으로 있을 수 있다. Bob의 부서장(있는 경우)의 이름을 얻으려면 아래 코드를 작성한다.
bob?.department?.head?.name
체인은 그 안에 있는 프로퍼티 중 하나라도 null이라면 null을 리턴한다. null이 아닌 값에 대해서만 특정 작업을 수행하려면 safe call 연산자를 let과 함께 사용할 수 있다.
val listWithNulls: List<String?> = listOf("Kotlin", null)
for (item in listWithNulls) {
    item?.let { println(it) } // prints Kotlin and ignores null
}
할당의 왼쪽에 safe call을 걸 수도 있다. 그 다음 안전한 호출 체인의 수신자 중 하나가 null이면 할당을 건너뛰고 오른쪽의 표현식은 전혀 평가되지 않는다.
// If either `person` or `person.department` is null, the function is not called:
person?.department?.head = managersPool.getManager()

 

자바에서 연락처를 구성하는 클래스가 아래와 같이 있을 경우

 

class Contact {
    @NotNull
    String name;
    
    @Nullable
    Address address;
}

class Address {
    @NotNull
    String line1;
    
    @Nullable
    String line2;
}

null이 들어갈 위험성이 있는 부분은 Address 클래스의 line1, line2다.

그래서 line2 변수에 접근하려 할 경우 아래와 같이 if문을 작성할 수 있다.

 

public static void main(String[] args) {
    Contact contact = new Contact();
    String line;
    if (contact.address != null && contact.address.line2 != null) {
        line = contact.address.line2;
    } else {
        line = "없는 주소임";
    }
}

 

address와 address 안의 line2를 null 체크해서 null이 아닐 경우에만 미리 선언해둔 line 변수에 그 값을 대입하고, 두 조건 중 하나라도 맞지 않다면 다른 문자열을 대입하는 간단한 코드다.

위와 같은 코드를 코틀린에선 객체를 만드는 코드를 제외하고 단 한 줄로 표현할 수 있다.

 

fun main() {
    val contact = Contact("name", null)
    val line: String? = contact.address?.line2  // safe call 연산자 사용
}

class Contact(name: String, address: Address?) {
    val address = address
}

class Address(line1: String, line2: String?) {
    val line1 = line1
    val line2 = line2
}

 

2번 라인의 코드가 위의 길었던 자바 코드와 같은 역할을 한다. 너무 간단하게 null 체크가 되서 어이없을 정도다. 변태같이 간단하다

여기서 좀 전에 설명한 엘비스 연산자와 같이 쓴다면 null 대신 다른 값을 사용하도록 지정할 수도 있다.

 

fun main() {
    val contact = Contact("name", null)
    val line = contact.address?.line2 ?: "주소가 없음"
}

 

as? : 안전한 자료형 캐스팅

 

코틀린 공식문서에선 아래와 같이 설명한다.

객체가 대상 타입이 아닌 경우 일반 캐스트로 인해 ClassCastException이 발생할 수 있다. 또 다른 옵션은 시도가 성공하지 못한 경우 null을 리턴하는 safe cast를 사용하는 것이다.
val aInt: Int? = a as? Int

 

로직을 짜다 보면 다른 자료형으로 캐스팅해야 하는 경우도 자주 있다. 그러다가 아래와 같은 코드를 짤 수도 있다.

 

fun main() {
    // java.lang.ClassCastException 발생. String은 Int로 캐스팅할 수 없음
    val foo: String = "foo"
    val bar: Int = foo as Int
}

 

자바에선 에러가 발생할 것 같은 코드들을 try-catch문 안에 넣어서 처리하지만 코틀린은 as? 연산자로 어느 정도 대응이 가능하다.

이 연산자가 하는 일은 캐스팅이 실패한 경우 예외 대신 null을 반환하는 것이다. 위의 코드를 as?를 사용한다면 아래처럼 바뀌어야 한다.

 

fun main() {
    val foo: String = "foo"
    // bar가 null값을 허용하도록 Int?로 정의한다
    // 자료형 캐스팅에 실패하므로 bar에는 null값이 할당된다
    val bar: Int? = foo as? Int
    println(bar)
    // >> null
}

 

as?의 결과로 null이 들어갈 수도 있기 때문에 Int 뒤에 ?를 붙여줘야 foo as? Int 부분에 빨간 줄이 생기지 않는다.

위 코드를 실행하면 콘솔에 null이 출력되는 걸 볼 수 있다.

마찬가지로 이 코드에도 엘비스 연산자를 써서 null 대신 사용할 값을 지정할 수 있다.

 

fun main() {
    val foo: String = "foo"
    val bar: Int = foo as? Int ?: -1
    println(bar)
    // >> -1
}

 

!! : null이 아님을 보증(명시)

null 값을 포함할 수 있는 타입에 null이 아닌 값만 포함되는 경우가 생길 수도 있다. 이런 때는 !! 연산자를 쓰면 null 값을 포함할 수 있는 타입을 null 값을 포함하지 않는 타입으로 바꿔 사용할 수 있다. 보증하려는 항목 뒤에 !!을 붙이면 된다.

 

fun main() {
    // 값 foo는 null 값을 포함할 수 있는 Foo 타입
    val foo: Foo? = Foo()
    // 값 foo는 null 값을 포함하지 않음을 보증
    val nonNullFoo: Foo = foo!!
    // 값 foo가 null이 아님을 보장하면서 bar() 호출
    foo!!.bar()
    // 값 foo가 null이 아님을 보장하면서 baz 프로퍼티에 접근
    val myBaz = foo!!.baz
}

 

다른 곳에선 이 연산자를 어떻게 설명하는지 확인해보자.

 

https://www.geeksforgeeks.org/kotlin-null-safety/

 

Kotlin Null Safety - GeeksforGeeks

A Computer Science portal for geeks. It contains well written, well thought and well explained computer science and programming articles, quizzes and practice/competitive programming/company interview Questions.

www.geeksforgeeks.org

not-null assertion 연산자(!!)는 모든 값을 null이 아닌 타입으로 변환하고 값이 null인 경우 예외를 던진다. NPE를 원한다면 이 연산자를 써서 명시적으로 요청할 수 있다.

 

https://medium.com/@mook2_y2/%EC%BD%94%ED%8B%80%EB%A6%B0-%EC%9E%85%EB%AC%B8-%EC%8A%A4%ED%84%B0%EB%94%94-7-nullability-77d92220aad2

 

코틀린 입문 스터디 (7) Nullability

스터디파이 코틀린 입문 스터디 (https://studypie.co/ko/course/kotlin_beginner) 관련 자료입니다.

medium.com

not-null assertion(!!)은 개발자가 논리적으로 절대 null이 발생할 가능성이 없다고 확신할 때를 제외하면 가능한 피하는 게 좋다. 이는 결국 기존 자바의 문제점인 런타임 상에서의 NPE를 허용하는 것이기 때문이다. 그럼에도 !!이 좋은 것은 자바에 비해 가독성 측면에서 NPE가 발생할 가능성이 있는 지점을 명시적으로 보여주기 때문이다.

 

모든 포스팅이 읽어볼 가치가 높지만 개인적으론 아래 포스팅이 조금 더 좋다고 생각된다. 안드로이드를 비롯해 좀 더 실용적인 예시를 설명하는 포스팅이고 다른 미디엄 포스팅도 링크로 걸어놨기 때문에 볼거리와 생각할 거리도 많다.

https://medium.com/@igorwojda/kotlin-combating-non-null-assertions-5282d7b97205

 

Kotlin — combating non-null assertions (!!)

I have been reviewing few Kotlin projects recently and I was greatly disturbed by the fact that developers use to much of non-null…

medium.com

non-null assertion(!!)을 여러 개 사용하면 잠재적인 NPE 가능성이 높아지므로 아래와 같은 코드를 사용하지 마라. 이 줄에서 예외가 발생하면 회사가 null인지 주소가 null인지 알 수 없다.
person.company!!.address!!.country
safe call(?.) 연산자는 코드를 안전하게 유지하고 NPE가 발생하지 않게 한다. 해당 변수가 null 값을 갖지 않을 거라고 정말로 확신하는 경우 safe call 연산자에 대한 필요성을 제거하기 위해 null을 허용하지 않는 것으로 선언하기만 하면 된다...(중략)

 

정리하면 !! 연산자를 쓰는 것 자체가 잠재적인 NPE를 발생시킬 위험이 있으니 가급적이면 ?. 연산자를 사용하고, 해당 변수에 null이 진짜 진짜 진짜로 없는 게 확실하더라도 쓰지 말라는 것이 이외의 블로그를 모두 돌아보고서 얻은 결론이다.

반응형
Comments