Search

Null Safety

null 안전성은 null 참조의 위험을 크게 줄이기 위해 고안된 Kotlin 기능이다.
Java를 포함한 여러 프로그래밍 언어에서 가장 흔한 문제 중 하나는 null 참조 멤버에 접근할 때 발생하는 널 참조 예외(NullPointerException, NPE)이다.
Kotlin은 null 가능성을 타입 시스템의 일부로 명시적으로 지원하며 어떤 변수나 속성이 null 값을 가질 수 있는지를 명확하게 선언할 수 있다.
null 값을 허용하지 않는 변수를 선언하면 컴파일러가 해당 변수가 null 값을 가질 수 없도록 강제해 NPE를 방지한다.
Kotlin의 null 안전성은 런타임이 아닌 컴파일 시점에 잠재적인 null 관련 문제를 잡아낸다.
이 기능은 코드의 견고성, 가독성, 유지보수성을 향상시키며 null 값을 명시적으로 표현하여 코드를 이해하고 관리하기 쉽게 만든다.

Kotlin에서 NPE가 발생할 수 있는 경우

1.
throw NullPointerException() 을 명시적으로 호출한 경우
2.
null 안전성 검사를 무시하는 !! 연산자를 사용한 경우
3.
초기화 중 데이터 불일치로 인한 경우
a.
생성자에서 초기화되지 않은 this를 다른 곳에서 사용하는 경우
b.
상위 클래스 생성자가 하위 클래스에서 초기화되지 않은 상태를 사용하는 경우
4.
Java와 상호 운용하는 경우
a.
플랫폼 타입의 null 참조 멤버에 접근하는 경우
b.
제네릭 타입에서 발생하는 문제들의 경우
c.
외부의 Java 코드로 발생하는 문제들의 경우
null 안전성과 관련된 또 다른 예외는 UninitializedPropertyAccessException 이다. 이 예외는 초기화되지 않은 속성에 접근하려고 할 때 발생하며 보장된 시점 이전에 non-null 속성이 사용되지 않도록 한다. lateinit 속성에서 주로 발생한다.

Nullable 타입 및 Non-nullable 타입

var a: String = "abc" a = null // 오류 발생: 널은 비널 타입인 String의 값이 될 수 없음
Kotlin
복사
Kotlin에서는 null 값을 가질 수 있는 타입과 null 값을 가질 수 없는 타입을 구분한다.
var b: String? = "abc" b = null // 정상 처리
Kotlin
복사
null 값을 허용하려면 변수 타입 뒤에 ? 를 붙여야 한다.
val l = b?.length
Kotlin
복사
null 가능 타입의 변수를 사용해 메서드나 속성에 접근하려 하면 컴파일러가 에러를 발생시킨다.
이를 해결하기 위해서는 안전한 호출 연산자 ?. 를 사용해야 한다.

Nullable 타입 처리 방법

if 조건을 통한 null 검사

// nullable 변수에 null 할당 val b: String? = null // 먼저 널 가능성을 확인한 후 length에 접근 val l = if (b != null) b.length else -1 print(l) // -1
Kotlin
복사
컴파일러는 스마트 캐스트를 사용하여 타입을 nullable String? 에서 non-nullable String 으로 변경한다.
또한 컴파일러는 null 여부를 확인한 정보를 추적하며 if 조건문 내에서 length 호출을 허용한다.
// nullable 문자열을 변수에 할당 val b: String? = "Kotlin" // 먼저 널 가능성을 확인한 후 length에 접근 if (b != null && b.length > 0) { print("문자열 길이: ${b.length}") // 조건을 충족하지 않을 경우 대체 값을 제공 } else { print("빈 문자열") // 출력: 문자열 길이: 6 }
Kotlin
복사
컴파일러가 b가 확인 후 사용 시까지 변경되지 않는다는 것을 보장할 수 있을 때만 작동한다.

안전한 호출 연산자

// nullable 문자열을 변수에 할당 val a: String? = "Kotlin" // nullable 변수에 null 할당 val b: String? = null // 널 가능성을 확인하고 길이를 반환하거나 null 반환 println(a?.length) // 6 println(b?.length) // null
Kotlin
복사
?. 연산자는 널 가능성을 더 간결하게 안전하게 처리할 수 있도록 도와준다.
객체가 null인 경우 NPE를 던지는 대신 ?. 연산자는 단순히 null을 반환한다.
Kotlin에서는 ?. 연산자를 varval 변수 모두에 사용할 수 있다.
var은 null이 아닌 값을 가지고 있다면 언제든지 이를 null로 변경할 수 있다.
val은 null이 아닌 값을 한 번 할당하면 이후에 이를 null로 변경할 수 없다.
bob?.department?.head?.name
Kotlin
복사
안전 호출은 체인에서 유용하게 사용된다.
// if 조건문으로 작성한 예시 if (person != null && person.department != null) { person.department.head = managersPool.getManager() } // 안전 호출로 작성한 예시 person?.department?.head = managersPool.getManager()
Kotlin
복사
안전 호출 체인 중 하나라도 null이면 할당이 건너뛰어지며 오른쪽의 표현식은 전혀 평가되지 않는다.

