Kotlin In Action - 널 가능성

SeungHyuk Shin·2022년 1월 13일
0
post-thumbnail

이 글은 Kotlin In Action 6장을 참고 요약 하였습니다.


null이 될수 있는 타입

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

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

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

fun getLen(str: String) = str.length

코틀린에서는 명시적으로 null을 인자로 넣을 수 없다. null을 넣는 구문이 있다면 compile 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없이 사용했기 때문이다.


안전한 호출 연산자 : ?.

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() {
    val ceo = Employee("Da Boss", null)
    val developer = Employee("Bob Smith", ceo) 
    println (managerName(developer)) 
    println (managerName(ceo))
}

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

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 country ?: "Unknown"
}

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

엘비스 연산자: ?:

위에 코드에서 사용 된 ?: 연산자가 보일 것이다.

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

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

엘비스 연산자는 우항으로 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() {
    val address = Address("Elsestr. 47", 80687, "Munich", "Germany")
    val jetbrains = Company("JetBrains", address) 
    val person = Person("Dmitry", jetbrains)
    printShippingLabel(person)
    printShippingLabel(Person("Alexey", null))
}

안전한 캐스트 : as?

스마트 캐스트인 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() {
    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; 
} ...

널 아님 단언: !!

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

fun main() {
    ignoreNulls(null)
}

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

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

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

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

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

!!가 꼭 변수가 소리지르는 것 같은데 이게 코틀린 개발자들이 의도한 바라고 한다.


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을 해주는 경우가 나을수도 있다.


나중에 초기화할 프로퍼티

객체의 인스턴스를 생성후 초기화는 나중에 하는 코드들이 많이 존재한다.
단, 코틀린에서의 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이 발생할 수 있다.


널이 될 수 있는 타입 확장

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

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을 체크할 수 있다.


타입 파라미터의 널 가능성

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)
}

T는 Any의 상한제한을 같기 때문에 이제 T는 not null 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을 사용할때 ArrayList<Stirng?>?로 사용해야만 합니다.

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

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

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

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

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

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

0개의 댓글