코틀린은 자바와 달리 nullable type 이 존재한다.
다음의 자바 코드를 보자.
int strLen(String s) {
return s.length();
}
이 함수는 안전하지 않다. 이 함수에 null 값을 넘기면 NullPointerException 이 발생하기 때문이다.
코틀린으로 이 함수를 다시 작성해보자.
fun strLen(s: String) = s.length
코틀린은 함수를 위와 같이 작성할 경우, null 값이나 null 이 될수도 있는 값을 인자로 넘기는 것은 애초에 금지된다.
위처럼 타입을 String 으로 작성하면 무조건 null 이 될수 없는 String 객체를 의미하며, null 값도 허용하고 싶은 경우 String? 처럼 끝에 ‘?’(물음표) 를 붙여주어야 한다.
String?, Int?, MyClass? 등 어떤 타입이든 타입 이름 뒤에 물음표를 붙이면 그 타입의 변수나 프로퍼티에 null 참조를 저장할 수 있다는 뜻이다.
Type? = Type 혹은 null
물음표가 없는 타입은 그 변수가 null 참조를 저장할 수 없다는 뜻이며, 따라서 코틀린에서 모든 타입은 기본 적으로 null 이 될 수 없는 타입이다.
타입(Type) 이란?
타입은 어떤 값들이 가능한지와 그 타입에 대해 수행할 수 있는 연산의 종류를 결정한다.
이말인 즉슨, double 타입은 일반적인 수학 연산을 적용할 수 있고 string 타입은 length 와 같은 문자열 연산을 적용할 수 있다. 하지만 자바에서 String 타입의 변수에는 String 이나 null 이라는 두 가지 종류의 값이 들어갈 수 있다. 하지만 String 과 null 은 완전히 다른 타입이며 적용되는 연산도 완전히 다르다.
이는 자바의 타입 시스템이 널을 제대로 다루지 못한다는 뜻이다.
변수에 선언된 타입이 있지만 null 여부를 추가로 검사하기 전에는 그 변수에 대해 어떤 연산을 수행할 수 있을지 확신할 수 없다.
그래서 코틀린은 이 문제를 해결하기 위해 null 이 될 수 있는 타입과 될 수 없는 타입을 구분했다. 이를 통해 각 타입의 값에 대해 어떤 연산을 적용할 수 있을지 명확해지고 RuntimeException 을 발생시킬 수 있는 여지를 줄일 수 있다.
Safe calls (안전한 호출) : ?.
코틀린의 ?. 문법은 null 검사와 메소드 호출을 한 번에 수행한다. 아래 두 줄의 코드는 같은 역할을 수행한다.
s?.toUpperCase()if (s != null) s.toUpperCase() else null
호출하려는 값이 null 이 아니면 ?. 은 일반 메소드 호출처럼 동작하는 것이다. 호출하려는 값이 null 이면 이 호출은 무시되고 null 이 반환된다.
이를 다시 말하면 safe call 의 return type 은 nullable 이라는 뜻이다.
val allCaps: String? = s?.toUpperCase()
Elvis Operator (엘비스 연산자) : ?:
코틀린에는 null 대신 사용할 디폴트 값을 지정할 때 사용할 수 있는 편리한 연산자를 제공한다. 이를 엘비스 연산자라고 부른다.
fun foo(s: String?) {
val t: String = s ?: "empty string"
}
엘비스 연산자의 우항에는 return, throw 등의 연산을 넣을 수 있다.
Safe casts (안전한 캐스트) : as?
as 는 코틀린의 타입 캐스트 연산자이다. 자바 타입 캐스트와 마찬가지로 대상 값을 as 로 지정한 타입으로 바꿀 수 없으면 ClassCastException 이 발생한다. 이를 예방하려면 as 를 사용할 때 마다 is 연산자로 해당 타입이 as 로 변환 가능한 타입인지 검사해야 한다. 코틀린은 이 작업을 간소화 시켜주는 as? 를 제공한다.
as? 는 어떤 값을 지정한 타입으로 변환을 시도하지만 만약 변환할 수 없다면 null 을 반환한다.
| -> foo is Type -> foo as Type
foo as? Type -
| -> foo is not Type -> null
그래서 일반적인 안전한 캐스팅 패턴은 as? 를 수행한 뒤 엘비스 연산자를 사용하는 것이다. 이는 equals 메소드에 특히 유용 하다.
override fun equals(o: Any?): Boolean { val otherPerson = o as? Person ?: return false // 타입 불일치
return otherPerson.firstName == firstName && // 안전한 캐스팅이 끝난 이후
otherPerson.lastNmae == lastName
}
not-null assertion : !!
느낌표를 두개 붙인 !! 는 not-null assertion 으로 어떤 값이든 널이 될 수 없는 타입으로 바꿀 수 있다. 하지만 실제로 null 인 값에 !! 를 적용하면 NPE 가 발생한다.
근복적으로 !! 는 컴파일러에게 “나는 이 값이 null 이 아님을 잘 알고있으며, 따라서 예외가 발생해도 감수하겠다” 라고 말하는 것이다.
따라서 정말 null 이 아님을 확신하지 않는이상 !! 를 남발하는 것은 피하자.
특히 !! 를 한줄에 여러개 사용하는 것은 반드시 피하자.
person.company!!.address!!.conutry
위 같은 경우는 NPE가 발생해서 stackTrace 를 따라가도 company가 null 이어서 발생했는지 address 가 null 이어서 발생했는지 알 수 없다.
let 함수
null 일 수 있는 어떤 변수가 null 이 아닐 경우에만 코드를 실행하고 싶을 경우 let 을 활용하자.
let? 을 호출하면 수신 객체가 null 이 아닌 경우에만 람다를 실행해준다.
let 함수는 자신의 수신 객체를 인자로 전달받은 람다에게 넘긴다. (it) null 일 수 있는 타입에 대해 let? 으로 람다를 호출하면 해당 람다에서는 null 이 될 수 없는 타입의 값으로 바뀌어 전달된다.
자바의 this와 코틀린의 this 의 차이점
자바에서는 메소드 안의 this는 그 메소드가 호출된 수신 객체를 가리키므로 항상 널이 아니다. 수신 객체가 널이었다면 NPE 가 발생해서 메소드 안으로 들어가지도 못한다. 따라서 자바에서 메소드가 정상 실행된다면 그 메소드의 this는 항상 널이 아니다.
하지만 코틀린에서는 널이 될 수 있는 타입의 확장 함수 안에서는 this가 널이 될 수 있다.
코틀린 타입 파라미터의 널 가능성
코틀린에서 함수나 클래스의 모든 타입 파라미터는 기본적으로 널이 될 수 있다. 타입 파라미터 T를 클래스나 함수 안에서 타입으로 사용하면 이름 끝에 물음표가 없더라도 T는 널이 될 수 있는 타입이다.
파라미터 t의 타입 T에는 ? 가 붙어있지 않지만 t는 null 을 받을 수 있다. t가 null 이 아님을 확실히 하려면 null 이 될 수 없는 타입 상한(upper bound) 을 지정해야 한다.
위의 경우 T에 Any(null 이 될 수 없는 최상위 클래스) 타입을 명시함으로써 null 이 될 수 없도록 한정했다.
자바와 코틀린의 nullable 타입 호환성
자바 타입 시스템은 널 가능성을 지원하지 않는다. 그래서 자바 코드에서 어노테이션으로 표시된 널 가능성에 대한 정보를 코틀린은 적극 활용한다. 따라서 자바의 @Nullable String은 코틀린에서 볼 때 String? 과 같고, 자바의 @NotNull String 은 코틀린의 String 과 같다.
하지만 자바에서 위와같은 null 가능성에 대한 어노테이션이 없는 경우 자바의 타입은 코틀린의 플랫폼 타입(Platform Type) 이 된다.
플랫폼 타입
플랫폼 타입이란 코틀린이 null 관련 정보를 알 수 없는 타입을 말한다. 이말인 즉슨, 자바와 마찬가지로 플랫폼 타입에 대해 수행하는 모든 연산에 대한 책임은 온전히 개발자에게 있다는 뜻이다. 컴파일러는 해당 타입에 대해 모든 연산을 허용하며, 컴파일러는 아무 경고도 표시해주지 않는다.
어떤 플랫폼 타입의 값이 null이 아닐 것이라 확신한다면 null 검사 없이 그 값을 직접 사용해도 된다. 하지만 자바와 마찬가지로 당신이 틀렸다면 NPE 가 발생한다.
코틀린에서 플랫폼 타입을 직접 선언할 수는 없다. 자바 코드에서 가져온 타입만 플랫폼 타입이 된다. 하지만 IDE나 컴파일러 오류 메시지에서는 플랫폼 타입을 볼 수 있다.
여기서 코틀린 컴파일러가 표시한 String! 이라는 타입은 자바 코드에서 온 플랫폼 타입이다. ! 표기는 String! 타입의 널 가능성에 대해 아무 정보도 없다는 뜻이다.