티스토리 뷰

Kotlin을 써온지 이제 1년 조금 넘은 시점에서 다시 코드를 돌아보고 있자니 Scope function을 많이 활용을 하지만 적절하게 활용하고 있다는 생각이 들지 않았다. 모호한 기준으로 Scope function을 활용하고 있다고 느꼈다. 이번 Kotlin의 Scope function은 Context object와 Return value가 아닌 각 Scope function이 어디에 쓰이는지 위주로 짚어볼 예정이다.

더 자세히 들어감에 앞서서 간단한 사용 가이드는 다음과 같다.

  • let (1) : Non-Nullable 객체에서 람다 함수를 수행할 때
  • let (2) : 식을 로컬범위의 변수로 사용할 때 (오역 가능성 높음)
  • apply : 객체 구성할 때
  • run (extension) : 객체 구성과 결과값을 계산할 때
  • run (non-extension) : expression(식)이 필요한 statement(문)을 동작시킬 때
  • also : 추가로 effect가 필요할 때
  • with : 객체의 함수 호출을 그룹핑할 때

코틀린의 Scope Function에 대해서 빠삭하다면 위 서술로도 충분히 파악이 가능하리라 생각이 들지만 나는 그렇지 않기에 아래에 공식문서의 예시와 함께 설명해보려고 한다.

1. let : Non-Nullable 객체에서 람다 함수를 수행할 때

코틀린은 Null-Safety 언어로 Non-Nullable과 Nullabe 변수를 구분하여 사용하면서 Null-Exception 오류를 방지하고 있다. 그리고 Nullable 변수가 Null일 때 해당 객체를 사용하지 않고자 ? 연산자를 활용하고 있는데 이 때, let과 같이 활용하면 코드를 줄일 수 있다.

val str: String? = "Hello"   
//processNonNullString(str)       // compilation error: str can be null
val length = str?.let { 
    println("let() called on $it")        
    processNonNullString(it)      // OK: 'it' is not null inside '?.let { }'
    it.length
}

?.let { } 을 활용하면 코드 블록 내부에서는 str을 Non-Nullable 객체로 활용할 수 있게 된다.

2. let : 식을 로컬범위의 변수로 사용할 때

Introducing an expression as a variable in local scope 을 위와 같이 ‘식을 로컬 범위의 변수로 사용할 때’ 라고 번역하였는데 만약 틀렸다면 댓글을 달아주기 바랍니다.

코딩을 하다보면 expression(식)이 길어지게 마련이다. 해당 식으로 도출된 결과값을 여러 함수에서 활용해야할 때 let을 활용해도 좋다.

val numbers = listOf("one", "two", "three", "four")
val modifiedFirstItem = numbers.first().let { firstItem ->
    println("The first item of the list is '$firstItem'")
    if (firstItem.length >= 5) firstItem else "!" + firstItem + "!"
}.uppercase()
println("First item after modifications: '$modifiedFirstItem'")

위 코드의 2번째 줄에 있는 식(numbers.first())을 총 3군데에서 활용하는데 따로 변수로 빼놓는게 아니라 위와 같이 하나의 코드 블록에서 활용하고 결과값을 반환해서 활용할 수 있다.

3. apply : 객체를 구성할 때

기존에 활용하던 Scope function 중에서 가장 제대로 활용하고 있던 함수인 것 같다. 생성한 객체값의 필드를 바로 수정해야 할 때 활용하였다.

val adam = Person("Adam").apply {
    age = 32
    city = "London"        
}
println(adam)

4. run (extension) : 객체 구성과 결과값을 계산할 때

apply와는 반대로 공식문서에서 적어놓은 것과 가장 어긋나게 활용하고 있던 함수이다. 공식문서를 번역하자면 객체 구성과 결과값을 반환해야할 때 잘 활용된다고 한다.

val service = MultiportService("<https://example.kotlinlang.org>", 80)

val result = service.run {
    port = 8080
    query(prepareRequest() + " to port $port")
}

5. run (non-extension) : expression(식)이 필요한 statement(문)을 동작시킬 때

run은 비확장함수로 쓰일 때도 있다. 이 경우에는 여러 식이 필요한 문을 동작시킬 때 활용된다.

val hexNumberRegex = run {
    val digits = "0-9"
    val hexDigits = "A-Fa-f"
    val sign = "+-"

    Regex("[$sign]?[$digits$hexDigits]+")
}

for (match in hexNumberRegex.findAll("+123 -FFFF !%*& 88 XYZ")) {
    println(match.value)
}

6. also : 추가로 effect가 필요할 때

  1. 문맥 객체(it)를 인자로 취하는 작업에 활용하면 좋다.
  2. 문맥 객체의 프로퍼티나 함수가 아닌 그 자체의 참조가 필요한 작업에 좋다.
  3. 이 참조를 외부 Scope로 부터 감쳐놓지 않고 싶을 때 좋다.
val numbers = mutableListOf("one", "two", "three")
numbers
    .also { println("The list elements before adding new one: $it") }
    .add("four")

7. with : 객체의 함수 호출을 그룹핑할 때

람다 결과값의 반환이 필요없고 문맥 객체(this)의 함수를 호출하고 싶을 때 with를 활용하면 좋다.

val numbers = mutableListOf("one", "two", "three")
with(numbers) {
    println("'with' is called with argument $this")
    println("It contains $size elements")
}

요약

  • 이제 각 Scope function들을 어떻게 활용해야 될지 감이 어느 정도 잡혔다.
  1. Nullable 체크하고 싶을 때 → let
  2. 길다란 식을 로컬변수로 활용하고 싶을 때 → let
  3. 객체를 구성할 때 → apply
  4. 구성한 객체를 바로 활용한 결과값을 얻을 때 → run
  5. 여러 식을 활용한 문을 결과값으로 쓸 때 → run (non-extension)
  6. 추가적인 효과를 내야할 때 → also (나는 로그 찍을 때 활용할 것 같다.)
  7. 한 객체의 함수를 여럿 써야할 때 → with

쓰다보니 처음에 썼던 사용 가이드와 동일하게 적은 것 같지만 수미상관 식으로 되새기듯이 끝내도 좋을 것 같다. 비슷한 효과를 가지고 있는 Scope function이기에 그 활용도를 개발자가 명확하게 인지하고 있어야한다. 일관되게 쓰도록 하자!!

참조 문헌

https://kotlinlang.org/docs/scope-functions.html#function-selectionScope function 제대로 쓰자!

 

반응형
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/12   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
글 보관함