라떼는말이야

[Kotlin] 코틀린 스코프 함수: run, let, apply, also, with 본문

공부

[Kotlin] 코틀린 스코프 함수: run, let, apply, also, with

MangBaam 2022. 1. 26. 03:02
반응형

코틀린에는 Scope 함수라고 하는 개념이 있다.

run, let, apply, also, with 키워드가 있으며 각각 비슷한 역할을 하기 때문에 서로 혼용하여 사용할 수도 있지만 분명히 다른 동작을 수행한다. 처음엔 헷갈리더라도 정확한 사용법과 용도를 안다면 좀 더 적재적소에 활용할 함수를 선택할 수 있을 것이다.

https://kotlinlang.org/docs/scope-functions.html

 

Scope functions | Kotlin

 

kotlinlang.org

나 역시 완벽하게 익히지 못했기 때문에 작성하는 내용이 틀릴 수도 있고, 적절하지 않은 예제일 수도 있다. 정확한 내용을 확인하기 위해서는 위의 공식 문서를 확인하길 바란다.


코틀린 스코프 함수 정리 표

위 사진은 공식 문서에 첨부된 표이다.

각 스코프 함수에서의 참조 방법과 리턴 타입 등에 대해 설명되어 있다.

이미 스코프 함수에 대해 학습한 이후에 보면 무슨 내용인지 알 수 있으나 처음 접한 내용이라면 위 표만 가지고 이해하기에는 무리가 있다고 생각한다. (내가 그랬다...)

 

 

구분 방법

Context object에 따른 구분: this로 받거나 it으로 받거나

인텔리제이에서의 사용

인텔리제이나 안드로이드 스튜디오와 같은 Jetbrain 사의 IDE를 사용하면 위와 같이 힌트를 보여준다. (참고로 Kotlin 언어도 Jetbrain에서 만든 언어이다)

run, apply, with를 보면 this로 list를 받는다. 그리고 letalsoit으로 list를 받는다.

this로 받는 run, apply, with

this로 받는 run, apply, with는 위 코드와 같이 속성에 접근할 때 this를 생략할 수도 있다. (run과 with 안에서도 this.size로 접근할 수 있고, apply 안에서도 this를 생략하고 속성에 접근할 수 있다)

그런데 run과 apply와 다르게 with의 형태는 조금 다르다. run과 apply는 확장 함수 형태인 것에 반해 with는 확장 함수가 아니기 때문에 일반 함수처럼 사용되게 된다. 즉, 대상(위 코드에서 list)이 null일 경우 with 함수보다는 apply나 run 함수를 사용하는 것이 좋다. 

this 키워드를 생략할 수 있지만 외부의 속성과 헷갈릴 가능성이 있기 때문에 this라고 명시를 해주는 것이 좋은 선택이 될 수도 있다.

 

it으로 받는 let, also

it으로 받는 let과 also의 경우 this와 다르게 it을 생략할 수 없다. 하지만 also의 예제처럼 다른 이름으로 변경할 수 있다. 따로 이름을 지정하지 않는다면 it이라는 이름으로 사용할 수 있다.

 

반환 값(Return value)에 따른 구분: this를 반환하거나 마지막 실행 코드를 반환하거나

Context object에 따른 구분을 보면 왜 똑같이 this를 사용하는 함수와 it을 사용하는 함수를 여러 개 만들어놨을까 싶을 테지만 각각의 반환 값이 다르기 때문에 각자의 역할이 구분되게 된다.

 

호출 대상인 this 자체를 반환: apply, also

apply와 also는 자기 자신을 반환한다. (Scope 함수 중에 'a'로 시작하는 것들은 자기 자신을 반환한다고 외우자)

var list = mutableListOf("Scope", "Function")

val afterApply = list.apply {
    add("Apply") // this.add("Apply") 와 동일
    count() // this.count() 와 동일
}
println("반환값 apply = $afterApply")

val afterAlso = list.also {
    it.add("Also")
    it.count()
}
println("반환값 also = $afterAlso")

apply와 also는 자기 자신(위 코드에서는 list)을 반환하기 때문에 afterApply와 afterAlso에는 반환된 list의 값이 들어가게 된다.

count() 함수를 호출했지만 출력하거나 다른 작업을 수행하지 않았기 때문에 사실상 의미 없는 코드이다.

실행해보면 다음과 같이 리스트가 저장된 것을 확인할 수 있다.

 

 

마지막 실행 코드를 반환: let, run, with
var list = mutableListOf("Scope", "Function")

val lastCount = list.let {
    it.add("Run")
    it.count()
}
println("반환값 let = $lastCount")

val lastItem = list.run {
    add("Run")
    get(size-1)
}
println("반환값 run = $lastItem")

val lastItemWith = with(list) {
    add("With")
    get(size-1)
}
println("반환값 with = $lastItemWith")

let, run, with의 경우 자기 자신(위 코드에선 list)이 아닌 마지막 실행 코드를 반환한다.

위 예시에서 let은 list의 크기를, run과 with의 경우 마지막 원소를 출력하는 코드가 반환되었다.

결과를 보면 아래와 같이 출력된다.

 


 

정리

