Kotlin - Null을 안전하게 처리하는 방법 (Null safety, 널 안정성)

자바의 경우 int, boolean과 같은 primitive type을 제외한 객체들은 항상 null이 될 수 있습니다. 코틀린은 자바와 다르게 Nullable과 Non-nullable 타입으로 프로퍼티를 선언할 수 있습니다. Non-nullable 타입으로 선언하면 객체가 null이 아닌 것을 보장하기 때문에 null check 등의 코드를 작성할 필요가 없습니다.

이 글에서는 코틀린에서 Null 객체를 다루는 방법들을 소개합니다. 먼저 Nullable과 Non-nullable 타입의 차이점에 대해서 알아보고, 안전하게 null을 처리하는 방법들에 대해서 알아볼 것입니다.

Nullable과 Non-Nullable 프로퍼티

코틀린에 익숙하시다면 차이점에 대해서 아실 것입니다. 타입을 선언할 때 ?를 붙이면 null을 할당할 수 있는 프로퍼티이고, ?가 붙지 않으면 null이 허용되지 않는 프로퍼티를 의미합니다.

var nullable: String? = "nullable"
var nonNullable: String = "non-Nullable"

nullable 프로퍼티는 null을 할당할 수 있지만, nonNullable에 null을 할당하려고 하면 컴파일 에러가 발생합니다.

nullable = null      // 컴파일 성공
nonNullable = null   // 컴파일 에러

코틀린은 nullable과 non-nullable 개념을 만들어, null에 안전한 프로그램을 만들 수 있게 도와줍니다. 그래서 코틀린만 사용한다면 Null Pointer Exception 같은 예외가 발생하지 않을 수 있습니다. 또한, 자바처럼 try-catch 구문을 많이 쓰지 않아도 됩니다.

코틀린에서 Null Pointer Exception(NPE)이 발생하는 경우

코틀린에서는 NPE가 발생하지 않을 것 같지만 자바의 라이브러리를 쓰는 경우 NPE가 발생할 수 있습니다. 자바에서는 non-nullable 타입이 없기 때문에 자바 라이브러리를 사용할 때 nullable 타입으로 리턴됩니다.

nullable 타입을 non-nullable 타입으로 변경하기

코틀린에서 아래와 같은 자바 라이브러리를 사용한다고 가정하겠습니다. 이 함수는 String을 리턴하며, 코틀린에서는 이 타입을 nullable인 String?으로 인식합니다.

String getString() {
  String str = "";
  ....
  return str;
}

코틀린에서 이 함수의 리턴 값을 non-nullable인 String으로 변환하고 싶습니다. 그럼 이 프로퍼티가 항상 null이 아닌 것을 보장할 수 있고, try-catch 로 NPE를 처리하지 않아도 됩니다. 하지만 아래 코드처럼 대입하면 컴파일 에러가 발생합니다. String?타입을 String타입에 할당하려고 했기 때문입니다.

var nonNullString1: String = getString()     // 컴파일 에러

반면에 아래 코드는 컴파일됩니다. 그 이유는 !! 연산자를 사용했기 때문입니다. !!연산자는 객체가 null이 아닌 것을 보장합니다. 만약 null이라면 NPE를 발생시킵니다.

var nonNullString2: String = getString()!!   // 컴파일 성공

이런 이유로 !!연산자는 null이 아닌 것을 보장할 수 있는 객체에만 사용해야 합니다.

안전하게 nullable 프로퍼티 접근하기

코틀린에서 nullable 프로퍼티를 안전하게 접근하는데 사용하는 다양한 방법들에 대해서 알아보겠습니다.

조건문으로 nullable 접근

가장 쉬운 방법은 if-else를 이용하는 것입니다. 자바에서는 흔히 사용하는 방식입니다.

아래 코드는 String?을 접근하기 전에 if로 null을 체크하는 코드입니다.

val b: String? = "Kotlin"
if (b != null && b.length > 0) {
    print("String of length ${b.length}")
} else {
    print("Empty string")
}

단점은 if-else 루프가 반복되는 경우 가독성을 해칠 수 있습니다.(if-else 루프 지옥)

Safe call 연산자로 nullable 접근

Safe call은 객체를 접근할 때 ?.로 접근하는 방법을 말합니다. 예를들어 아래 코드에서 b?.length를 수행할 때 b가 null이라면 length를 호출하지 않고 null을 리턴합니다. 그렇기 때문에 NPE가 발생하지 않습니다.

val a: String = "Kotlin"
val b: String? = null
println(b?.length)
println(a?.length) // Unnecessary safe call

위의 코드에서 a?.length는 불필요하게 Safe call을 사용하고 있습니다. a는 Non-nullable이기 때문입니다.

아래 여러 객체로 둘러 쌓인 String에 접근하는 코드입니다. a?.b?.c?.d?를 수행할 때, 이 객체들 중에 null이 있으면 null을 리턴합니다.

println(a?.b?.c?.d?.length)

안전하게 nullable 프로퍼티 할당

어떤 프로퍼티를 다른 프로퍼티에 할당할 때, 객체가 null인 경우 default 값을 할당하고 싶을 수 있습니다. 자바에서는 삼항연산자를 사용하여 아래 코드처럼 객체가 null인 경우 default값을 설정해줄 수 있습니다.

String b = null;
int l =  b != null ? b.length() : -1;

하지만 코틀린은 삼항연산자를 지원하지 않습니다. 삼항연산자를 대체할 수 있는 것들에 대해서 알아보겠습니다.

if-else

if-else로 삼항연산자를 대체할 수 있습니다. 자바와는 다르게 코틀린은 한줄로 if-else를 쓸 수 있습니다. 다음은 if-else를 사용하여 삼항연산자와 동일한 내용을 구현한 코드입니다.

val l = if (b != null) b.length else -1

엘비스 연산자(Elvis Operation)

엘비스 연산자는 ?:를 말합니다. 삼항연산자와 비슷한데 ?: 왼쪽의 객체가 null이 아니면 이 객체를 리턴하고 null이라면 ?:의 오른쪽 객체를 리턴합니다. 다음은 위의 if-else 예제를 엘비스 연산자를 사용하여 구현한 코드입니다.

val l = b?.length ?: -1

Safe Cast

코틀린에서 형변환할 때 Safe Cast를 이용하면 안전합니다. 아래 코드에서 string은 문자열이지만 Any타입입니다. as?를 이용하여 String과 Int로 형변환을 시도하고 있습니다. String은 가능하기 때문에 성공하였고, Int는 타입이 맞지 않기 때문에 null을 리턴하였습니다.

val string: Any = "AnyString"
val safeString: String? = string as? String
val safeInt: Int? = string as? Int
println(safeString)
println(safeInt)

실행 결과를 보면 safeInt는 캐스팅이 실패하여 null이 할당되었습니다.

AnyString
null

Collection의 Null 객체를 모두 제거

Collection에 있는 Null 객체를 미리 제거할 수 있는 함수도 제공합니다. 다음은 List에 있는 null 객체를 filterNotNull 메소드를 이용하여 삭제하는 코드입니다.

val nullableList: List<Int?> = listOf(1, 2, null, 4)
val intList: List<Int> = nullableList.filterNotNull()
println(intList)

실행 결과를 보면 null이 제거된 나머지 아이템들만 출력이 됩니다.

[1, 2, 4]

정리

코틀린은 Null에 안전하도록 설계되었습니다. 그래서 프로그램이 null에 안전하게 구현하는 기능들에 대해서 알아보았습니다.

참고

Loading script...
codechachaCopyright ©2019 codechacha