Search

Scope functions

Kotlin 표준 라이브러리에는 객체의 맥락에서 코드 블록을 실행하기 위한 여러 함수가 있다.
이러한 함수는 객체에 대해 호출되고 제공된 람다 표현식이 있을 때 임시 범위를 형성한다.
범위 내에서 객체의 이름 없이 접근할 수 있다.
이러한 함수를 범위 함수라고 한다.
이들에는 let, run, with, apply, also 가 있다.
// let과 함께 작성 Person("Alice", 20, "Amsterdam").let { println(it) it.moveTo("London") it.incrementAge() println(it) } // let 없이 작성 val alice = Person("Alice", 20, "Amsterdam") println(alice) alice.moveTo("London") alice.incrementAge() println(alice)
Kotlin
복사
기본적으로 이 함수들은 모두 객체에서 코드 블록을 실행하는 동일한 작업을 수행한다.
그러나 객체가 블록 내에서 어떻게 사용되는지와 전체 표현식의 결과가 무엇인지에 따라 차이가 있다.
범위 함수 없이 코드를 작성하면 새 변수를 도입해야 하고 사용할 때마다 그 이름을 반복해야 한다.
범위 함수는 새로운 기술적 기능을 도입하지 않지만 코드를 더 간결하고 읽기 쉽게 만들 수 있다.
범위 함수들 사이의 유사성으로 인해 사용 사례에 맞는 올바른 함수를 선택하는 것이 어려울 수 있다.
선택은 주로 의도와 프로젝트 내 일관성에 따라 달라진다.

범위 함수 선택

함수
객체 참조
반환 값
확장 함수 여부
let
it
람다 결과
run
this
람다 결과
run
-
람다 결과
아니오: 컨텍스트 객체 없이 호출됨
with
this
람다 결과
아니오: 컨텍스트 객체를 인수로 받음
apply
this
컨텍스트 객체
also
it
컨텍스트 객체
의도에 따라 범위 함수를 선택하는 간단한 가이드
null이 아닌 객체에서 람다 실행 → let
표현식을 지역 범위의 변수로 도입 → let
객체 구성 → apply
객체 구성 및 결과 계산 → run
표현식이 필요한 문 실행 → 비확장 run
추가 효과 → also
객체에 대한 함수 호출 그룹화 → with
범위 함수는 코드를 더 간결하게 만들 수 있지만 과도하게 사용하지 않도록 주의해야 한다.
이는 코드의 가독성을 떨어뜨리고 오류를 초래할 수 있다.
또한 범위 함수의 중첩을 피하고 체이닝 시 주의하는 것이 좋다.

차이

스코프 함수는 유사한 특성을 가지므로 이들 간의 차이를 이해하는 것이 중요하다.
각 스코프 함수는 컨텍스트 객체에 대한 참조 방식과 반환 값의 두 가지 주요한 차이점이 존재한다.

컨텍스트 객체에 대한 참조 방식

