코틀린 타입 시스템(1)

tkppp·2021년 12월 25일
0

Kotlin

목록 보기
6/11

Nullable

코틀린은 null이 될 수 있는 타입과 null이 될 수 없는 타입을 구분한다. 만약 변수가 null이 될 수 있는 가능성이 있을 경우에는 해당 변수 타입 뒤에 ?를 붙여 지정해 Nullable한 변수라는 것을 명시해야한다.

val str: String? = ...

안전한 호출 연산자 ?.

nullable 타입을 지정하면 이에 대한 null 처리가 필수적이다. if로 널 처리를 핻도 되지만 이를 ?. 연산자를 이용하여 간단하게 처리할 수 있다.

val p = Person()

p?.getName()

if(p.getName() != null){
    return p.name
}
else{
    return null
}

엘비스 연산자 ?:

?.를 이용해 널처리를 쉽게 할 수 있지만 변수가 null일 경우 null만 반환한다는 문제가 있다. 이를 해결하기 위해 엘비스 연산자로 null일때 반환할 값을 지정할 수 있다.

val p = Person()

p?.getName() ?: "none"

if(p.getName() != null){
    return p.name
}
else{
    return "none"
}

코틀린에서는 return이나 throw 등도 연산의 식이다. 따라서 엘비스 연산자의 우항에 return이나 throw 등의 연산을 집어 넣을 수도 있다.

안전한 캐스트 as?

코틀린에서는 as 키워드를 통해 캐스팅을 한다. 만약 캐스팅을 할 수 없다면 ClassCastException이 발생한다. as? 는 캐스팅이 불가능한 경우 예외가 아닌 null을 반환한다. 이를 엘비스 연산자와 연계해 사용할 수 도 있다.

class Person(val firstName: String, val lastName: String){
    override fun equals(o: Any?): Boolean{
        val otherPerson = o as? Person ?: return false
        
        return otherPerson.firstName == firstName && otherPerson.lastName == lastName
    }
}

널 아님 단언 !!

!! 를 통해 Nullable 타입을 일반 타입으로 변환할 수 있다. 만약 그 변수가 null이라면 코틀린은 예외를 발생시킨다.

가급적 !!을 사용하기보다는 다른 방법을 사용하는 것이 좋다.

let 함수

let 함수를 안전한 호출 연산자와 함께 사용하면 원하는 식을 평가해서 결과가 널인지 검사한 다음 그 결과를 변수에 넣는 작업을 할 수 있다. 수신 객체가 null이 아니라면 let 함수의 람다 인자로 그 객체를 넘긴다. null이라면 let 함수는 실행되지 않는다.

fun sendEmailTo(email: String) { ... }

sendEmailTo( ) 함수는 널이 아닌 문자열만 인자로 넣을 수 있다. 만약 Nullable한 인자를 넣으려면 먼저 널 검사를 수행해야 한다.

val email = "gowldla0423@naver.com"
if(email != null) sendEmailTo(email)

위 코드를 let함수를 이용해 변경 할 수 있다.

"gowldla0423@naver.com"?.let { sendToEmail(it) }

만약 let을 안전한 호출 연산자 ?.와 같이 쓰지 않는다면 수신객체는 Nullable 타입으로 취급받는다.

나중에 초기화할 프로퍼티

코틀린에서 널이 될 수 없는 프로퍼티는 생성자 안에서 초기화 하지 않고 특별한 메소드 안에서 초기화할 수 없다. 프로퍼티의 초기화를 하지 않고 사용할려면 그 프로퍼티를 Nullable한 타입으로 선언해야한다. 하지만 프로퍼티 사용에 있어 널 체크나 !! 연산자를 사용해야만 하는 불편함이 있다. 하지만 lateinit 키워드를 이용해 널이 될 수 없는 타입의 프로퍼티도 나중에 초기화할 수 있다.

class MyService {
    fun performAction(): String = "foo"
}

class MyTest {
    private var myService: MyService? = null
    
    @Before fun setUp(){
        myService = MyService()
    }
    
    @Test fun testAction() {
        Assert.assertEquals("foo", myService!!.performAction())
    }
}

// lateinit
class MyTest {
    private lateinit var myService: MyService
    
    @Before fun setUp(){
        myService = MyService()
    }
    
    @Test fun testAction() {
        // 널 체크 필요 X
        Assert.assertEquals("foo", myService.performAction())
    }
}

만약 lateinit 프로퍼티를 초기화하지 않고 사용하면 어떻게 될까? 이때는 lateinit property가 초기화되지 않았다는 예외가 발생한다. 런타임에 에러가 발생하지만 정확이 어디서 초기화가 되지 않았는지 알 수 있기 때문에 널포이터 예외보다 빠른 디버깅이 가능하다.

nullable과 자바

만약 코틀린에서 null을 반환할 수 있는 함수로 변수를 초기화 한다고 생각해보자. 이 경우에도 자연스럽게 코틀린 컴파일러는 nullable 타입임을 추론 할 수 있다. 코틀린 함수에서는 null을 반환할 경우 반환 타입을 nullable로 강제하기 때문에 NullPointerException이 발생할 가능성을 컴파일 단계에서 차단한다. 문제는 자바 라이브러리를 사용하는 경우다.

public class NullableClass {
    public static String getIsNull(int n){
        if(n % 2 == 0){
            return "I'm not null";
        }
        else{
            return null;
        }
    }
}
fun getIsNullInKt(n: Int): String? = when(n % 2 == 0){
    true -> "I'm not null"
    else -> null
}

for(i in 0 until 5){
    val res1 = getIsNullInKt(i)
    val res2 = NullableClass.getIsNull(i)
    println("res1 - $i : ${res1.length}")	// 1번 - 컴파일 에러, 널 처리 필요
    println("res2 - $i : ${res2.length}")	// 2번 - 에러 X, 실행중 널포인터 에러
}

위의 코드에서 1번과 2번을 실행한다면 1번에서는 컴파일 에러가 뜬다. nullable한 타입임을 명시하지 않았더라도 컴파일러가 nullable한 타입임을알아채고 null 처리를 하라고 한다. 하지만 자바 코드를 사용한 2번에서는 컴파일러가 이를 잡아내지 못한다. 자바에서는 nullable 타입이 없기 때문이다. 그렇기 때문에 자바 라이브러리를 사용하는 경우 널이 될 수 있다면 스마트 캐스트를 이용한 초기화는 지양해야 한다.

for(i in 0 until 5){
    val res1 = getIsNullInKt(i)
    val res2: String? = NullableClass.getIsNull(i)
    println("res1 - $i : ${res1.length}")	// 1번 - 컴파일 에러, 널 처리 필요
    println("res2 - $i : ${res2.length}")	// 2번 - 컴파일 에러, 널 처리 필요
}

위와 같이 변수의 타입을 nullable로 명시하면 정상적으로 컴파일러가 잡아낸다.

플랫폼 타입

자바에서 Nullable 타입이 없을 뿐이지 구별은 가능하다. @NotNull, @Nullable 등의 어노테이션을 통해 이를 구분할 수 있다. 하지만 그렇지 않는 경우에는 코틀린은 자바의 타입을 플랫폼 타입으로 규정한다.

플랫폼 타입은 널 관련 정보를 알 수 없는 타입을 말한다. 타입을 Nullable, NotNull로 처리하던지 컴파일러는 상관하지 않는다. 플랫폼 타입에 대해서는 책임을 사용자에 있다는 것이다. 위의 코드에서 컴파일 에러가 뜨지 않는 것이 바로 이 때문이다.

0개의 댓글