Kotlin의 scope functions는 쉽게 말해서 객체의 context(범위) 내에서 코드를 더 간결하고 명확하게 작성할 수 있도록 도와준다.
이 함수들은 람다 표현식을 받아 객체에서 호출되면 임시 스코프를 형성하며, 이 스코프 안에서는 객체의 이름을 명시하지 않고도 접근할 수 있다.
let, run, with, apply, also 이렇게 5가지 scope functions이 있고, 각 함수는 객체를 다루는 방식에 약간의 차이가 있다.
임시 스코프란?
scope function이 객체와 함께 생성한 짧은 컨텍스트로, 이 안에서는 객체를 참조하기 위한 명시적 이름이 필요하지 않은 상태를 말한다.
이해를 돕기 위한 예시 코드user.run { println(name) // 'user'를 명시하지 않고도 'name'에 접근 가능 println(age) }
run블록 안에서는 임시 스코프가 형성되어, 마치user객체가 바로 그 자리에서 사용되는 것처럼 작동한다. 따로user.name이 아니라 그냥name으로 접근할 수 있는 것.

이 표는 스코프 함수의 공통점과 차이점을 요약한 것 인데. 정리하자면,
공통점
차이점
let, also는 it 키워드로, 나머지는 this 키워드를 사용해 객체에 접근apply, also는 객체 자신을 반환하고, 나머지는 람다 표현식의 결과를 반환let, run(this), apply, also는 확장함수로 사용되지만(객체에서 바로 호출 가능)with, run(매개변수 없이 호출)은 확장함수가 아니다.(with은 객체를 인자로 받고 run은 인자 없이 호출)스코프 함수의 축약적인 내용은 살펴봤고, 이제 각 스코프 함수가 어느 경우에 사용하면 좋은지, 예시 케이스와 설명을 기술해보겠다.
let은 객체를 인자로 받아 해당 객체에 대한 여러 작업을 수행하고, 람다의 결과값을 반환한다.
주로 체이닝된 함수 호출 결과를 처리하거나, nullable 객체에 대한 안전한 처리를 위해 사용된다.
1. 체이닝 된 결과 처리
val numbers = listOf("one", "two", "three", "four", "five")
numbers.map { it.length }.filter { it > 3 }.let {
println(it) // [5, 5, 4]
}
컬렉션에서 길이가 3보다 큰 항목을 필터링 후 출력
체이닝(Chaining)
여러 메서드나 함수 호출을 연속적으로 이어서 호출하는 방식이다.
2. Method Reference
numbers.map { it.length }.filter { it > 3 }.let(::println)
단일 함수를 호출하는 경우, 람다 대신 메서드 참조를 사용할 수 있다.
3. null-safe 연산
val str: String? = "Hello"
str?.let {
println("let() called on $it")
it.length // 'it'이 null이 아님을 보장
}
let은 nullable 객체에 대해 안전하게 작업을 수행할 때 빈번히 사용된다.
?.let 구문을 통해 객체가 null이 아닐 때만 작업을 진행할 수 있다.
4. 지역 변수 도입
val numbers = listOf("one", "two", "three", "four")
val modifiedFirstItem = numbers.first().let { firstItem ->
if (firstItem.length >= 5) firstItem else "!$firstItem!"
}.uppercase()
println(modifiedFirstItem) // '!ONE!'
특정 블록 내에서만 사용할 임시 변수 도입
with은 확장함수가 아닌 함수로, 객체를 인자로 받아 그 객체에 여러 작업을 수행할 수 있는 함수다.
람다 블록 안에서 객체는 this로 참조되며, 람다 블록의 결과값이 반환된다.
주로 객체를 변경하거나 메서드를 호출하는데 많이 사용되며, 반환값이 필요 없을 때 권장 된다.
1. 객체에 대해 작업 수행
val numbers = mutableListOf("one", "two", "three")
with(numbers) {
println("'with' is called with argument $this")
println("It contains $size elements")
}
특정 객체에 대해 여러 작업을 수행할 때 간편하게 사용 가능하다.
val numbers = mutableListOf("one", "two", "three")
val firstAndLast = with(numbers) {
"The first element is ${first()}, the last element is ${last()}"
}
println(firstAndLast)
위 코드는 number 리스트의 첫 번째와 마지막 요소를 출력하는 문자열을 만드는 예시로, with을 통해 numbers 객체에 대해 연속적인 메서드 호출을 간단히 처리 가능하다.
run은 객체를 확장함수로 호출하여 특정 작업을 수행하고, 그 결과값을 반환하는 함수다.
with과 비슷하지만, run은 확장함수이기 때문에 객체에 점(.) 표기법으로 호출할 수 있다.
또한, 초기화 작업과 함께 결과 값을 계산할 때 유용하게 사용된다.
1. 객체 초기화 및 결과값 반환
val service = MultiportService("https://example.kotlinlang.org", 80)
val result = service.run {
port = 8080
query(prepareRequest() + " to port $port")
}
println(result)
네트워크 서비스 객체를 초기화한 후, 쿼리결과를 처리하는 예시코드.
service.run은 service객체를 초기화한 후, port값을 수정하고, query( ) 메서드를 호출한 결과를 반환한다.
this 키워드를 통해 service속성과 메서드에 접근하고, 최종적으로 query결과를 리턴한다.
2. let과의 차이
val letResult = service.let {
it.port = 8080
it.query(it.prepareRequest() + " to port ${it.port}")
}
let은 it으로 접근하지만, run은 this로 객체에 접근한다.
run은 초기화 작업과 함께 결과를 계산하는 데 더 적합하다.
3. Non-extension 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)
}
run은 확장 함수 뿐만 아니라 비확장 함수로도 사용할 수 있다.
이 경우, 객체 없이도 여러 작업을 수행하고 결과값을 반환할 수 있다.
비확장 run은 코드 블록을 실행하고 결과를 계산할 때 유용하게 사용될 수 있다.
run은 객체의 초기화 작업과 결과 계산을 함께 처리해야 할 때 유용한 함수다.apply는 객체 자신을 반환하는 함수로, 주로 객체의 여러 속성을 초기화 하거나 설정할 때 사용된다.
this 키워드로 객체에 접근할 수 있고, 객체를 반환하기 때문에 객체의 설정 작업을 간결하게 처리할 수 있다.
주로 객체 구성과 관련된 작업에 자주 사용된다.
1. 객체 초기화 및 설정
val adam = Person("Adam").apply {
age = 32
city = "London"
}
println(adam)
객체의 여러 속성을 한꺼번에 설정.
apply를 사용해 adam객체의 속성인 age와 city를 간결하게 설정 가능하다.
그리고 최종적으로 adam객체가 반환된다.
2. UI 요소 초기화
val button = Button(this).apply {
text = "Click Me"
textSize = 16f
setOnClickListener {
// 클릭 리스너 처리
}
}
Button 객체의 여러 속성을 한 번에 설정하고, 최종적으로 button 객체를 반환.
3. 객체 구성 후 연속적인 호출
val config = Config().apply {
url = "https://example.com"
timeout = 5000
}.run {
// run으로 추가 작업 처리
fetchData(this)
}
apply속성을 통해 Config객체의 속성을 설정하고, 그 후 run을 통해 추가 작업을 진행 가능 하다. apply는 객체를 반환하므로, 연속적인 호출을 이어서 할 수 있음.
apply는 객체 구성 작업에서 매우 유용하며, 여러 속성을 설정한 후 객체를 반환하는 작업에 유용하다.also는 객체를 인자로 받아 여러 작업을 수행하고, 최종적으로 객체 자체를 반환하는 함수다.
객체에 대한 참조를 유지하며 부수적인 작업을 할 때 유용하다.
주로 디버깅, 로그 출력, 또는 객체를 처리하면서 추가적인 작업을 수행할 때 사용된다.
1. 부수적인 작업과 객체 반환
val numbers = mutableListOf("one", "two", "three")
numbers
.also { println("The list elements before adding a new one: $it") }
.add("four")
println(numbers)
리스트에 새로운 요소를 추가하기 전에 리스트의 상태를 출력하는 작업 수행.
2. 객체를 그대로 유지하면서 추가 작업 수행
val user = User("John", 30)
user.also {
println("User details: $it") // 객체를 그대로 출력하면서 로깅
}
user객체를 그대로 유지하면서 로그를 출력.
3. 메서드 체이닝에서 부수적인 작업 처리
val result = mutableListOf(1, 2, 3)
.also { println("Before filtering: $it") }
.filter { it > 1 }
.also { println("After filtering: $it") }
필터링 작업을 하기 전에 리스트의 상태를 출력 하고, 필터링 후에도 리스트의 상태를 출력.
also는 객체의 참조를 유지한 채 부수적인 작업을 수행해야 할 때 유용하다.| 함수 | 객체 참조 방법 | 반환 값 | 확장 함수 여부 | 주요 사용 목적 |
|---|---|---|---|---|
let | it | 람다 결과 반환 | 확장 함수 | 객체 변환, null-safe 처리, 체이닝된 호출의 결과 처리 |
run | this | 람다 결과 반환 | 확장 함수 | 객체 초기화 및 결과 계산, 블록 내에서 코드 실행 후 결과 반환 |
with | this | 람다 결과 반환 | 비확장 함수 | 객체에 여러 작업을 수행할 때, 반환 값이 필요 없을 때 |
apply | this | 객체 자신 반환 | 확장 함수 | 객체의 설정 및 초기화, 객체 구성 후 다시 반환 |
also | it | 객체 자신 반환 | 확장 함수 | 부수적인 작업 (로깅, 디버깅), 객체 참조 유지 |
왜이렇게 잘생기셨나요...?