본문으로 바로가기
반응형

이 글은 Kotlin In Action을 참고 하였습니다.

더욱 자세한 설명이나 예제는 직접 책을 구매하여 확인 하시기 바랍니다

코틀린에서는 자바보다 null 처리를 좀더 명확하게 합니다.

따라서 NPE(NullPointerException)가 발생하는 빈도를 현저히 낮출 수 있습니다.

6.1.1 null이 될수 있는 type

코틀린은 null이 될수 있는 type을 명시적으로 표시할 수 있습니다.

 //자바 
public int getLen(String str) {
    return str.lengh();
}

자바에서 위 함수는 컴파일시 문제없이 빌드 되지만, run time에 인자로 null이 들어오면 NPE가 발생합니다.

 fun getLen(str: String)  = str.length
코틀린에서는 명시적으로 null을 인자로 넣을수 없습니다.

null을 넣는 구문이 있다면 complie time에 에러를 발생시킵니다.

fun strLenSafe(s: String?): Int =
    if (s != null) s.length else 0

fun main(args: Array) {
    val x: String? = null
    println(strLenSafe(x))
    println(strLenSafe("abc"))
}

type에 ?를 붙임으로서 null이 가능한 변수임을 명시적으로 표현합니다.

만약 위 코드에서 if(s != null) 없이 s.length를 호출한다면 이 역시 컴파일에러가 발생합니다.

null이 들어올 수 있는 타입이지만 내부에서 null check없이 사용했기 때문입니다.


6.1.2 null safe operator

null을 안전하게 처리하기 위해 코틀린은 ?. 연산자를 지원합니다.

fun printAllCaps(s: String?) {
    val allCaps: String? = s?.toUpperCase()
    println(allCaps)
}

fun main(args: Array) {
    printAllCaps("abc")
    printAllCaps(null)
}

?. 연산자를 사용하면, 앞의 변수가 null이 아닐때만 오른쪽 함수가 수행되고 null이면 null을 반환합니다.

if (s != null) s.toUpperCase() else null 와 같습니다.

이렇게 긴 문장을 ?. 하나로 표현 가능합니다.

여기서 왼쪽항이 null이면 바로 null을 반환한다는걸 꼭 명심하시기 바랍니다.


property 접근시에도 ?. 연산자를 사용하면 편리하게 null 처리를 할 수 있습니다.

class Employee(val name: String, val manager: Employee?)

fun managerName(employee: Employee): String? = employee.manager?.name

fun main(args: Array) {
    val ceo = Employee("Da Boss", null)
    val developer = Employee("Bob Smith", ceo)
    println(managerName(developer))
    println(managerName(ceo))
}

property 접근시에도 ?.를 이용하여 쉽게 null 처리를 할 수 있습니다.

또한 아래 코드처럼 연속적인 사용도 가능합니다.

class Address(val streetAddress: String, val zipCode: Int,
              val city: String, val country: String)

class Company(val name: String, val address: Address?)

class Person(val name: String, val company: Company?)

fun Person.countryName(): String {
   val country = this.company?.address?.country
   return if (country != null) country else "Unknown"
}

fun main(args: Array) {
    val person = Person("Dmitry", null)
    println(person.countryName())
}

엄청나게 코드가 간소화 되는게 보이시는지요?


6.1.4 Elvis operator

?. 연산자는 좌항이 null이면 null을 반환합니다.

코드를 작성하다 보면 null인 경우 default 값을 주고 싶은경우가 있습니다.

이때 ?: 를 사용할 수 있습니다. (생긴게 엘비스 프레슬리 헤어를 닮았다고 해서 붙여진 이름이랍니다.)

 fun getName(str: String?) {
    val name = str ?: "Unknown"
}

위 코드는 if (str != null) str else "Unknown" 과 같은 코드 입니다.

엘비스 연산자는 우항으로 return이나 throw도 넣을 수 있습니다.

따라서 간결한 코드로 원하는 형태의 null 처리가 가능 합니다.

