백엔드 개발 블로그
[Kotlin] 코틀린의 널 안정성 (Null-Safety) 본문
2022/08 기준이며, Java에 익숙하다는 가정 하에 작성된 자료입니다.
- Kotlin의 장점으로는 간결성, JVM 호환성, 코루틴 등이 자주 언급되지만 그 중에서도 흔히 언급되는 것은 널 안정성이다.
- Java에서는 프로그래머가 주의 깊게 코드를 작성하지 않으면 NPE(NullPointerException)가 빈번하게 일어난다.
- Kotlin은 언어의 정적 타입 시스템에 null 관련 구조를 추가하였기 때문에 변수가 null일지 아닐지 고민하거나 수많은 Optional 등을 타이핑하지 않고도 효과적으로 NPE를 막을 수 있다.
- 기존 Java와 비교하여 어떤 점이 다른지 알아보자.
기존 Java에서는?
- String 객체의 레퍼런스는 실제 값을 가질 수도 있고 null 값을 가질 수도 있다.
String myString = null;
- 파라미터에 null이 들어올지 아닐지 모르기 때문에 null에 대한 예외 처리가 요구된다.
public void doSomething(String inputString) {
if (inputString == null) {
// ...
}
int stringLength = inputString.length();
// ...
}
- Spring 등 라이브러리에선 이러한 예외 처리를 줄이기 위해 @NonNull Annotation 등과 같은 방식으로 non-null 강제를 하고자 하였다.
- 다만 Java 언어 자체에서 제공하는 기능은 아니기 때문에 IDE 등에서 경고는 해줄지 모르지만 주의해서 코드를 작성하지 않으면 여전히 NPE가 발생하기 쉽다.
public void doSomething(@NonNull String inputString) {
int stringLength = inputString.length();
// ...
}
- Java 8에서는
Optional
이 추가되어 null일 가능성이 있는 레퍼런스를 직접 사용하기보단 Wrapper로 한번 감싸서 NPE를 줄이고자 하였다. - 하지만
Optional
을 사용하고 말고는 언어 단에서 강제되지 않기 때문에Optional
이 붙지 않았다고 해서 non-null이라고는 담보할 수 없었다. - 그리고 코드가 비교적 간결하지 못하다는 단점이 있다.
public void doSomething(Optional<String> inputStringOptional) {
String inputString = inputStringOptional.orElse("default");
int stringLength = inputString.length();
// ...
}
- 위와 같이 Java 진영에서도 NPE를 막기 위한 여러 도구를 사용하고자 했지만 언어 자체에 녹여내기는 어려웠고 NPE는 여전히 골칫거리였다.
- Kotlin에서는 다소 다른 정적 타입 구조를 도입함으로써 NPE를 근본적으로 방지하고자 하였다.
Kotlin의 nullable 타입
- Kotlin에서
String
과 같이 타입을 선언한다면, 해당 타입의 레퍼런스에는 null이 들어갈 수 없다. - NPE를 걱정하지 않고 사용할 수 있다.
- 물론 글 마지막에서 설명할 예외 케이스가 존재한다.
fun doSomething(inputString: String) {
val stringLength = inputString.length
// ...
}
- 아래와 같이
String?
와 같이 타입을 선언한다면, 해당 타입의 레퍼런스에는 null이 들어갈 수 있다. - JDK 8의
Optional
과 유사하다고 할 수 있다. - nullable한 타입의 변수에 대해 NPE가 발생할 수 있는 메소드 / 필드에 접근하고자 하면 컴파일 단계에서 오류가 발생한다.
fun doSomething(inputString: String?) {
// 오류 발생!!
val stringLength = inputString.length
// ...
}
Safe calls
- nullable한 변수의 메소드나 필드에 접근할 때 변수명에
?
을 붙인 것을 safe call 연산자라고 한다. - 아래 코드에서
inputString?.length
와 같이 접근했을 때,inputString
이 null이 아닌 경우라면 length의 값이 되지만, null이라면stringLength
에도 null 값이 들어가게 된다. - 자연스럽게
stringLength
의 타입 또한Int
가 아닌 nullable한Int?
가 된다.
fun doSomething(inputString: String?) {
// stringLength 또한 nullable(Int?) 타입이 된다.
val stringLength = inputString?.length
// ...
}
- 이와 같은 Safe call은 위처럼 여러 겹으로 감싸져있는 필드에 접근할 때 매우 유용하다.
- 만약 Java 코드에서 아래처럼 접근하려면 몇개의 if 문 혹은 삼항 연산자가 필요했을지 생각해보자.
val memberStatus = response?.member?.status
let
함수를 호출하여 null이 아닐 경우에만 특정 코드를 실행할 수 있다.
fun doSomething(inputString: String?) {
val stringLength = inputString?.length
// stringLength가 null이 아닐 경우에만 출력이 된다.
stringLength?.let { println(it) }
// ...
}
- 아래와 같이 좌변에서도 safe call을 사용할 수 있다.
- 만약 좌변의 chain 중 어떤 값이 null일 경우 assignment는 수행되지 않는다.
request?.member?.status = memberStatus
엘비스 연산자 (Elvis Operator, ?:
)
- Java에서 null에 따른 예외 처리 / default 값을 사용하기 위해선 아래와 같이 if 문 혹은 삼항 연산자를 사용하거나...
public void doSomething(String inputString) {
int stringLength = inputString != null ? inputString.length() : -1;
// ...
}
- Optional 등을 이용하는 방법이 있었다.
public void doSomething(Optional<String> inputStringOptional) {
int stringLength = inputStringOptional.map(String::length).orElse(-1);
// ...
}
- Kotlin에서는 엘비스 연산자 (Elvis Operator,
?:
)를 사용하면 위와 동일한 기능을 구현할 수 있다. - 아래 코드에서
stringLength
는Int?
가 아닌Int
타입을 가지게 된다.
fun doSomething(inputString: String?) {
val stringLength = inputString?.length ?: -1
}
- Kotlin에서는
return
과throw
또한 expression이기 때문에 중간에 null을 반환하거나...
fun doSomething(inputString: String?): String? {
val stringLength = inputString?.length ?: return null
// ...
}
- 오류를 throw할 수 있다.
fun doSomething(inputString: String?): String? {
val stringLength = inputString?.length ?: throw IllegalArgumentException("input expected")
// ...
}
!!
연산자 (사용 지양)
!!
연산자를 사용하면 nullable한 type을 강제로 non-null로 변환할 수 있다.- 아래 코드에서
stringLength
는Int?
가 아닌Int
타입을 가지게 된다.
fun doSomething(inputString: String?) {
val stringLength = inputString!!.length
// ...
}
- 이 때 레퍼런스가 null의 값을 가지고 있다면 NPE가 발생한다.
- 당신이 NPE 애호가가 아닌 이상
!!
연산자는 지양하고 엘비스 연산자(?:
)를 사용하여 별도의 예외처리를 하는 것이 대부분의 경우 적절하다고 생각한다.
Nullable collections
filterNotNull()
메소드를 이용하면 nullable한 collection을 non-null collection으로 안전하게 변경할 수 있다.
val nullableList: List<Int?> = listOf(1, 2, null, 4)
val intList: List<Int> = nullableList.filterNotNull()
정말 NPE가 발생하지 않나요?
- 이 글에서 Kotlin이 NPE를 근본적으로 방지하고자 했다고 하지만 정말 NPE가 발생하지 않을까? 당연하겠지만 예외는 있다.
throw NullPointerException()
으로 NPE를 직접 던지는 NPE 애호가!!
연산자를 굳이 사용하는 NPE 애호가- 객체 초기화 시 아직 초기화되지 않은 객체에 잘못된 접근을 하는 경우
- 객체 생성자에서는 객체 초기화가 완료되지 않은
this
에 접근할 수 있는데 이 때 외부에서this
의 속성들에 접근할 경우 NPE가 발생할 수 있다. - 부모 클래스의 생성자에서 자식 클래스의 아직 초기화되지 않은 open 멤버에 접근할 경우
- 공식 가이드 참고
- 객체 생성자에서는 객체 초기화가 완료되지 않은
- 다른 언어의 코드와 함께 사용할 경우
- Java 코드에서는 null을 미연에 방지할 수 없기 때문
참고
'Kotlin' 카테고리의 다른 글
[Kotlin] 코틀린 launch, async 차이점 (0) | 2022.08.23 |
---|
Comments