본문 바로가기

코틀린

코틀린(Kotlin) - 타입 파라미터의 null 가능성

코틀린에서는 함수나 클래스의 모든 파라미터는 null이 될 수 있습니다.

따라서 타입 파라미터 T를 클래스나 함수 안에서 사용하면 변수명 끝에 물음표가 없더라도 T가 null이 될 수 있는 타입입니다.

 

/* null이 될 수 있는 타입 파라미터 예제 */
fun <T> printHashCode(t: T) {
   println(t?.hashCode()) //"t"가 null이 될 수 있으므로 안전한 호출을 사용
}

>>> printHashCode(null) //"T"의 타입은 "Any?"로 추론된다.
null

 

위의 예제 printHashCode 함수 호출에서 타입 파라미터 T에 대해 추론한 타입은 null이 될 수 있는 Any? 타입으로 추론이 되었습니다.

t 파라미터의 타입 T에는 분명히 물음표가 붙어있지 않지만 t는 null이 될 수 있습니다.

 

타입 파라미터 T가 null이 아님을 확실히 하려면 null이 될 수 없는 타입 상한(upper bound)를 지정해야 합니다.

타입 상한을 지정하게 되면 null이 될 수 있는 값을 제한할 수 있게 됩니다.

 

/* 타입 파라미터 T에 대해 null이 될 수 없는 타입 상한 지정하는 예제 */
fun <T: Any> printHashCode(t: T) { //이제 "T"는 null이 될 수 없는 타입입니다.
   println(t.hashCode())
}

>>> printHashCode(null) //이 코드는 컴파일되지 않습니다. 
                        //null이 될 수 없는 타입의 파라미터에 null을 넘길 수 없습니다.
Error: Type parameter bound for 'T' is not satisfied

>>> printHashCode(42)
42

 

타입 파라미터 T는 null이 될 수 있는 타입을 표시하기 위해 반드시 물음표(?)를 타입명 뒤에 붙여야 하는 규칙의 유일한 예외입니다.

 

null 가능성과 자바

코틀린은 자바와 상호운용성을 강조하는 언어지만 이미 알다시피 자바 타입 시스템에는 null 가능성이란 것을 지원하지 않습니다.

그렇다면 자바와 코틀린을 조합하면 어떤 일이 생길까요?

이 둘을 조합한 프로그램은 불안전할까요?

아니면 모든 값을 쓸 때마다 null인지 체크해야 할까요?

아니면 좋은 해법이 있을까요?

 

이제 그 질문에 대한 해답을 찾아보겠습니다! 😎

 

먼저,

자바 코드에도 어노테이션(Annotation)으로 표시된 null 가능성 정보가 있어서, 이런 정보가 코드에 명시되어 있으면 코틀린도 이 정보를 활용합니다.

따라서 자바의 @Nullable String 은 코틀린 입장에서 볼 때 String? 와 같고, 자바의 @NotNull String 은 코틀린의 String과 같습니다.

 

코틀린은 어노테이션이 붙은 자바 타입을 어노테이션에 따라 null 가능여부 타입으로 취급한다.

 

아래는 흔히 볼 수 있는 @Nullable과 @NonNull 어노테이션이 적용된 사례입니다.

 

 

하지만 대부분의 경우 이런 어노테이션을 사용하지 않는 경우가 훨씬 더 많은데요 흥미로운 사실은 이런 경우 자바의 타입은 코틀린의 플랫폼 타입(platform type)이 됩니다.

 

플랫폼 타입

플랫폼 타입은 코틀린이 null 관련 정보를 알 수 없는 타입을 말합니다.

그 타입을 null이 될 수 있는 타입으로 처리해도 되고, null이 될 수 없는 타입으로 처리해도 된다는 의미입니다.

즉, 자바와 동일하게 해당 값이 null이 될 수도 있음을 개발자가 알고 있다면 이 값을 사용하기 전에 null 체크를 할 필요가 있고, 반드시 null이 아닐 것 같다면 이 값을 그대로 사용해도 됩니다.

다만, 우리의 예상이 틀렸다면 자바와 마찬가지로 NullPointerException이 발생하게 될 것입니다.

 