class Address(val streetAddress: String, val zipCode: Int,
              val city: String, val country: String)

class Company(val name: String, val address: Address?)

class Person(val name: String, val company: Company?)

fun printShippingLabel(person: Person) {
    val address = person.company?.address
      ?: throw IllegalArgumentException("No address") //company 정보가 없으면 exception 강제 발생
    with (address) {
        println(streetAddress)
        println("$zipCode $city, $country")
    }
}

fun main(args: Array) {
    val address = Address("Elsestr. 47", 80687, "Munich", "Germany")
    val jetbrains = Company("JetBrains", address)
    val person = Person("Dmitry", jetbrains)
    printShippingLabel(person)
    printShippingLabel(Person("Alexey", null))
}

위 코드에서 엘비스 연산자 우항에 throw를 통해 강제로 exception을 발생시켰습니다.

또한 with를 통해 address 객체의 반복없이 간결하게 property를 호출할 수 도 있습니다.


자바에서는 if not null을 위해서 엄청난 군더더기 코드들이 들어갑니다.

그런 부분들에 있어서 코틀린은 참 획기적이네요.

참고로 ?. 를 cascading하게 사용하여 최종적으로 ?: 로 처리하면 어디서 null이 발생했는지는 알기 어렵습니다.

각 단계별로 default값을 다른게 찍고 싶다면 ?. 을 따로 분리해서 사용해야 합니다. 

6.1.5 safe cast

스마트 캐스트인 is 를 이용하면 as를 사용하지 않고도 type을 변환할 수 있습니다.
단 as를 바로 사용하여 casting 할때 type이 맞지 않으면 ClassCastException이 발생합니다.

따라서 kotlin에서는 이를 방지하는 as? 를 지원합니다.
as? 는 casting을 시도하고, casting이 불가능 하면 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
   }

   override fun hashCode(): Int =
      firstName.hashCode() * 37 + lastName.hashCode()
}

fun main(args: Array) {
    val p1 = Person("Dmitry", "Jemerov")
    val p2 = Person("Dmitry", "Jemerov")
    println(p1 == p2)
    println(p1.equals(42))
}

위 예제에서 o as? Person ?: return false는 자바로 한다면 아래 구문과 같습니다.

//자바
Person o = null;
if (o instanceOf Person) {
    o = (Person)o;
} else {
    return false;
}
...   
코드 라인수만 해도 얼마나 간소화 되었는지 알 수 있습니다.


6.1.6 강제 not null 처리

변수를 nullable로 설정한다면, 해당 변수는 사용할 때 마다 null 처리를 진행해야 합니다.
실제로 코딩을 하다보면 변수는 nullable로 설정하였으나, 코드 flow상 null이 절대 들어가지 않는 경우가 있습니다.
하지만 컴파일러는 이를 인식할 수 없기 때문에 계속 null 처리를 해주면서 코딩을 해야 합니다.

사실....자바에서는 이렇게 strict 하게 null을 다루지 않기 때문에 코틀린에서 null 처리를 하다보면 엄청 짜증나는 역효과를 경험하게 됩니다.

이럴줄 알았는지, 코틀린에서는 nullable로 설정된 property를 강제로 not null로 바꿔주는 operator를 지원합니다.

fun ignoreNulls(s: String?) {
    val sNotNull: String = s!!
    println(sNotNull.length)
}

fun main(args: Array) {
    ignoreNulls(null)
}

!! 이 표시를 property 나 변수에 붙이면 강제로 null이 아님을 선언하게 됩니다.

따라서 그 이후부터는 not null로 인식되어 처리됩니다.

물론 이렇게 해 놓고 null을 넣으면 NPE가 발생합니다.

만약 null을 인자로 넣은 경우 자바였다면 println(sNotNull.length) 라인에서 NPE가 발생합니다.

하지만 kotlin에서는 val sNotNull: String = s!! 라인에서 NPE가 발생하여 좀더 명확한 위치를 콕 찝어 줍니다.


