본문으로 바로가기
반응형

이 글은 Kotlin:Don't just use LET for null check 블로그를 기반으로 번역한 글입니다.

https://medium.com/@elye.project/kotlin-dont-just-use-let-7e91f544e27f



대부분의 모든 사람들이 kotlin에서 let 이라는 함수를 이용하여 null safe 처리를 할 수 있다는걸 알고 있습니다.

null safety 문서에서 아래와 소개있으니 말이죠.

val listWithNulls: List = listOf(“Kotlin”, null)
for (item in listWithNulls) {
 item?.let { println(it) } // prints Kotlin and ignores null
}

따라서 let은 필연적으로 "아~ 이거 if (variable != null) 을 대신할수 있는 코드구나~" 라고 이해하게 됩니다.


// Conventional approach
if (variable != null) { /*Do something*/ }

// Seemingly Kotlin approach
variable?.let { /*Do something*/ }


더군다나 위와같은 설명을 본다면야 더 그렇죠.

하지만! 이러한 코드들이 허용될 수는 있으나, 언제나 올바른 let을 사용법은 아닙니다.

1. When not to use LET

1-1. 불변 변수의 null check (When just checking null for an immutable variable, don't use LET)
// NOT RECOMMENDED
fun process(str: String?) {
    str?.let { /*Do something*/   }
}

위 함수는 nullable인 string을 인자로 받아 내부에서 null이 아니면~ 이라는 형태로 처리하는 함수 입니다.

"let을 제대로 사용하고 있어!!" 라고 생각할 수 있으나, 이를 자바로 디컴파일해 보면 아래와 같은 형태가 됩니다.

public final void process(@Nullable String str) {
   if (str != null) {
      boolean var4 = false;
      /*Do something*/
   }
}

네..그냥 if null check한거랑 같습니다.

하지만 오히려 쓸데없는 추가 변수가 늘었습니다. 만약 이 함수가 많이 호출되어 사용될 경우 아무 이득없이 추가적인 작업만 더 하는 코드가 됩니다.

(뭐 라인수는 줄었네요..)


이런경우 차라리 아래와 같이 쓰는게 더 낫습니다.

// RECOMMENDED
fun process(str: String?) {
    if (str != null) {
        // Do Something
    }
}

if문 내부에서는 이미 str이 non null로 auto-cast되기 때문에 추가적인 변수가 들어갈 필요가 없습니다.

따라서 decompile된 코드를 보면 훨씬 직관적입니다.

public final void process(@Nullable String str) {
   if (str != null) {
      /*Do something*/
   }
}


1-2. 특정 변수의 내부 항목에 대한 값을 설정할때 (If you just want to access the checked variable's content, but not the outer scope class' content, don't use LET.)

특정 변수가 null인지 아닌지를 확인하여 내부 변수의 값을 세팅하고자 할때는 let을 사용하는게 바람직 하지 않습니다.

// NOT RECOMMENDED
webviewSetting?.let {
    it.javaScriptEnabled = true
    it.databaseEnabled = true
}


이런경우 아래와같이 run을 쓰는게 더 낫습니다.

// RECOMMENDED
webviewSetting?.run {
    javaScriptEnabled = true
    databaseEnabled = true
}

run을 사용할 경우 내부 scope은 this의 성질을 같습니다.

따라서 군더더기 같은 it을 걷어낼 수 있습니다.


1-3. 변수에 let을 사용하고 나서 그 변수값을 그대로 chaining을 해야 하는 경우 (If you need to chain the original variable content after the scope, don't use LET)

예를들어 nullable한 string list의 사이즈를 출력한 후에 전체 구성요소를 print 한다면 아래와 같이 쓸수 있습니다.

// NOT RECOMMENDED
stringList?.let {
    println("Total Count: ${it.size}")
    it
}?.forEach{ println(it) }


일단 print를 한 이후에 original variable을 다시 반환하기 위해 it을 사용했습니다.

let은 scope의 마지막 값이 return되기 때문인데, 이런경우 also를 사용하면 심플해 집니다.

(also는 반환값이 마지막 값이 아닌 자기 자신입니다.)

// THIS IS BETTER THAN PREVIOUS
stringList?.also {
    println("Total Count: ${it.size}")
}?.forEach{ println(it) }


주석에 RECOMMEND가 달리지 않은 까닭은 여러개의 ?이 사용되기 때문입니다.

이를 제거하기 위한 방법은 좀더 아래에서 다룹니다.


2. When to use LET

let을 쓰면 더 비효율적인 상황들을 알아 봤으니, 이제 언제 let을 쓰면 좋은지를 알아보겠습니다.

2-1. Mutable 변수의 null을 체크할 경우 Let을 사용하면 scope 내부에서 immutable을 보장해 줍니다. (If you checking for the null status of a mutable variable, use LET to ensure checked value is immutable.)

만약 mutable한 전역 변수에 대한 null check을 사용한다면 아래와 같이 사용하면 됩니다.

// RECOMMENDED
private var str: String? = null

fun process() {
    str?.let { /*Do something*/ }
}


"응?? 1.1하고 뭐가 달라?" 라고 할수 있으나, 실제 이걸 if문을 사용해서 null check을 하기 위해서는 아래과 같이 해야 합니다.

// NOT RECOMMENDED
private var str: String? = null

fun process() {
    if (str != null) {
        println(str?.length)
    }
}


if 문으로 null check을 했으나 if문 안에서도 ?를 써서 변수에 접근해야 합니다.

이는 str이 mutable한 값이기 때문에 누군가가 접근해서 값을 바꿔버릴수 있기 때문입니다. (str에 ? 없이 접근하면 IDE가 친철하게 에러를 띄워 줍니다.  compile 에러가 뜨는거죠.)


하지만 간혹 null일때 는 다른 코드를 수행해야 할때가 있습니다.

그럼 전 아래와 같이 작성해서 사용합니다.

private var str: String? = null

fun process() {
    val immutableStr = str
    if (immutableStr != null) {
        println(immutableStr.length)
    } else {
        println("str is null!!!")
    }
}


아쉽지만..이렇게라도 써야죠..

2.2 scope 내부에서 외부 scope의 값을 적용하려고 할때는 let을 사용하여 명시적으로 this와 구분할 수 있습니다.(If you just want to access the outher scope's content, use LET to distinguish 'it' and 'this' easier.)


제목만으로는 무슨말인지 가늠이 가질 않으시죠?

예제로 확인해 보겠습니다.

// ERROR
var javaScriptEnabled = false
var databaseEnabled = false

webviewSetting?.run {
    javaScriptEnabled = javaScriptEnabled
    databaseEnabled = databaseEnabled
}


위 코드는 에러 코드 입니다.

webviewSetting의 내부 변수와 외부 변수의 이름이 같기 때문에 컴파일러가 혼동할수밖에 없습니다.

대안으로는 변수이름을 살짝~ 바꿔줄수 있습니다.

이름은 비슷하지만 혼돈을 막기 위해 IDE가 두 변수의 색깔을 달리 표현해 줍니다.

하지만 리뷰하는 사람은 헷깔리겠죠..(보통 IDE로 리뷰하지는 않으니..)


따라서 이럴때는 run 보다는 let을 써서 외부 변수와 내부변수를 명확하게 구분해 주는게 좋습니다.

// RECOMMENDED
var javaScriptEnabled = false
var databaseEnabled = false

webviewSetting?.let {
    javaScriptEnabled = it.javaScriptEnabled
    databaseEnabled = it.databaseEnabled
}


만약에 run을 쓰고 내부에서 this까지 쓴다면 정말 혼돈의 상태가 도래 합니다.

// NOT RECOMMENDED
var javaScriptEnabled = false
var databaseEnabled = false

webviewSetting?.run {
    javaScriptEnabled = this.javaScriptEnabled
    databaseEnabled = this.databaseEnabled
}


this가 누구의 this인지...


2-3 긴 nullable chain을 사용할때 (When you have a long nullable chain with the front, use LET to eliminate the extra null '?' check.)

긴 nullable chain을 사용할 경우 ?를 계속 찍고 가는것 보다는 let으로 시작부분을 체크해주는게 훨씬 심플해 집니다.

// NOT RECOMMENDED
fun process(string: String?): List? {
    return string?.asIterable()?.distinct()?.sorted()
}


쓰기에는 간단하지만 이 코드를 decompile하면 아래와 같이 나옵니다.

@Nullable
public final List process(@Nullable String string) {
   List var2;
   if (string != null) {
      Iterable var10000 = StringsKt.asIterable(
                           (CharSequence)string);
      if (var10000 != null) {
         var2 = CollectionsKt.distinct(var10000);
         if (var2 != null) {
            var2 = CollectionsKt.sorted((Iterable)var2);
            return var2;
         }
      }
   }

   var2 = null;
   return var2;
}


계속 내부로 들어가면서 if null check을 하는걸 볼수 있는데, 이는 복잡도가 너무 올라갑니다.

따라서 실제 nullable한 부분을 let을 끊어내면, decomplie시점에 좀더 심플한 코드로 변환 됩니다.

// RECOMMENDED
fun process(string: String?): List? {
    return string?.let {
        it.asIterable().distinct().sorted()
    }
}


Decompile한 코드
@Nullable
public final List process(@Nullable String string) {
   List var10000;
   if (string != null) {
      int var4 = false;
      var10000 = CollectionsKt.sorted(
                   (Iterable)CollectionsKt.distinct(
                      StringsKt.asIterable((CharSequence)string)));
   } else {
      var10000 = null;
   }

   return var10000;
}


2-4 scope의 마지막 값을 사용하고자 할때 (When you need to access the result, use LET to return the final result in the scope.)

immutable 변수가 있을경우 1-1에서 let보다는 if문으로 null check하는게 더 낫다고 언급했습니다.


다만 아래 예제를 보시죠.

// NOT RECOMMENDED
fun process(stringList: List?, removeString: String): Int? {
    var count: Int? = null

    if (stringList != null) {
        count = stringList.filterNot{ it == removeString }
                        .map { it.length }
                        .sum()
    }
    
    return count
}


실제로 가지고 가고 싶은 값은 sum() 입니다.

하지만 if문을 사용함에 따라 불필요한 count라는 함수가 선언됐습니다.

이럴경우 차라리 let을 쓰는게 훨신더 간결하고 불필요한 변수 추가를 막을 수 있습니다.

// RECOMMENDED
fun process(stringList: List?, removeString: String): Int? {
    return stringList?.let {
        it.filterNot{ it == removeString }.map { it.length }.sum()
    }
}



글을 읽어보면 사실 법칙1, 법칙2 처럼 명확하게 뭐는 맞고 뭐는 틀리다인것 같지는 않습니다.

컴파일이 어떻게 된다라는걸 인지하고 let을 사용한다면 좀더 효율적인 코드를 만들수 있지 않을까 싶네요.


반응형