[Kotlin] Scope Functions : let, run, with, apply, also

Kotlin에는 범위 지정 함수 5가지가 있다. let, run, with, apply, also에 대해서 알아보자. 사용 목적에 따라 범위 함수를 선택하고 본질적으로 유사한 각각의 범위 함수간의 차이점을 알아보자.
Apr 19, 2024
[Kotlin] Scope Functions : let, run, with, apply, also

범위 함수(Scope Functions)

Kotlin의 범위 함수는 객체를 일시적으로 특정 범위(scope) 안에서 사용하도록 도와주는 함수들이다. 이 범위 안에서 객체를 이름 없이 접근할 수 있다.

범위 함수는 새로운 기술적 기능을 도입하지는 않지만 코드를 더 간결하고 가독성 있게 만들 수 있다. 범위 함수의 선택은 주로 프로젝트의 의도와 사용의 일관성에 따라 달라진다.

범위 함수의 선택

  • let : 널이 아닌 객체에서 람다 실행 (null이 아닌 경우에만 실행하고 싶을 때)

  • let : 로컬 범위에서 표현식을 변수로 도입 (블록 내에서 임시 변수를 만들고 싶을 때)

  • apply : 객체 구성 (객체를 초기화하거나 구성할 때)

  • run : 객체 구성 및 결과 계산 (객체를 초기화하면서 결과 값을 계산할 때)

  • non-extensionrun : 표현식이 필요한 경우 실행문 (범위를 형성하지 않고 코드 블록을 실행해야 할 때)

  • also : 추가 효과 (주로 객체를 반환하면서 로그를 남기거나 디버깅할 때)

  • with : 객체에 대한 함수 호출 그룹화 (같은 객체에 대해 여러 함수를 호출해야 할 때)

범위 함수는 코드를 더 간결하게 만들 수 있지만, 남용하지 않도록 주의해야 한다. 남용하면 코드가 읽기 어려워지고 오류가 발생할 수 있다. 또한, 범위 함수를 중첩해서 사용하지 않도록 하고, 체이닝할 때도 주의해야 한다. 현재 컨텍스트 객체와 thisit의 값이 무엇인지 혼동하기 쉽기 때문이다.

범위 함수의 2가지 주요 차이점

1. 컨텍스트 객체를 참조하는 방식 (this or it)

2. 반환 값

범위 함수에 전달된 람다 내에서 컨텍스트 객체는 실제 이름 대신 짧은 참조로 사용할 수 있다. 각 범위 함수는 람다 수신자(this) 또는 람다 인수(it)로 컨텍스트 객체를 참조하는 두 가지 방법 중 하나를 사용한다. 둘 다 동일한 기능을 제공한다.

Context object: this or it

run, with, apply는 컨텍스트 객체를 람다 수신자(receiver)로 참조하며, 키워드 this를 사용한다. 이 함수들의 람다 블록 내에서는 객체가 일반 클래스 함수처럼 사용 할 수 있다.

대부분의 경우, 수신 객체의 멤버에 접근할 때 this를 생략할 수 있어서 코드가 더 짧아진다. 그러나 this를 생략하면 수신 객체의 멤버와 외부 객체 또는 함수 간의 구분이 어려울 수 있다.

따라서 객체의 함수 호출이나 속성에 값을 할당하는 등 주로 객체의 멤버를 조작하는 람다에서는 this를 수신 객체로 사용하는 것이 권장됩니다.

val adam = Person("Adam").apply { 
    age = 20             // same as this.age = 20
    city = "London"
}
println(adam)

// Person(name=Adam, age=20, city=London)

두 가지 범위 함수(let, also)는 람다 블록 안에서 컨텍스트 객체를 it이라는 이름으로 참조합니다. it은 기본적으로 제공되는 이름으로, 이를 통해 객체에 접근할 수 있습니다.

val name = "Kotlin"

name.let {
    println(it)  // it은 "Kotlin"을 가리킴
    it.length  // it의 길이 반환
}

위 코드에서 itname 객체를 가리킨다. it을 사용하면 객체의 속성과 메서드에 접근할 수 있다. itthis보다 짧아서 코드가 더 간결하고 표현이 간단해져서 읽기 쉽다.

주의할 점은 암시적으로 접근할 수 없다는 점이다. 객체의 메서드나 속성을 호출할 때 this처럼 암시적으로 접근할 수 없어서, 항상 it을 사용해야 한다.

그렇다면 언제 it을 사용하는 것이 적합할까?

  • 함수 인자로 주로 사용될 때: 객체가 함수의 인자로 주로 사용될 때 it이 더 적합하다.

  • 여러 변수를 사용할 때: 코드 블록에서 여러 변수를 사용할 때 it이 더 명확하다.

val age = 25

person.let {
    println(it.name)  // it은 person을 가리킴
    println(age)  // 외부의 age를 가리킴
}

이렇게 하면 it과 외부 변수를 명확하게 구분할 수 있다.

반환 값 (Return value)

범위 함수들은 반환 값에 따라 다르다.

  • applyalso는 컨텍스트 객체를 반환한다.

  • let, run, with는 람다의 결과를 반환한다.

let

val numbers = mutableListOf("one", "two", "three", "four", "five")
val resultList = numbers.map { it.length }.filter { it > 3 }
println(resultList)

let을 사용하여 위의 예제를 다시 작성하면 다음과 같다.

val numbers = mutableListOf("one", "two", "three", "four", "five")
numbers.map { it.length }.filter { it > 3 }.let { 
    println(it)
    // and more function calls if needed
} 

list의 연산 결과를 변수에 재할당하지 않을 수 있다.

let에 전달된 코드 블록이 it을 인자로 받는 단일 함수인 경우, 람다 대신 메서드 참조(::)를 사용할 수 있다.

val numbers = mutableListOf("one", "two", "three", "four", "five")
numbers.map { it.length }.filter { it > 3 }.let(::println)

let은 널이 아닌 값이 포함된 코드 블록을 실행하는 데 자주 사용된다. 널이 아닌 객체에 대해 작업을 수행하려면 해당 객체에 안전 호출 연산자 ?. 를 사용하고 해당 람다에 있는 작업으로 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 ull inside '?.let { }'
    it.length
}

// let() called on Hello

run

with

apply

also

Share article

code-with-me