위에서도 했던 얘기지만 !!도 아래와 같이 cascading해서 사용할 수 있습니다.

person.company!!.address!!.country

하지만 이건 정말 어디서 NPE가 발생했는지 알수 없습니다.

따라서 !!은 cascading해서 쓰지 않도록 권장됩니다.


6.1.7 let 함수

not null인 경우 특정 구문을 수행하고 싶을때가 많습니다.

코틀린에서는 not null인 경우에만 지정된 구문을 실행해 주는 let이란 함수를 제공합니다.

let 함수를 사용하면 자신의 receiver 객체를 람다식 내부로 넘겨줍니다.

fun sendEmailTo(email: String) {
    println("Sending email to $email")
}

fun main(args: Array) {
    var email: String? = "yole@example.com"
    email?.let { sendEmailTo(it) }
    email = null
    email?.let { sendEmailTo(it) }
}

let 함수 내부에서는 receiver 객체를 it으로 받아서 표현합니다.

따라서 email?.let {email -> sendEmailTo(email)} 로 사용해도 됩니다.

위 코드에서 ?.을 사용하여 let을 호출했으므로 람다 내부에서 it은 null이 아닙니다.

또한 null이라면 let의 람다구문은 수행조차 안됩니다.


let은 계속하여 중첩사용이 가능하지만 중첩이 늘어나면 가독성이 떨어질 수 있으므로 차라시 if로 null check을 해주는 경우가 나을수도 있습니다.


6.1.8 Property의 초기화 지연

객체의 인스턴스를 생성후 초기화는 나중에 하는 코드들이 많이 존재합니다.
단, 코틀린에서의 property 초기화는 항상 생성자 안에서만 가능하기 때문에 나중에 초기화를 해야하는 코드들을 사용하기가 어렵습니다.
(특히나 val 로 선언된 property는 생성자 안에서 반드시 초기화 해야 합니다.)

이를 지원하기 위해 코틀린에서는 lateinit 키워드를 지원합니다.
class MyService {
    fun performAction(): String = "foo"
}

class MyTest {
    private lateinit var myService: MyService

    @Before fun setUp() {
        myService = MyService()
    }

    @Test fun testAction() {
        Assert.assertEquals("foo",
            myService.performAction())
    }
}

lateinit으로 myService 변수를 선언했기 때문에 not null이지만 초기화 작업을 바로 하지 않아도 됩니다.

따로 초기화 변수에서 초기화 진행을 수행하면 됩니다.


myService 변수는 항상 null이 아니다라는 가정을 갖습니다.

따라서 초기화 이전에 해당 함수에 접근한다면 run time에 myService has not been initialized" 란 exception이 발생할 수 있습니다.


6.1.9 nullable type의 extension function

널이 가능한 객체에 확장 함수를 선언할 수 있습니다.

fun verifyUserInput(input: String?) {
    if (input.isNullOrBlank()) {
        println("Please fill in the required fields")
    }
}

fun main(args: Array) {
    verifyUserInput(" ")
    verifyUserInput(null)
}

String? type에서는 isNullOrBlank() 또는 isNullOrEmpty()란 함수를 지원합니다.

해당 객체가 null이거나 빈 객체라면 false를 반환합니다.

보통 자바라면 null.isNullOrBlank()를 호출하는 경우이므로 NPE가 발생했겠지만 위 함수는 정상 동작 합니다.


이는 해당 함수가 아래와 같이 선언되어 있기 때문입니다.

fun String?.isNullOrBlank(): Boolean =
    this == null || this.isBlank()

즉 확장함수 선언시 receiver 객체의 null을 검사하는 코드를 먼저 넣음으로서 NPE 없이 null을 체크할 수 있습니다.


6.1.10 Generic의 Nullable

Generic을 사용하면 이는 무조건 nullable로 인식됩니다.

fun <T> printHashCode(t: T) {
    println(t?.hashCode())
}

fun main(args: Array) {
    printHashCode(null)
}