fun main() { val str = "Hello" // this str.run { println("The string's length: $length") //println("The string's length: ${this.length}") // 같은 의미 } // it str.let { println("The string's length is ${it.length}") } }
Kotlin
복사
스코프 함수에 전달된 람다 안에서는 컨텍스트 객체를 실제 이름 대신 짧은 참조로 사용할 수 있다.
각 스코프 함수는 람다 리시버(this) 또는 람다 인자(it)의 두 가지 방법 중 하나로 컨텍스트 객체를 참조한다.
두 방법 모두 동일한 기능을 제공하므로 각 사용 사례에 따른 장단점을 설명하고 추천 사항을 제공한다.

this

val adam = Person("Adam").apply { age = 20 // this.age = 20와 동일 city = "London" } println(adam)
Kotlin
복사
run, with, apply 는 컨텍스트 객체를 람다 리시버로 참조한다.
따라서 람다 안에서 객체는 일반 클래스 함수와 동일하게 this로 사용할 수 있다.
대부분의 경우 리시버 객체의 멤버에 접근할 때 this를 생략할 수 있어 코드가 더 간결해진다.
그러나 this를 생략하면 리시버의 멤버와 외부 객체 또는 함수 간의 구분이 어려울 수 있다.
따라서 리시버 객체가 주로 자신의 멤버에 대해 작업하는 람다에서는 this를 사용하는 것이 권장된다.

it

// 기본 이름인 it 을 사용하는 예시 fun getRandomInt(): Int { return Random.nextInt(100).also { writeToLog("getRandomInt() generated value $it") } } // 인자 이름을 value 로 사용하는 예시 fun getRandomInt(): Int { return Random.nextInt(100).also { value -> writeToLog("getRandomInt() generated value $value") } } val i = getRandomInt() println(i)
Kotlin
복사
let 과 also 는 컨텍스트 객체를 람다 인자로 참조한다.
인자 이름이 지정되지 않으면 객체는 암시적인 기본 이름인 it 으로 접근된다.
it은 this보다 짧고 it을 사용하는 표현은 일반적으로 읽기 쉽다.
그러나 객체의 함수나 속성을 호출할 때는 this처럼 암시적으로 객체를 사용할 수 없으므로 it은 주로 함수 호출에서 인자로 객체를 사용할 때 더 적합하다.
또한 여러 변수를 사용하는 코드 블록에서도 유리하다.

반환값

스코프 함수는 반환 값에서 차이를 보인다.
apply 와 also 는 컨텍스트 객체를 반환한다.
let, run, with 는 람다 결과를 반환한다.
어떤 반환 값을 원하는지 신중히 고려해야 하며 이는 코드에서 다음에 수행할 작업에 따라 적절한 스코프 함수를 선택하는데 도움이 된다.

컨텍스트 객체 반환

// 호출 체인으로 사용 val numberList = mutableListOf<Double>() numberList.also { println("Populating the list") } .apply { add(2.71) add(3.14) add(1.0) } .also { println("Sorting the list") } .sort() // 컨텍스트 객체 자체 반환으로 사용 fun getRandomInt(): Int { return Random.nextInt(100).also { writeToLog("getRandomInt() generated value $it") } }
Kotlin
복사
apply 와 also 의 반환값은 컨텍스트 객체 자체이다.
따라서 이러한 함수는 호출 체인에 포함될 수 있으며 같은 객체에 대해 연속적으로 함수 호출을 이어갈 수 있다.
또한 컨텍스트 객체 자체를 반환하는 함수의 반환문에도 사용할 수 있다.

람다 결과 반환

// 결과를 변수에 할당 val numbers = mutableListOf("one", "two", "three") val countEndsWithE = numbers.run { add("four") add("five") count { it.endsWith("e") } } println("There are $countEndsWithE elements that end with e.") // 반환 값을 무시하고 지역 변수를 위한 임시 범위 생성 val numbers = mutableListOf("one", "two", "three") with(numbers) { val firstItem = first() val lastItem = last() println("First item: $firstItem, last item: $lastItem") }
Kotlin
복사
let, run, with 는 람다 결과를 반환한다.
따라서 결과를 변수에 할당하거나 결과에 대해 연산을 연결할 때 사용할 수 있다.
또한 반환 값을 무시하고 스코프 함수를 사용하여 지역 변수를 위한 임시 범위를 생성할 수 있다.

스코프 함수

let

컨텍스트 객체
반환값
인자로 사용 가능(it)
람다 결과
// 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) // 필요한 경우 추가 함수 호출 }
Kotlin
복사
let 은 호출 체인의 결과에서 하나 이상의 함수를 호출하는데 사용할 수 있다.
val numbers = mutableListOf("one", "two", "three", "four", "five") numbers.map { it.length }.filter { it > 3 }.let(::println)
Kotlin
복사
let 을 사용하면 람다에서 인자(it)를 사용한 단일 함수 호출 시 메서드 레퍼런스(::)를 사용할 수 있다.
val str: String? = "Hello" val length = str?.let { println("let() called on $it") processNonNullString(it) it.length }
Kotlin
복사
let 은 주로 null이 아닌 값을 포함하는 코드 블록을 실행하는데 사용된다.
null인 객체에 대해 작업을 수행하려면 안전 호출 연산자(?.)를 사용하고 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'")
Kotlin
복사
let 을 사용하여 제한된 범위를 가진 로컬 변수를 도입하여 코드를 더 쉽게 읽을 수 있다.
컨텍스트 객체에 대한 새 변수를 정의하려면 람다 인자로 이름을 제공하여 기본 it 대신 사용할 수 있다.

with

컨텍스트 객체
반환값
수신자로 사용 가능(this)
람다 결과
val numbers = mutableListOf("one", "two", "three") with(numbers) { println("'with' is called with argument $this") println("It contains $size elements") }
Kotlin
복사
with 는 컨텍스트 객체의 함수 호출을 위한 것으로 반환 결과가 필요하지 않을 때 사용을 권장한다.
코드는 “이 객체와 함께 다음을 수행”으로 읽을 수 있다.
val numbers = mutableListOf("one", "two", "three") val firstAndLast = with(numbers) { "The first element is ${first()}," + " the last element is ${last()}" } println(firstAndLast)
Kotlin
복사
with 를 사용하여 헬퍼 객체의 속성이나 함수로 값을 계산하는데 사용할 수도 있다.

run

컨텍스트 객체
반환값
수신자로 사용 가능(this)
람다 결과
// run 을 사용한 코드 val service = MultiportService("https://example.kotlinlang.org", 80) val result = service.run { port = 8080 query(prepareRequest() + " to port $port") } // let 을 사용한 코드 val letResult = service.let { it.port = 8080 it.query(it.prepareRequest() + " to port ${it.port}") }
Kotlin
복사
run 은 with 와 동일하지만 확장 함수로 구현된다.
따라서 let 처럼 컨텍스트 객체에서 점 표기법을 사용하여 호출할 수 있다.
run 은 객체를 초기화하고 반환 값을 계산할 때 유용하다.
val hexNumberRegex = run { val digits = "0-9" val hexDigits = "A-Fa-f" val sign = "+-" Regex("[$sign]?[$digits$hexDigits]+") }
Kotlin
복사
run 은 비확장 함수로도 호출할 수 있다.
비확장 run 은 컨텍스트 객체가 없지만 여전히 람다 결과를 반환한다.

apply

컨텍스트 객체
반환값
수신자로 사용 가능(this)
객체 자체
val adam = Person("Adam").apply { age = 32 city = "London" } println(adam)
Kotlin
복사
apply 는 컨텍스트 객체 자체를 반환하므로 주로 값이 반환되지 않고 수신자 객체의 멤버를 주로 조작하는 코드블록에 사용된다.
apply 의 가장 일반적인 사용 사례는 객체 구성이다.

also

컨텍스트 객체
반환값
인자로 사용 가능(it)
객체 자체
val numbers = mutableListOf("one", "two", "three") numbers .also { println("The list elements before adding new one: $it") } .add("four")
Kotlin
복사
also 는 컨텍스트 객체를 인자로 받는 작업을 수행하는데 유용하다.
객체의 속성 및 함수보다 객체의 대한 참조가 필요한 작업에 사용하거나 외부 스코프의 this 참조를 가리고 싶지 않을 때 사용한다.
코드에서 also 는 “그리고 다음 작업을 수행하라” 고 읽을 수 있다.

takeIf 와 takeUnless

스코프 함수 외에도 표준 라이브러리에는 takeIf 와 takeUnless 함수가 포함되어 있다.
이러한 함수는 객체 상태의 검사를 호출 체인에 포함할 수 있게 해준다.
val number = Random.nextInt(100) val evenOrNull = number.takeIf { it % 2 == 0 } val oddOrNull = number.takeUnless { it % 2 == 0 } println("even: $evenOrNull, odd: $oddOrNull")
Kotlin
복사
takeIf
객체가 주어진 조건을 만족하면 이 객체를 반환하고 그렇지 않으면 null을 반환한다.
즉, takeIf는 단일 객체에 대한 필터링 함수이다.
takeUnless
객체가 주어진 조건을 만족하면 null을 반환하고 그렇지 않으면 객체를 반환한다.
takeIf 와 takeUnless 를 사용할 때 객체는 람다 인자(it)로 사용 가능하다.
val str = "Hello" val caps = str.takeIf { it.isNotEmpty() }?.uppercase() println(caps)
Kotlin
복사
takeIf 와 takeUnless 후에 다른 함수를 체인할 때 반환 값이 nullable 이므로 null 체크를 수행하거나 안전 호출 연산자(?.)를 사용해야 한다.
// 스코프 함수 없이 작성한 예제 fun displaySubstringPosition(input: String, sub: String) { val index = input.indexOf(sub) if (index >= 0) { println("The substring $sub is found in $input.") println("Its start position is $index.") } } displaySubstringPosition("010000011", "11") displaySubstringPosition("010000011", "12") // 스코프 함수로 작성한 예제 fun displaySubstringPosition(input: String, sub: String) { input.indexOf(sub).takeIf { it >= 0 }?.let { println("The substring $sub is found in $input.") println("Its start position is $it.") } } displaySubstringPosition("010000011", "11") displaySubstringPosition("010000011", "12")
Kotlin
복사
takeIf 와 takeUnless 는 특히 스코프 함수와 조합하여 사용하는 것이 유용하다.
예를 들어, 객체가 주어진 조건을 만족할 때 코드 블록을 실행하기 위해 takeIf 를 호출한 후 안전 호출 연산자(?.)와 함께 let 을 사용할 수 있다.