run과 with가 this를 받아서 마지막 코드를 반환하는 공통점이 있고, 나머지 스코프 함수들은 겹치지 않는다.

하지만 run과 with도 차이점이 있다. with는 확장 함수가 아니며, run을 비롯한 나머지 스코프 함수들은 확장 함수라는 점이다. (확장 함수는 list.run { }과 같이 '.'으로 확장되는 형태이다)

 



 

이제 각각의 함수의 활용을 알아보자. 위에서도 말했듯이 비슷한 동작을 하기 때문에 기술적으로는 서로 바꿔서 사용할 수도 있다.

 

let

context object로 it을 사용하고, 반환 값으로 마지막 코드를 반환한다.

let은 중요한 특징이 있다. 바로 null이 아닐 때만 실행한다는 점이다. 그래서 보통 safe call operator (?.)과 함께 사용된다.

만약 str이 null이었다면 let 내부의 과정들은 실행되지 않았을 것이다.

 

자바로 안드로이드를 짜봤던 사람이라면 아래와 같은 코드로 인텐트를 작성했을 것이다.

Intent intent = new Intent()
intent.putExtra("Key", "value")
intent.putExtra("Key1", "value")

코틀린으로 넘어와서도 위 자바 코드처럼 작성하는 경우를 흔히 볼 수 있다.

val intent = Intent()
intent.putExtra("Key", "value")
intent.putExtra("Key1", "value")

하지만 코틀린의 스코프 함수를 잘 활용하면 좀 더 코틀린스럽게 짤 수 있게 된다.

val intent = Intent().let {
    it.putExtra("Key", "value")
    it.putExtra("Key1", "value")
}

 

여기서 주의해야 할 점이 있다. let은 마지막 코드를 반환하기 때문에 아래와 같은 경우엔 Unit이 반환되어 intent의 타입은 Intent가 아니게 된다.

val intent = Intent().let {
    it.putExtra("Key", "value")
    it.putExtra("Key1", "value")
    it.action = "CUSTOM_APP_ACTION" // Unit
}

위 코드를 우리가 원하는대로 intent의 타입이 Intent가 되려면 마지막 코드를 제거하거나 순서를 변경하면 된다.

val intent = Intent().let {
    it.putExtra("Key", "value")
    it.action = "CUSTOM_APP_ACTION"
    it.putExtra("Key1", "value") // Intent
}

 

run

context object로 this를 사용하고, 반환 값으로 마지막 코드를 반환한다.

run도 let과 마찬가지로 null-safe 하기 때문에 safe call operator(?.)와 함께 사용하는 경우가 많다. 특히 let은 it으로 받아서 내부에서도 it을 써줘야 하는 반면, run은 this로 받기 때문에 this를 생략할 수 있어서 좀 더 간결한 코드를 짤 수 있다.

 

apply

context object로 this를 사용하고, 반환 값으로 자기 자신을 반환한다.

위에서 소개한 let의 활용에서 intent.let{ }을 수행할 때 내부 코드의 순서에 주의해야 한다는 점을 언급했다.

하지만 apply를 사용하면 자기 자신을 반환하기 때문에 마지막 코드에 intent 타입이 오지 않아도 된다.

val intent = Intent().apply {
    putExtra("Key", "value")
    putExtra("Key1", "value")
    action = "CUSTOM_APP_ACTION"
}

이와 같이 apply는 주로 속성 등을 할당할 때 사용하게 된다.

 

also

context object로 it을 사용하고, 반환 값으로 자기 자신을 반환한다.

also는 주로 다른 함수나 표현식 등의 결과에 추가적인 작업을 할 때 사용하게 된다.

10.plus(5)의 결과는 15이다. 하지만 이 결과를 가지고 "Price is 30"이라는 값을 formattedPrice에 대입하기 위해 also가 사용된 코드이다.

also는 자기 자신을 반환하기 때문에 결과 값을 가지고 다른 작업을 수행하더라도 반환 값으로 저장된 price 변수에는 자기 자신인 15가 들어가 있게 되는 것이다.

 

with

context object로 this를 사용하고, 반환 값으로 마지막 코드를 반환한다.

with는 null이 아닌 값을 받아야 한다. 나는 주로 안드로이드 개발 시 binding을 떼기 위해 사용한다.

binding.name.text = person.name
binding.age.text = person.age
binding.city.text = person.city
with(binding) {
    name = person.name
    age = person.age
    city = person.city
}

 

만약 프래그먼트에서 binding을 하게 되면 주로 아래와 같이 nullable 한 값으로 만들게 되는데

private var binding: FragmentMainBinding? = null

이 경우 with를 사용하게 되면 null 체크를 하기 위해 모든 곳에 ? 를 붙여야 한다.

with(binding) {
    this?.root?.isVisible = true
}

 

이때는 let이나 run을 사용하면 편하다.

// let 사용
binding?.let {
    it.root.isVisible = true
}

// run 사용
binding?.run {
    root.isVisible = true
}

 


참고 문서

https://kotlinlang.org/docs/scope-functions.html

https://proandroiddev.com/dont-abuse-kotlin-s-scope-functions-6a7480fc3ce9

 

반응형
Comments