Notice
Recent Posts
Recent Comments
Link
«   2024/12   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
Tags
more
Archives
Today
Total
관리 메뉴

백엔드 개발 블로그

[Kotlin] 코틀린의 널 안정성 (Null-Safety) 본문

Kotlin

[Kotlin] 코틀린의 널 안정성 (Null-Safety)

베꺼 2022. 8. 6. 17:47

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, ?:)를 사용하면 위와 동일한 기능을 구현할 수 있다.
  • 아래 코드에서 stringLengthInt?가 아닌 Int 타입을 가지게 된다.
fun doSomething(inputString: String?) {
    val stringLength = inputString?.length ?: -1
}
  • Kotlin에서는 returnthrow 또한 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로 변환할 수 있다.
  • 아래 코드에서 stringLengthInt?가 아닌 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 Null Safety 공식 가이드

'Kotlin' 카테고리의 다른 글

[Kotlin] 코틀린 launch, async 차이점  (0) 2022.08.23
Comments