(작성자인 저와 같은) kotlin 입문자라면 가장 먼저 마주치게 될 키워드가 바로 "Null Safety"이지 않을까 싶다.
코틀린에선 자바를 포함한 다른 프로그래밍 언어에서 갖고 있는 "NPE(NullPointerException)"를 제거하기 위해 Nullable과 Non-Null 타입으로 프로퍼티를 선언할 수 있게끔 하였다. (그렇다고 완벽하게 모든 case에서 NPE가 제거되는 것은 아니다 _ 해당 케이스는 나중에 따로 알아보자)
그리고 아마도 여러 문서 및 블로그에서 null값을 조작하는 방법으로 let을 사용하는 케이스를 본 적이 있을 것이다. 더하여 let과 Elvis operator(엘비스 연산자)를 함께 사용하는 경우까지 보았을 것이다. 아래와 같이 말이다.
+ with Elvis Operator
?.let { } 형태에서 block 내부엔 non-null 만 들어올 수 있다. 즉, Not Null checking에 사용될 수 있는 이유이다. 그리고 두 번째 이미지에서 볼 수 있듯이 Elvis Operator를 이용하여 앞선 프로퍼티가 null일 경우에 대한 default 값을 지정해 줄 수 있다.
let이란 람다를 활용한 스코프 함수를 사용하여 null을 아주 간편하게 조작한 것을 확인할 수 있다.
아무 문제가 없어보인다. 그렇다면 난 왜 해당 포스팅을 작성하고자 하였을까...
바로 ?.let { } 을 온전히 null checking으로써 대체 가능하게 사용할 수 있는가? 에 대한 의문이 들었기 때문이다. 절대 위의 내용을 "쓰지말자, 잘못되었다"라 말하고자 하는 것이 아니다. 단지 "null checking"의 활용용도로써 체이닝을 통한 let 스코프 함수가 확실히 alternative한 케이스 인가에 대한 의문인 것이다.
흔히 접할 수 있는 예시
// Conventional approach
if (variable != null) { /*Do something*/ }
// Seemingly Kotlin approach
variable?.let { /*Do something*/ }
사실 위의 예시까지는 문제 될 일이 없다고 본다.
아래의 경우는 "Elvis Operator와 run 스코프 함수"를 추가한 case이다.
// Conventional approach
if (variable != null) {
/* Do something */
} else {
/* Do something else */
}
// Seemingly Kotlin approach
variable?.let {
/* Do something */
} ?: run {
/* Do something else */
}
언뜻 보기엔 아무 문제가 없어 보인다.
let 내부 람다를 통해 non-null인 경우에 대한 처리를 할 수 있고 (if문 내부와 통용) 엘비스 연산자와 run 연산자를 통해 variable 변수의 null 일 경우에 대한 처리를 한 것 처럼 보인다(else문 내부와 통용).
자, 그럼 아래의 예시를 보고 판단해 보자.
✔ case1)
fun main(args: Array<String>) {
val test = TestClass()
test.doPrinting()
}
class TestClass {
private var str1: String? = "not-null string"
private var str2: String? = null
fun doPrinting() {
str1?.let {
str2?.let {
println("output 2")
}
} ?: run {
println("output 1")
}
}
}
위의 doPrinting() 함수를 앞서 얘기 나누었던 대로 if ... else 문으로 변환 한다면 아래와 같을 것이다.
✔ case2)
class TestClass {
private var str1: String? = "not-null string"
private var str2: String? = null
fun doPrinting() {
if (str1 != null) {
if (str2 != null) {
println("output 2")
}
} else {
println("output 1")
}
}
}
자, 그럼 결과가 어떻게 될까?
kotlin의 let 스코프 함수와 elvis operator를 사용한 case1과 if ... else로 변환(사실 변환 자체를 잘못한 것이긴 하다, 직관적 변환이니 오해 마세요)한 case2의 결과는 같을까?
정답은 "그렇지 않다" 이다.
case1) output 1 출력
case2) 아무것도 출력되지 않는다.
Elvis operator(?:)는 left side의 모든 실행에 의존하게 된다. str1과 str2가 null인지 아닌지의 체킹도 중요하지만 해당 변수에서 체이닝 된 람다 내부 식 "자체"의 null 여부에도 의존하게 된다. (`?.`를 통해 체이닝된 식을 계속 매핑해 나간다고 생각하면 된다) 즉, case1도 단순 null 체킹용도로 생각하면 아무것도 출력이 되지 않을 것 같지만 elvis operator 이후의 식이 출력되는 이유도 이 때문이다.
if ... else 문은 온전한 binary다. 둘 중 하나(if 내부이거나 else 내부이거나)이다. 하지만 Elvis Operator를 사용하면 "둘 다(both)"일 수 있단 것이다.
data?.updateData(data) ?: run { showLoadingSpinner() }
위의 예시를 통해 조금 더 와닿게 말하자면, data가 null인지 아닌지 여부를 통해 있을 경우 updateData()를, 없을 경우 showLoadingSpinner()를 실행하는 위의 구문은 의도와 맞지 않다는 것이다. updateData()와 showLoadingSpinner() 모두를 중복 실행시키게 될 것이기 때문이다.
이렇게 let을 통한 null-checking이 완벽하게 모든 케이스에 적합하진 않다는 것을 느껴보았다. 물론 어쩌면 당연한 사실 일 수도 있지만 충분히 입문자 입장에선 오해의 소지를 불러올 수 있는 키워드라 생각이 든다. let은 자기 자신을 받아서 리턴하는 람다 스코프 함수로써 객체의 상태를 조작할 수 있다는 점에서 좋은 연산자이다. 하지만 이것이 정말 완전한, 대체 가능한 null-checking이 될 수 있는지는 의문이다. (특히 위와 같이 여러번의 변수 체이닝과 elvis operator를 결합한 경우의 얘기이다.)
현재 본인의 코드를 파악하고, 순수한 null checking이 더 적합한지 혹은 충분히 의도한 대로 스코프 함수를 두어도 문제 없는지를 등을 판단하는 것이 중요할 것 같다.
'Kotlin' 카테고리의 다른 글
[Kotlin] Sequence와 Lazy evaluation (1) | 2023.12.27 |
---|