Elvis 연산자

// if 조건을 사용한 예시 val b: String? = null val l: Int = if (b != null) b.length else 0 println(l) // 0 // Elvis 연산자를 사용한 예시 val b: String? = null val l = b?.length ?: 0 println(l) // 0
Kotlin
복사
nullable 타입을 다룰 때 null을 체크하고 대체 값을 제공할 수 있다.
Elvis 연산자는 ?: 왼쪽에 있는 표현식이 null이 아닐 경우 해당 값을 반환하고 null일 경우 오른쪽에 있는 표현식을 평가하고 반환한다.
fun foo(node: Node): String? { // getParent()를 체크하여 null이 아니면 parent에 할당, null이면 null을 반환 val parent = node.getParent() ?: return null // getName()을 체크하여 null이 아니면 name에 할당, null이면 예외를 던짐 val name = node.getName() ?: throw IllegalArgumentException("name expected") // ... }
Kotlin
복사
Kotlin에서 throwreturn 은 표현식이므로 Elvis 연산자의 오른쪽에 사용할 수 있다.

Not-null 선언 연산자

// nullable 문자열을 변수에 할당 val b: String? = "Kotlin" // b를 non-null로 취급하고 그 길이에 접근 val l = b!!.length println(l) // 6
Kotlin
복사
!! 연산자를 null이 아닌 값에 적용하면 해당 값은 안전하게 non-nullable 타입으로 처리된다.
// nullable 변수에 null을 할당 val b: String? = null // b를 non-null로 취급하고 그 길이에 접근하려 함 val l = b!!.length println(l) // Exception in thread "main" java.lang.NullPointerException
Kotlin
복사
!! 연산자를 null인 값에 적용하면 NPE(NullPointerException)가 발생한다.

Nullable 수신자 타입

fun main() { // nullable Person 객체를 person 변수에 null로 할당 val person: Person? = null // nullable person 변수에 .toString()을 적용하고 문자열을 출력 //toString()의 확장함수 내부에서 null을 처리 println(person.toString()) // null } // 간단한 Person 클래스를 정의 data class Person(val name: String)
Kotlin
복사
nullable 수신자 타입으로 확장 함수를 사용할 수 있으며 null일 수 있는 변수에서도 호출될 수 있다.
nullable 수진자 타입으로 확장 함수를 정의하면 함수를 호출할 때마다 null을 체크하는 대신 함수 내에서 null 값을 처리할 수 있다.
확장 함수를 통해 null 값을 처리하지 않는다면 NPE(NullPointerException)이 발생한다.
fun main() { // nullable Person 객체를 변수에 할당 val person1: Person? = null val person2: Person? = Person("Alice") // person이 null이면 "null"을 출력하고, 그렇지 않으면 person.toString() 결과를 출력 println(person1?.toString()) // null println(person2?.toString()) // Person(name=Alice) } // Person 클래스를 정의 data class Person(val name: String)
Kotlin
복사
확장 함수가 nullable 문자열을 반환하길 기대한다면 안전 호출 연산자 ?. 를 사용할 수 있다.

let 함수

// nullable 문자열의 리스트를 선언 val listWithNulls: List<String?> = listOf("Kotlin", null) // 리스트의 각 항목을 반복 for (item in listWithNulls) { // 항목이 null인지 확인하고 null이 아닌 값만 출력 item?.let { println(it) } // Kotlin }
Kotlin
복사
null 값을 처리하고 null이 아닌 타입에서만 연산을 수행하려면 안전 호출 연산자 ?.let 함수를 함께 사용할 수 있다.
이 조합은 표현식을 평가하고 결과가 null인지 확인한 다음 null이 아닐 경우에만 let 함수를 실행하여 수동 null 체크를 피하는데 유용하다.

안전한 캐스트

// 어떤 타입의 값도 가질 수 있는 Any 타입의 변수를 선언 val a: Any = "Hello, Kotlin!" // 'as?' 연산자를 사용하여 Int로 안전하게 캐스트 val aInt: Int? = a as? Int // 'as?' 연산자를 사용하여 String으로 안전하게 캐스트 val aString: String? = a as? String println(aInt) // null println(aString) // "Hello, Kotlin!"
Kotlin
복사
정규 캐스트 연산자는 as 연산자이지만 객체가 대상 타입이 아닐 경우 예외를 발생시킬 수 있다.
안전한 캐스트를 위해 as? 연산자를 사용할 수 있다.
안전한 캐스트는 값을 지정된 타입으로 캐스트하려고 시도하고 값이 해당 타입이 아닐 경우 null을 반환한다.

nullable 타입의 컬렉션

// null과 non-null 정수 값을 포함하는 리스트를 선언 val nullableList: List<Int?> = listOf(1, 2, null, 4) // null 값을 필터링하여 non-null 정수의 리스트를 생성 val intList: List<Int> = nullableList.filterNotNull() println(intList) // [1, 2, 4]
Kotlin
복사
nullable 요소의 컬렉션이 있고 null이 아닌 요소만 유지하고 싶다면 filterNotNull() 함수를 사용할 수 있다.