본문 바로가기

개발 이야기/Kotlin

[Kotlin] apply, also, let, run, with 상황에 맞게 사용하기

Scope Functions

Kotlin 표준 라이브러리는 몇 가지 객체의 Context 내에서 코드 블록{}을 실행하는 것이 유일한 목적인 몇 가지 함수가 포함되어 있다. 객체에서 이 람다 함수를 호출하면 해당 함수는 일시적인 Scope를 생성하고, 해당 Scope 안에서는 객체의 이름 없이도 접근이 가능하다. 이러한 함수를 Scope Function(범위 지정 함수)이라고 하며, let, run, with, apply, also가 있다.

 

기본적으로 이 5가지 함수들은 동일한 기능을 수행하기 때문에 어떤 상황에 어떤 Scope Function을 사용하는 것이 맞는지 매우 혼동스럽기 때문에 처음 Kotlin을 사용하는 사용자 입장에서는 난처한 상황을 겪게된다. 일단 아래 5가지 함수의 정의를 살펴보자.

 

inline fun <T, R> with(receiver: T, block: T.() -> R): R {
    return receiver.block()
}

inline fun <T> T.also(block: (T) -> Unit): T {
    block(this)
    return this
}

inline fun <T> T.apply(block: T.() -> Unit): T {
    block()
    return this
}

inline fun <T, R> T.let(block: (T) -> R): R {
    return block(this)
}

inline fun <T, R> T.run(block: T.() -> R): R {
    return block()
}

 

5가지 함수가 각각 다른 정의를 가지고 있다는 것을 확인할 수 있다. 중요한 포인트는 각각의 함수 정의는 크게 아래 3가지 특징을 서로 다르게 가지고 있다는 점이고, 이 특징에 맞게 상황에 따라 다르게 사용하면 된다는 것이다.

 

(1) 수신 객체가 매개 변수로 명시적으로 전달 or 수신 객체의 확장 함수로 암시적으로 전달

(2) 수신 객체가 명시적 매개 변수로 전달 or 수신 객체의 확장 함수로 암시적으로 코드 블록 내부로 전달

(3) 수신 객체 자체를 반환 or 수신 객체 지정 람다의 실행 결과를 반환

apply 사용

apply는 객체를 설정하는 상황에서 사용한다. apply는 객체 자신을 다시 반환하기 때문에 특정 객체의 프로퍼티를 설정 후 바로 사용하기 쉽다.

val adam = Person("Adam").apply {
    age = 32
    city = "London"        
}
println(adam)

also 사용

also는 객체의 속성을 전혀 사용하지 않거나 이를 변경하지 않으면서 사용하는 경우에 사용한다. 대표적으로 객체의 데이터 유효성을 확인하거나, 디버그, 로깅 등의 부가적인 목적으로 사용할 때에 적합하다.

val numbers = mutableListOf("one", "two", "three")
numbers
    .also { println("The list elements before adding new one: $it") }
    .add("four")

let 사용

let은 call chain의 결과에서 1개 혹은 그 이상의 함수를 호출하는 데 사용할 수 있다. 예를 들어,

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
} 

 

또한 let은 non-null 값인 경우 코드 블럭을 실행하는 경우 많이 사용한다. non-null 객체인 경우에만 호출하기 위해서 아래처럼 ?. 오퍼레이터를 함께 사용하면 된다.

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 null inside '?.let { }'
    it.length
}

 

또 객체를 지역 변수로 범위를 제한해서 사용할 경우 사용한다. 아래 코드를 보면 firstItem은 코드 블록 안에서 제한적으로 사용되며 변경할 수 없다.

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 + "!"
}.toUpperCase()

println("First item after modifications: '$modifiedFirstItem'")

// The first item of the list is 'one'
// First item after modifications: '!ONE!'

with 사용

with는 람다 결과를 제공하지 않고 객체의 컨텍스트 안에서 함수를 호출하는 경우 사용한다. 주의할 점은 non-null인 객체만 사용 가능하다.

val numbers = mutableListOf("one", "two", "three")
with(numbers) {
    println("'with' is called with argument $this")
    println("It contains $size elements")
}

 

또는 값을 계산하기 위해 객체 프로퍼티나 함수를 사용하는 Helper 객체를 선언할 때 사용한다. (아래의 firstAndLast)

val numbers = mutableListOf("one", "two", "three")
val firstAndLast = with(numbers) {
    "The first element is ${first()}," +
    " the last element is ${last()}"
}
println(firstAndLast)

// The first element is one, the last element is three

run 사용

apply와 비슷하지만 이미 생성된 객체에 대한 call chain으로 호출한다는 점이 다르다. run은 람다 함수 안에 객체 초기화와 return 값의 계산이 포함될때 유용하게 사용할 수 있다.

val service = MultiportService("https://example.kotlinlang.org", 80)

val result = service.run {
    port = 8080
    query(prepareRequest() + " to port $port")
}

// the same code written with let() function:
val letResult = service.let {
    it.port = 8080
    it.query(it.prepareRequest() + " to port ${it.port}")
}

 

이 외에도 non-extension 함수로도 사용 가능하다. 예를 들어 어떤 객체를 생성하기 위한 코드를 블럭 안에 위치하도록 하면서 가독성을 좋게 만들어줄 수 있다.

val hexNumberRegex = run {
    val digits = "0-9"
    val hexDigits = "A-Fa-f"
    val sign = "+-"
​
    Regex("[$sign]?[$digits$hexDigits]+")
}
​
for (match in hexNumberRegex.findAll("+1234 -FFFF not-a-number")) {
    println(match.value)
}

// +1234
// -FFFF
// -a
// be