엄범


최근 ``kt null``에 대한 접근 방법은 런타임에 발생하는 NPE를, 컴파일 타임으로 옮기는 것이다.

널이 될 수 있음과 없음에 대한 모든 검사는 컴파일 타임에 수행되기 때문에, 실행 시점에는 널이 될 수 있는 타입과 널이 될 수 없는 타입의 객체가 같아진다. 

단, ``kt @NotNull`` 애너테이션이나 

``java Intrinsics.checkExpressionValueIsNotNull()``

``java Intrinsics.checkParameterIsNotNull()``같은 체크가 추가된다.


그리고 또 한 가지 중요한 것이, 모든 검사가 컴파일 타임에 수행되기 때문에 런타임에 가져오는 데이터에 대해서는 Exception이 발생할 수 있다.

따라서 런타임에 가져오는 데이터가 널이 될 수 있는지, 없는지를 조금 더 신경써서 생각해야 한다. 널이 될 수 있는 타입으로 지정해 놓으면 ``kt ?.`` 등의 널 처리 연산자를 이용해 간편하게 처리할 수 있다.

런타임에 ``kt null``을 가져와 널이 될 수 없는 타입에 집어 넣거나, ``kt !!``를 잘못 사용하는 경우 프로그램 실행 도중 다음 예외가 발생하며 종료될 수 있다.

  • ``java java.lang.IllegalStateException: ??? must not be null``
  • ``kt kotlin.TypeCastException: null cannot be cast to non-null type``

그러나 여기서 또 중요한게... 분명 널이 될 수 없는 타입인데 외부 데이터를 가져오면서 여기에 ``kt null``이 들어갔음에도 예외가 발생하지 않는 경우가 있으므로 주의해야 한다. 예를 들어 ``kt Gson()``은 인스턴스화에 Java reflection을 사용하는데, 이러한 경우 코틀린이 이러한 프로세스에 관여할 수 있는 방법이 없으므로 널이 될 수 없는 타입을 ``kt null``로 초기화해도 예외가 발생하지 않는다. 이런 경우 객체 초기화 시점에서가 아니라 멤버에 접근하는 시점에서 ``java java.lang.NullPointerException``이 발생하게 된다.


안전한 호출 연산자 ?.    /    엘비스 연산자 ?:

```kt

s?.toUpperCase()    // 다음 구문과 동일

if (s != null) s.toUpperCase() else null    // s==null이면 결과 타입도 null이 된다는 점 유의.

```

연쇄해서 사용한다면 코드를 굉장히 줄일 수 있다. 

엘비스 연산자 ``kt ?:``와 연계해서 사용하면 이런 모습이다.

```kt

class Address(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 = 

         this.company?.address?.country ?: "Unknown"


>>> val p = Person("umbum", null)

>>> println(p.countryName())

```

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

또는, ``kt Nothing`` 타입 함수를 엘비스 연산자의 우항에 넣고 사용하면 좋다.

```kt

val address = person.company?.address ?: throw IllegalArgumentException("No address")

```


안전한 캐스트 as?

```kt
o as? Person    // 다음 구문과 동일
if (o is Person) o as Person else null    // 역시 결과 타입이 null이 된다는 점.
```

이 것도 연산 수행 결과가 ``kt null``이 될 수 있기 때문에 엘비스 연산자와 연계해서 사용한다.
```kt
o as? Person ?: return false
```


널 아님 단언 !!

웬만하면 다른 방법을 사용하는 것이 좋으나 다음과 같은 상황에 유용하다.
  • ``kt var``이거나, 커스텀 접근자를 사용하는 프로퍼티에 대한 null check 수행 이후. (스마트 캐스트가 동작하지 않음)
  • 액션 API에서 액션이 호출되었을 때, 어떤 값이 필연적으로 존재해야만 이 액션이 호출될 수 있는 경우. 즉, 그 값이 ``kt null``일 수가 없음이 자명한 경우.
``kt !!``를 사용했는데 ``kt null``값이 들어오면 NPE가 발생한다.
Note ) 스마트 캐스트가 작동하는 프로퍼티에 대해서는 널 체크를 수행하면서 스마트 캐스트가 작동하기 때문에 ``kt !!``를 사용할 필요가 없다. 
```kt
fun f1(v: String?) {
    val str = v ?: return
    f2(str)    // 여기서 !!는 필요 없다.
}

fun f2(str: String) {
    println(str)
}
```

