코틀린의 특징을 말하자면 코드를 nullable 하게 짤 수 있다는 점일 겁니다. 오늘을 그 null은 어떻게 처리하는 게 좋을까라는 주제로 말을 해볼까 합니다.
null은 잘못사용하게 되면 어떻게 될까요?
val printer: Printer? = getPrinter()
printer.print() // 컴파일 오류
아래처럼 컴파일러 차원에서 오류가 나오기도 합니다. 또한 null이 아니라는 것을 확신을 하여!!(not- null assertion)을 사용하게 된다면 해당값이 null 이 아니라면 잘 써지겠지만 만약 이 printer가 null이라면 NPE(null point exception)가 생기고 맙니다. 그렇기 때문에 이번장에서는 null을 어떻게 처리해야 안전하게 처리할 수 있는지에 대해서 자세히 설명하고 있습니다.
책에서는 null을 처리하는 방법을 3가지 소개하고 있습니다.
- ?. 스마트캐스팅, Elvis 연산자 등을 활용해서 안전하게 처리한다
- 오류를 throw 한다.
- 함수 혹은 프로퍼티를 리팩터링 해서 nullable 나오지 않게 한다.
null을 안전하게 처리하기
?.(safe call) 방식을 사용하게 되면 해당값이 null이 아닐 경우에만 실행하기 때문에 안전합니다. 그렇지만 매번 safe call을 사용할 수는 없으니 스마트 캐스팅을 이용하여 null이 아님을 보장한 후 해당스코프 이후부터는 안전하게 사용할 수 있습니다. 스마트케스팅은 직접적으로 조건문으로 null 이 아님을 확인하는 방법도 있지만 require나 여러 함수들이 contract를 지원받기 때문에 더욱 쉽게 스마트 캐스팅을 할 수도 있습니다. 이밖에도 :?(Elvis 연산자)를 이용하여 null일 경우 평범하게 default 값을 넣어줄 수도 있지만 return을 하거나 throw를 하여 null 일경우 여러 방식으로 구현을 할 수도 있습니다. 또한 여러 함수로 null 대신 다른 값을 받게 할 수도 있습니다. 예를 들어 Collection이 null 일경우 빈 컬렉션을 반환하게 만들 수 있는 orEmpty() 함수를 이용하여 null대신 빈 컬렉션을 넣어줄 수도 있습니다.
이처럼 코드들을 안전하게 처리하게 처리하는 방어적 프로그래밍을 할 수도있지만 모든 상황을 안전하게 해결한다는 것은 사실상 불가능에 가깝습니다. 그렇다면 우리는 공격적 프로그래밍을 할수도 있습니다. 예상치 못한 문제가 발생했을 때 require, check, assert같이 조건을 만족하지 않을 경우 예외를 throw 하는 함수들로 이러한 문제를 개발자에게 알려서 수정하게 만드는 방법입니다.
오류 throw 하기
위에서 말해던 공격적 프로그래밍에 관한 내용입니다. 결과적으로 오류가 생긴다는 것은 그 부분의 어디가 이상이 있어 고쳐야 한다는 말입니다 그래서 throw,!!, requireNotNull, checkNotNull 등으로 이상이 생길 수 있는 구간에 오류를 발생시킵니다.
not-null assertion (!!)과 관련된 문제
!! 를 사용하면 해당값이 null 이 아니라는 것을 보증하게 됩니다. 그렇지 그 값이 null이라면 NPE가 발생합니다. 이건 꽤 큰일입니다.!! 는 사용하기 쉽긴 하지만 설명도 없는 제네릭 예외가 발생하기에 이예외가 왜 생긴 것인지 잘 알려주지 않습니다. 그렇다면 널리 정말로 아닐 때만 쓰면 되지 않느냐라고 할 수도 있지만 이게 시간이 지나고 나서도 null이 아니라고 확신할 수는 없습니다.
fun largestOf(vararg nums: Int): Int = nums.max()!!
예를 들어 이런 함수가 있다고 생각해 봅시다. 이경우 nums에 아무것도 넣어주지 않게 될 겨우 NPE가 생깁니다. 또한 위와 같은 형식처럼 nullability(null일 수 있는지)와 관련된 정보는 숨겨지게 되므로 놓칠 수 있습니다. 이처럼!! 연산자를 쓰거나 명시적으로 예외를 발생시키는 형태로 설계하면 미래 어느 시점에 해당 코드가 오류를 발생시킬 수 있다는 걸 염두에 둬야 합니다.. 예외는 예상하지 못한 잘못된 부분을 알려주기 위해 쓰는 것입니다. 그렇기에 명시적 오류는 제네릭 NPE보단 더 많은 정보를 제공해 줄 수 있어서!! 연산자를 쓰는 것보단 훨씬 좋습니다. 결과적으로는 이러한 문제들 때문에!! 의 사용을 피해야 합니다.
의미 없는 nullability 피하기
여러 코딩테스트를 하면서 nullable 한 변수를 여러 번 사용하고 싶었던 적은 있었지만 결과적으로는 해당 작업을 수행하지는 않았습니다. 그 이유는 nullable을 사용하는 것만으로도 일반변수에 비해서 추가 비용이 들기 때문입니다. 그렇기에 정말로 nullability가 필요하지 않다면 사용하지 않는 것이 좋습니다. nullability를 피하는 방법은 여러 가지가 있습니다.
- 클래스에서 nullability에 따라 여러 함수를 만들어 제공할 수도 있습니다. ex) get, getOrNull()
- 어떤 값이 클래스 생성 이후에 확실하게 설정된단 보장이 있으면 lateinit 프로퍼티와 notNull 델리게이트를 쓰는 것이 좋습니다.
- null 대신에 빈 컬렉션으로 리턴하십시오.
- nullable enum 대신에 None enum 으로 처리하기
lateinit 프로퍼티와 notNull 델리게이트
lateinit 같은경우 나중에 초기화될 것을 명시하는 키워드입니다. lateinit 또한 비용이 발생합니다. 만약 초기화 전에 값을 사용하려고 하면 예외가 발생합니다. 그렇지만 이렇경우 공격적 프로그래밍으로 해당 부분만 수정하기 때문에 그렇게 나쁜 부분은 아닐 것입니다. lateinit은 nullable과 비교해서 아래와 같은 차이가 있습니다.
- !! 연산자로 언팩 하지 않아도 됩니다
- 이후 어떤 의미를 나타내기 위해 null을 쓰고 싶을 때 nullable로 만들 수도 있습니다
- 프로퍼티 초기화 이후에는 초기화되지 않은 상태로 돌아갈 수 없습니다
lateinit : 프로퍼티를 처음 쓰기 전에 반드시 초기화될 거라고 예상되는 상황에 활용합니다.(호출에 명확한 순서가 있을 경우)
lateinit을 쓸 수 없는 경우 : JVM에서 Int, Long, Double, Boolean 같은 기본형과 연결된 타입으로 프로퍼티를 초기화해야 하는 경우. 이 경우 lateinit보다 느리지만 Deletages.notNull을 사용합니다.
class DoctorActivity: Activity() {
private var doctorId: Int by Delegates.notNull()
private var fromNotification: Boolean by Delegates.notNull()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
doctorId = intent.extras.getInt(DOCTOR_ID_ARG)
fromNotification = intent.extras.getBoolean(FROM_NOTIFICATION_ARG)
}
}
위코드는 Deletages.notNull을 사용하는 예시입니다.
class DoctorActivity: Activity() {
private var doctorId: Int by arg(DOCTOR_ID_ARG)
private var fromNotification: Boolean arg(FROM_NOTIFICATION_ARG)
}
onCreate 때 초기화하는 프로퍼티는 지연초기화하는 형태로 아래처럼 프로퍼티 위임을 쓸 수도 있습니다.
'코틀린 > Effective Kotlin' 카테고리의 다른 글
[Effective Kotlin] 10. 안정성 - 단위 테스트(Unit Test)를 만들어라 (0) | 2023.08.02 |
---|---|
[Effective Kotlin] 9. 안정성 - use를 사용하여 리소스를 닫아라 (0) | 2023.08.01 |
[Effective Kotlin] 7. 안정성 - 결과 부족이 발생할 경우 null과 Failure를 사용하라 (0) | 2023.07.24 |
[Effective Kotlin] 6. 안정성 - 사용자 정의 오류보다는 표준 오류를 사용 하라 (0) | 2023.07.21 |
[Effective Kotlin] 5. 안정성 - 예외를 활용해 코드에 제한을 걸어라 (0) | 2023.07.19 |