Logo

Kotlin의 Scope 함수 정리

Kotlin의 Scope 함수는 객체의 컨텍스트 내에서 코드 블록을 실행할 수 있게 해주는 강력한 기능입니다.

Scope 함수에는 let(), run(), also(), apply(), with() 이렇게 5개가 있는데요. 얘네들이 비슷한 것 같으면서도 미묘하게 달려서 잘 정리해두지 않으면 언제 어떤 녀석을 써야 하는지 은근히 헷갈릴 수 있습니다.

이번 포스팅에서는 Kotlin의 Scope 함수 5종의 사용법을 정리해드리도록 하겠습니다.

예제 클래스

다음과 같이 간단한 클래스를 가지고 간단한 예제 코드를 작성하면서 각 Scope 함수에 대해서 설명을 드리려고 합니다.

data class Fruit(var name: String = "", var price: Int? = null)

대상 객체를 그대로 반환하는 Scope 함수

also() 함수와 apply() 함수는 대상 객체를 그대로 반환하는 Scope 함수입니다. 하지만 임시 범위 내에서 대상 객체를 어떻게 접근하느냐(it vs. this)에서 차이를 보입니다.

also() 함수

어떤 객체를 상대로 also() 함수를 호출하면 해당 객체를 가지고 부수적인 작업을 할 수 있습니다.

Fruit("사과", 100).also {
    println("시작") // 시작
    println(it) // Fruit(name=사과, price=100)
    println("종료") // 종료
}

위와 같이 also() 함수는 특히 디버깅하면서 콘솔에 어떤 객체를 잠깐 출력해볼 때 유용합니다.

만약에 also() 함수가 없었다면 과일 객체를 콘솔에 출력하기 위해서 임시 변수에 저장을 해놨어야 했을 것입니다.

val fruit = Fruit("사과", 100)
println("시작") // 시작
println(fruit) // Fruit(name=사과, price=100)
println("종료") // 종료

객체를 출력하는 코드를 삭제하면 이 변수는 더 이상 사용되지 않기 때문에 변수를 귀찮게 정리해 줬어야겠죠?

val fruit = Fruit("사과", 100) // ⚠️ unused variable

영단어 also의 뜻처럼 대세에 지장을 주지 않는 선에서 사용하는 것이 좋습니다.

apply() 함수

어떤 객체를 상대로 apply() 함수를 호출하면 그 객체의 속성과 함수를 쉽게 호출할 수 있습니다.

val fruit = Fruit()
fruit.apply {
    name = "사과"
    price = 100
}
println(fruit) // Fruit(name=사과, price=100)

apply() 함수의 임시 범위에서는 대상 객체가 this에 할당되어 this.를 생략하고 바로 객체의 속성과 함수에 접근이 가능합니다. 반면에 also() 함수의 임시 범위에는 대상 객체가 람다 함수의 인자로 할당이 되죠.

그러므로 위 코드를 굳이 also() 함수로 재작성하면 속성과 함수 앞에 it.을 붙여줘야 합니다.

val fruit = Fruit()
fruit.also {
    it.name = "사과"
    it.price = 100
}
println(fruit) // Fruit(name=사과, price=100)

영단어 apply의 뜻처럼 임시 범위에서 어떤 객체의 속성을 설정해줄 때 빛을 발휘하는 Scope 함수입니다.

람다 함수의 결과를 반환하는 Scope 함수

let() 함수와 run() 함수, with() 함수 람다 함수의 결과를 반환하는 Scope 함수입니다. let() 함수와 run() 함수는 임시 범위 내에서 대상 객체를 어떻게 접근하느냐(it vs. this)에서 차이를 보입니다. with() 함수는 run() 함수와 동일하게 임시 범위 내에서 this를 통해 대상 객체에 접근하지만 임시 범위를 생성하는 방식이 다릅니다.

let() 함수

어떤 객체를 상대로 let() 함수를 호출하면 해당 객체를 가지고 어떤 작업을 한 결과를 얻을 수 있습니다.

val nameList = Fruit("사과", 100).name.let {
    listOf(it, it, it)
}
println(nameList) // [사과, 사과, 사과]

let() 함수는 특히 ?. 연산자와 함께 깔끔한 널(null) 값 처리를 위해서 쓰는 경우가 많습니다.

val discountedPrice = Fruit("사과", 100).price?.let {
    it * 0.8
}
println(discountedPrice) // 80.0

?.let() 조합을 사용하지 않았다면 코드가 좀 지저분했을 것입니다.

val price = Fruit("사과", 100).price
val discountedPrice = if (price != null) price * 0.8 else null
println(discountedPrice) // 80.0

run() 함수

어떤 객체를 상대로 run() 함수를 호출하면 그 객체의 속성과 함수를 가지고 어떤 작업을 한 결과를 얻을 수 있습니다.

val message = Fruit("사과", 100).run {
    "$name 가격이 $price 원입니다."
}
println(message) // 사과 가격이 100 원입니다.

물론 let() 함수를 통해서도 동일한 목적을 달성할 수 있지만, 대상 객체가 람다 함수의 인자로 넘어오기 때문에 앞에 it.을 붙여줘야 합니다. 위에서 살펴본 apply()also() 차이점과 같죠.

val message = Fruit("사과", 100).let {
    "$it.name 가격이 $it.price 원입니다."
}
println(message) // 사과 가격이 100 원입니다.

참고로 run() 함수는 대상 객체가 없이 단독으로 사용할 수도 있습니다.

run {
    println(Fruit("사과", 100)) // Fruit(name=사과, price=100)
}

with() 함수

지금까지 살펴본 Scope 함수와 다르게 with()는 객체를 상대로 호출하지 않습니다. 대신 객체를 인자로 넘겨서 임시 범위를 만들어 내며 람다 함수의 결과값을 반환합니다.

val message = with(Fruit("사과", 100)) {
    "$name 가격이 $price 원입니다."
}
println(message) // 사과 가격이 100 원입니다.

이 점을 제외하고는 run() 함수와 사용법이 동일합니다.

정리

임시 범위 내에서 대상 객체를 접근하고 싶다면 let()이나 also()를 쓰는 편이 유리합니다. let()은 람다 함수의 결과값을 반환하는 반면에, also()는 대상 객체를 그대로 반환합니다.

임시 범위 내에서 대상 객체의 속성이나 함수에 싶다면 run()이나 apply(), with()를 씁니다. run()은 람다 함수의 결과값을 반환하는 반면에, apply()는 대상 객체를 그대로 반환합니다. with()는 객체를 인자로 넘겨서 임시 범위를 만들어 내며 람다 함수의 결과값을 반환합니다.

마치며

지금까지 객체를 변수에 저장하지 않고 람다 함수를 통해 임시 범위에서 접근하도록 해주는 Kotlin의 Scope 함수에 대해서 알아보았습니다.

5개의 Scope 함수 중에 뭘 써도 상관이 없는 경우도 있지만, 목적에 맞는 Scope 함수를 골라 써야 빛을 발휘할 때가 많습니다.

Scope 함수를 적지적소에 잘 활용하면 더욱 간결하고 표현력있게 작성할 수 있지만, 오납용하면 오히려 역효과 날 수도 있어서 주의해야합니다. 각 Scope 함수의 특성을 잘 이해하고 상황에 맞게 사용하는 것이 무엇보다 중요하겠습니다.