자바 타입은 코틀린에서 플랫폼 타입으로 표현됩니다. null이 될 수 있는 타입이나 null이 될 수 없는 타입 모두로 사용할 수 있습니다.

 

/* 어노테이션이 없는 자바 클래스 */
public class Person {
    
    private final String name;

    public Person(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

 

getName()은 null을 리턴하게 될지 안 할지 코틀린 컴파일러는 String 타입의 null 가능성에 대해 전혀 알 수 없습니다.

그렇기 때문에, null 체크를 우리 개발자가 직접 처리해야만 합니다.

물론, 이 변수가 null이 아님을 확신할 수 있다면 자바와 마찬가지로 null 체크 없이 사용해도 되지만 만약 null일 경우에는 NPE가 발생할 수 있음을 염두해야 합니다.

 

/* null 체크 없이 자바 클래스 접근 예제 */
fun upperCase(person: Person) {
    println(person.name.toUpperCase()) //person.name이 null이여서 예외 발생
}

>>> upperCase(Person(null))

java.lang.IllegalArgumentException: Parameter specified as non-null
is null: method toUpperCase, parameter $receiver

 

여기서 NullPointerException이 아니라 toUppercase()가 수신 객체($receiver)로 null을 받을 수 없다는 더 자세한 예외가 발생함을 주목해볼 수 있습니다.  

 

/* null 체크를 통해 자바 클래스 접근 예제 */
fun upperCase(person: Person) {
    println(person.name?.toUpperCase())
}

 

Tip !
자바 API를 다룰 때는 대부분의 경우 null 관련 어노테이션을 쓰지 않기 때문에 마치 모든 타입이 null이 아닌 것처럼 느껴질 수 있습니다.
그렇기 때문에 오류를 피하려면 자바 API를 다룰 때 해당 값에 대한 null 체크를 해주는 것이 좋습니다. 

앞서 알아보았지만 플랫폼 타입은 null이 될 수 있는 타입이나 없는 타입 어느 쪽으로든 사용할 수 있기 때문에

아래 두 선언은 모두 올바른 선언입니다.

 

val name: String? = person.name //null이 될 수 있는 타입으로 볼 수 있다.
val name: String = person.name //null이 아닌 타입으로 볼 수 있다.

상속

코틀린에서 자바 메소드를 오버라이드 할 때 그 메소드의 파라미터와 반환 타입을 null이 될 수 있는 타입으로 선언할지 null이 될 수 없는 타입으로 선언할지 결정해야 합니다.

 

/* String 파라미터가 있는 자바 인터페이스 예제 */
interface StringProcessor {
   void process(String value);
}

 

코틀린 컴파일러는 다음과 같이 두 가지 가능성으로 인터페이스 구현이 가능합니다.

 

/* 자바 인터페이스를 여러 다른 null 가능성으로 구현하는 예제 */
class StringPrinter : StringProcessor {
   override fun process(value: String) {
      println(value)
   }
}

class StringPrinter : StringProcessor {
   override fun process(value: String?) {
      if(!value.isNullOrBlank) {
         println(value)
      }
   }
}

 

자바 클래스나 인터페이스를 코틀린에서 구현할 경우 null 가능성을 제대로 처리하는 일이 매우 중요합니다.

 

마무리

지금까지 null이 될 수 있는 타입, null이 될 수 없는 타입과 의미에 대해 살펴보면서

- 안전한 호출(?.)

- 엘비스 연산자(?:)

- 안전한 캐스트(as?)

등 안전한 프로그래밍을 위한 안전 연산자와 안전하지 못한 null 아님 연산자(!!) 에 대해 알아보았습니다.

 

또한 let 함수를 사용해 null 여부에 따라 인자를 전달할 수 있음과 확장 함수(String?.isNullOrBlank() 등...)를 통해 null 체크를 함수 안으로 옮길 수 있음을 살펴보았습니다.

 

그리고 자바 타입을 코틀린에서 표현할 때 사용하는 플랫폼 타입에 대해 정리하였습니다.

 

코틀린의 null 가능성 관련 지식은 코틀린 프로그래밍을 할 때 굉장히 중요한 역할을 하기 때문에 반드시 숙지하고 기억해야 하는 개념입니다.😀