var 지연 초기화 : lateinit

안드로이드에서 액티비티 초기화에 사용하는 ``kt onCreate``같이 생성자 안에서 초기화하지 않고 특별한 메소드에서 초기화해야 하는 경우, 일단 변수에 ``kt null``을 집어넣고 이후에 초기화해야 하는 방식을 사용해야 한다.
근데 변수에 일단 ``kt null``을 집어넣으려면 ``kt bar?``로 선언해주어야 하는데, 이러면 널 체크를 해줘야 하는 등 불편하다.
이럴 때 ``kt lateinit``을 사용하면, 프로퍼티를 나중에 초기화 할 수 있다.
```kt
class LateInitTest {
    lateinit var myService: MyService

    @Before fun setUp() {
        myService = MyService()
    }
}
```
초기화하기 전에 프로퍼티에 접근하면 예외가 발생하며, 단순 NPE가 발생하는 것 보다 확인하기 용이하다.
Note ) ``kt lateinit`` modifier is not allowed on properties of primitive types

val 지연 초기화 : lazy

immutable에 대해서 지연 초기화 하려면 ``kt by lazy {}``를 뒤에 붙여준다.
lateinit과 lazy의 차이는 이 것 말고도 몇 가지 더 있다.

널이 될 수 있는 타입 확장

직접 정의할 수도 있고, 그냥 가져다 사용할 수도 있으나 굳이 사용해야 하는 필요성은 못느끼겠다. 오히려 더 헷갈릴 지도 모르겠다.
널이 될 수 있는 타입에 대해 확장을 정의했다면, 널이 될 수 있는 값에 대해서 그 확장을 호출할 수 있다. 
``kt this``가 널이 될 수 있으므로 정의할 때 명시적으로 널 체크 해주어야 한다.
```kt
>>> var s:String? = null
>>> s.isNullOrBlank()
true
```


플랫폼 타입

자바의 타입은 코틀린에서 플랫폼 타입, 즉 널 관련 정보를 알 수 없는 타입이 된다.

단, 자바 원시 타입의 값은 널이 될 수 없으므로 널이 될 수 없는 타입으로 취급된다.


플랫폼 타입은 널이 될 수 있는 타입으로 처리해도 되고 널이 될 수 없는 타입으로 처리해도 되기 때문에, 이 값이 널이 될 수 있는지 없는지 잘 생각해보고 널이 될 수 없다면 그냥 사용하면 되고, 널이 될 수 있다면 널 체크를 수행해주어야 한다.

아무튼, 자바 API를 사용할 때는 널을 반환할지 아닐지를 잘 생각해보아야 한다.

```kt

>>> val p = JavaPerson("umbum")

>>> println(p.name.capitalize())

Umbum

>>> val np = JavaPerson(null)

>>> println(np.name.capitalize())

java.lang.IllegalStateException: np.name must not be null

```


플랫폼 타입은 ``kt Type!``으로 표시된다. 직접 선언할 수는 없고 자바에서 가져온 것만 이렇게 표기된다.

플랫폼 타입은 널이 될 수 있는 타입이 될 수도, 널이 될 수 없는 타입이 될 수도 있기 때문에 둘 다 대입할 수 있다.

```kt

>>> val i: Int = np.name

error: type mismatch: inferred type is String! but Int was expected

>>> val kp: String? = p.name

>>> val kp: String = p.name

```

그러나, 이미 ``kt null``이 들어 있는 상태에서 널이 될 수 없는 타입에 대입하려고 하면 Exception이 발생한다.

```kt

>>> val kp: String? = np.name

>>> val kp: String = np.name

java.lang.IllegalStateException: np.name must not be null

```


아무튼, 그냥 널이 될 수 있는지 없는지만 신경쓰면 된다.

자바 클래스나 인터페이스를 코틀린에서 상속 또는 구현하는 경우에도 널이 될 수 있는지 없는지만 신경써서 붙여주면, 나머지는 컴파일러가 알아서 해준다.