T?가 붙지 않았지만 기본으로 이는 ? 붙은것과 다르지 않습니다.

따라서 함수 내부에서는 반드시 null check을 해야 합니다.


따라서 non null을 기본으로 제너릭을 사용하려면 upper bound에 대한 제한을 명시적으로 넣어야 합니다.
fun <T: Any> printHashCode(t: T) {
    println(t.hashCode())
}

fun main(args: Array) {
    printHashCode(null)
}

TAny의 상한제한을 같기 때문에 이제 T는 not null type 입니다.


6.1.11 Platform type

코틀린에서는 null이 될수 있음과 없음을 변수에서 선언하여 사용함으로써 NPE 발생 확률을 늦춤니다.
문법적으로 지정하기 때문에 가능한 NPE 발생 가능성을 막도록 코딩을 유도 하는것이죠.

다만 자바와 연동에 있어 자바는 이러한 제한없이 쓰기 때문에 문제가 됩니다.

  • 자바의 @Nullable String == 코틀린의 String?
  • 자바의 @NonNull String == 코틀린의 String
자바의 annotation에 따라 코틀린의 null type이 정해 집니다.
(어노테이션은 표준, 안드로이드, 젯브레인 모두를 지원합니다.)

문제는 자바에서 해당 어노테이션 없이 쓰는 변수나 인자들이 대부분이라는 점입니다.

이런 불분명한 타입은 코틀린에서 플랫폼 타입으로 변환됩니다.


플랫폼 타입은 널처리를 해도 되고 안해도 상관이 없습니다.

다만 널 체크가 필요한 부분에 하지 않는다면 NPE가 발생하며, null check 여부는 개발자의 몫입니다.

코틀린 컴파일러는 플랫폼 타입에 한해서 null 처리가 중복된다거나, 필요없는데 했다거나 하는 warning을 띄우지 않습니다.

// 자바
public class Person {
    private final String mName;
    public Person(String name) {
        mName = name;
    }

    public String getName() {
        return mName;
    }
}


위와 같은 자바클래스가 존재한다면 getName을 했을때 null 발생 소지가 있습니다.

코틀린에서 null체크 없이 사용하더라도 전혀 컴파일시 문제가 되지 않습니다.

다만 개발자의 판단에 의해서 아래와 같이 코딩해야만 Exception을 피할 수 있습니다.

(단! 이런경우 NPE가 아닌 IllegalArgumentException이 발생 합니다.)

fun yellAtSafe(person: Person) {
    println((person.name ?: "Anyone").toUpperCase() + "!!!")
}

fun main(args: Array) {
    yellAtSafe(Person(null))
}


만약 코틀린에서 플랫폼 타입이 아닌 무조건 nullable type으로 자바 type을 치환했다면 ArrayList<String>을 사용할때 ArrayList<Stirng?>?로 사용해야만 합니다.

따라서 이런 어이없는 경우를 막기위해서 플랫폼 타입이 적용되었다고 합니다.


플랫폼 타입은 !를 써서 표현됩니다.

따라서 컴파일 내부에서 String! 으로 표현되지만 실제 개발자는 코트상에서 플랫폼 타입을 직접 선언할 수는 없습니다.


자바와 코틀린의 null에 대한 접근자체가 다르지만 호환성을 지원해야 하기 때문에 null에 대한부분 만큼은 개발자가 신경써서 사용해야 합니다.

예를틀어 자바의 인터페이스나, 객체를 상속받아 해당 메서드를 구현할때 override 하는 method의 인자는 null check가 필요한지 아닌지를 명확하게 결정하고 사용해야 합니다.

해당 함수를 다른 코틀린 코드가 접근할 수 있으므로 컴파일러는 null이될수 없는 타입으로 선언한 모든 파라미터에 대한 널이 아님을 검사하는 단언문을 만들어 줍니다. 이 함수를 자바에서 null을 넣고 호출한다면 전부 예외가 발생하게 됩니다.

반응형