공부저장소

범위 지정 함수의 정의는 코틀린 공식 문서에서 볼 수 있다.
The Kotlin standard library contains several functions whose sole purpose is to execute a block of code within the context of an object.
When you call such a function on an object with a lambda expression provided, it forms a temporary scope.
In this scope, you can access the object without its name.
Such functions are called scope functions.

 

쉽게 말하면 특정 범위 안에서 객체의 이름을 생략하고 코드를 실행시켜주는 함수이다.
이 말은 범위 지정 함수를 사용하는 이유와도 관련이 있다.
범위 지정 함수를 사용하는 이유는 코드의 간결성과 유지보수의 용이함 때문이다.
범위 지정 함수 자체가 이름을 생락하고 코드를 실행시켜주는 함수기 때문에, 사용하는 이유도 코드의 간결성과 유지보수의 용이함이 되는 것이다.

코틀린에서의 범위 지정 함수는 apply, run, with, also, let이 있다.
범위 지정 함수는 두가지 구성 요소를 가진다.
1. 수신 객체(receiver)
2. 수신 객체 지정 람다
범위 지정 함수를 이해하려면 수신 객체를 알아야 한다.
수신 객체라는 용어는 Kotlin의 확장 함수에서 등장하는데 먼저 확장함수에 대해 알아보자.

 

확장 함수

확장함수는 어떤 클래스의 맴버 메소드인 것처럼 호출할 수 있지만 그 클래스의 밖에 선언된 함수이다.
예를들어 Kotlin을 설치하면 기본으로 제공되는 String 클래스의 확장 함수를 정의하면 새로운 맴버 메소드처럼 사용할 수 있는 기능이다.
아래 예시를 보자.

// 확장 함수의 정의
fun String.lastChar(): Char = this[this.length - 1]

String 클래스의 확장 함수를 정의한 예시이다.
기본 String 클래스는 마지막 문자를 반환하는 함수가 없지만, 위와 같이 확장함수를 정의하면 String 클래스의 객체는 lastChar() 라는 함수를 사용하여 마지막 문자를 가져올 수 있다.

확장 함수를 정의하는 방법은 일반 함수를 정의하는 방법에서 fun 키워드와 함수 이름 사이에 확장할 클래스의 이름과 마침표를 붙여주면 된다.

이제 확장 함수를 사용해 보자.

// 확장 함수를 사용
val last: Char = "hello".lastChar()
println(last)
// 결과 : o

확장 함수를 사용하여 String 객체의 마지막 글자를 얻을 수 있다.

위의 확장 함수를 정의하는 예시에서 일반 함수와 다른 점이 있다.
this는 원래 현재 함수가 정의된 클래스이거나, 최상위 함수라면 컴파일 오류가 발생한다.
그러나 확장 함수에서의 this는 확장된 크래스의 객체, 즉 확장 함수를 사용하는 그 객체를 의미한다.
그 객체가 바로 수신 객체이고, 확장할 클래스의 타입이 수신 객체 타입이다.
밑의 그림을 보자.

그럼 수신 객체는 무엇을 받는 것일까?
'Kotlin in Action' 에서는 수신 객체를 '확장 함수가 호출되는 대상이 되는 값(객체)'라고 설명하고 있다.
'수신' 을 무엇을 받는다는 의미로 본다면, 확장함수의 코드를 실행할 대상이 되기 때문에 '객체가 코드를 받는다' 라는 뜻으로 생각하면 될 것 같다.
말 그대로 확장함수를 실행할 값(객체) 라고 생각하면 된다.

우리가 평소에 자주 쓰던 기본함수로 생각해보자.

// 코틀린의 기본 함수 형식
fun lastChar(str: String): Char = str[str.length - 1]

위에서의 str 이 수신 객체라고 생각하면 편할 것 같다.

 

범위 지정 함수(Scope functions)

범위 지정 함수에는 apply, run, with, also, let이 있다.

위에서 말했듯이 범위 지정 함수는 2가지 구성요소를 가진다.
1. 수신 객체(receiver)
2. 수신 객체 지정 람다

수신 객체는 알아 보았지만, 수신 객체 지정 람다는 무엇일까?

 

수신 객체 지정 람다

범위 지정 함수들의 정의를 보자.

// 범위 지정 함수의 정의
inline fun <T> T.apply(block: T.() -> Unit): T {
    block()
    return this
}

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

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, R> T.let(block: (T) -> R): R {
    return block(this)
}

위에서 배운 수신 객체를 토대로 정의에서 receiver(T)가 수신 객체라는것을 알 수 있다.
그리고 나머지 구성요소인 수신 객체 지정 람다는 block 부분인데, 말 그대로 수신객체를 지정하는 람다식이라는 뜻이다.

 

범위 지정 함수의 사용

범위 지정 함수를 쓰는 이유가 코드의 간결성과 유지보수의 용이함 때문이라고 말하였다.
밑의 범위 지정 함수를 쓴 코드와 쓰지 않은 코드를 비교해 보자.

class Person {
    var name: String? = null
    var age: Int? = null
}

val person: Person = Person()
fun main(args: Array<String>) {

    val person: Person = Person()

    // 범위 지정 함수를 사용하지 않는 예
    println(person.age)
    println(person.name)

    // 범위 지정 함수를 사용하는 예 1
    with(person) {
        println(age)
        println(name)
    }

    // 범위 지정 함수를 사용하는 예 2
    person.also {
        println(it.age)
        println(it.name)
    }
}

범위 지정 함수를 사용하여 person 객체의 중복 사용을 방지하였다.
그리고 person 객체가 어디서부터 어디까지 사용되는지 쉽게 볼수 있게 되었다.
이렇게 범위 지정 함수는 많은 상황에서 유용하다.
그런데 왜 범위 함수가 5개나 필요할까?

 

범위 지정 함수들의 차이점

위에서 보았던 with also를 다시 보자.

// with와 also의 정의
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
}

with also는 다음과 같은 차이점을 갖는다.

1. 호출 시에 수신 객체의 전달
- with는 수신 객체가 매개변수 T로 명확하게 제공된다.
- also T의 확장함수로 수신객체가 암시적으로 제공된다.
2. 전달된 수신 객체가 수신 객체 지정 람다에 전달되는 방식
- with는 수신 객체 지정 람다가 T의 확장함수 형태로 코드블록 내에 암시적으로 전달된다.
- also는 수신 객체 지정 람다에 매개변수 T로 코드 불록 내에 명시적으로 전달된다.
3. 반환값
- with는 람다를 실행한 결과를 반환 한다.
- also는 코드블록 내에 전달된 수신 객체를 반환한다.

이 외에는 다른점이 없다.
다시말하면 범위 지정 함수들은 이 3가지 차이 때문에 하는 일이 비슷하더라도 다른 방식으로 사용해야 한다.

아래는 각 함수의 차이점을 잘 나타내주는 표이다.

이제 다섯 가지 함수가 무엇이 다른지 알게 되었다.
그러나 여전히 이 함수들은 비슷해 보이고, 실제로 많은 상황에서 서로를 대체하여 사용할 수 있다.
코틀린 공식 문서에서는 이 다섯가지 함수에 대한 몇 가지 모범 사례와 규칙을 제시하고 있다.

 

범위 지정 함수의 사용 규칙

apply 사용 규칙

수신 객체 람다 내부에서 수신 객체의 함수를 사용하지 않고 수신 객체 자신을 다시 반환하려는 경우에 apply를 사용한다.
수신 객체의 프로퍼티 만을 사용하는 대표적인 경우가 객체의 초기화이며, 이때 apply를 사용한다.

// apply 사용 예
val cheolsu = person.apply {
    name = "cheolsu"
    age = 15
}
// apply를 사용하지 않는 동일한 예
val younghee = Person()
person.name = "younghee"
person.age = 15

 

run 사용 규칙

어떤 값을 계산할 필요가 있거나 여러개의 지역 변수의 범위를 제한하려면 run을 사용한다.
매개 변수로 전달된 명시적 수신 객체를 암시적 수신 객체로 변환 할 때 run()을 사용할 수 있다.

// run 사용 예
val insert: Boolean = run {
    // person과 personDao의 범위를 제한
    val person: Person = getPerson()
    val personDao: PersonDao = getPersonDao()

    // 수행결과를 반환
    personDao.insert(person)
}

fun printPerson(person: Person) = person.run {
    // person을 수신 객체로 변환하여 age 값을 사용
    print(name)
    print(age)
}
// run을 사용하지 않는 동일한 예
val person: Person = getPerson()
val personDao: PersonDao = getPersonDao()
val inserted: Boolean = personDao.insert(person)

fun printPerson(person: Person) {
    print(person.name)
    print(person.age)
}

 

with 사용 규칙

Non-nullable (Null이 될 수 없는) 수신 객체이고 결과가 필요하지 않은 경우에만 with를 사용한다.

// with 사용 예
val person: Person = getPerson()
with(person) {
    print(name)
    print(age)
}
//with를 사용하지 않는 동일한 예
val person: Person = getPerson()
print(person.name)
print(person.age)

 

also 사용 규칙

수신 객체 람다가 전달된 수신 객체를 전혀 사용하지 않거나 수신 객체의 속성을 변경하지 않고 사용하는 경우 also를 사용한다.
also apply와 마찬가지로 수신 객체를 반환하므로 블록 함수가 다른 값을 반환해야 하는 경우에는 also를 사용할 수 없다.
예를 들어 객체의 사이드이팩트를 확인하거나 수신 객체의 프로퍼티에 데이터를 할당하기 전에 해당 데이터의 유효성을 검사할 떄 매우 유용하다.

// also 사용 예
class Book(author: Person) {
    val author = author.also {
        requireNotNull(it.age)
        print(it.name)
    }
}
// also를 사용하지 않는 동일한 예
class Book(val author: Person) {
    init {
        requireNotNull(author.age)
        print(author.name)
    }
}

 

let 사용 규칙

다음과 같은 경우에 let을 사용한다.
- 지정된 값이 null이 아닌 경우에 코드를 실행해야 하는 경우
- Nullable 객체를 다른 Nullable 객체로 변환하는 경우
- 단일 지역 변수의 범위를 제한하는 경우

// let 사용 예
getNullablePerson()?.let {
    // null이 아닐때만 실행됨
    promote(it)
}

val driversLicence: Licence? = getNullablePerson()?.let {
    // nullable personal 객체를 nullable driversLicence 객체로 변경
    licenceService.getDriversLicence(it)
}

val person: Person = getPerson()
getPersonDao().let { dao ->
    dao.insert(person)
}
// let을 사용하지 않는 동일한 예
val person: Person? = getPromotablePerson()
if (person != null) {
    promote(person)
}

val driver: Person? = getDriver()
val driverLicence: Licence? = if (driver == null) null else licenceservice.getDriversLicence(it)

val person: Person = getPerson()
val personDao: PersonDao = getPersonDao()
personDao.insert(person)

 

여러 범위 지정 함수 결합

코드 가독성을 향상시키기 위해 범위 지정 함수를 분리하여 어떻게 사용할 수 있는지 알아보았다.
하나의 코드 블록 내에서 여러 범위 지정 함수를 중첩 하려는 경우가 종종 있다.
범위 지정 함수가 중첩되면 코드의 가독성이 떨어지고 파악하기 어려워진다.

원칙적으로는 중첩을 하지 않는것이 좋다.

수신 객체 지정 람다에 수신 객체가 암시적으로 전달되는 apply, run, with는 중첩하지 말아야 한다.

이 함수들은 수신 객체를 this 또는 생략하여 사용하고, 수신객체의 이름을 다르게 지정할 수 없기 때문에 중첩될 경우 혼동하기 쉽다.

also let을 중첩 해야만 할 때는 암시적 수신 객체를 가르키는 매개변수인 it을 사용하지 말아야 한다.
대신 명시적인 이름을 제공하여 코드 상에서 이름이 혼동되지 않도록 해야 한다.

 

 

범위 지정 함수를 호출 체인에 결합 할 수 있다.

중첩과는 달리 범위 지정 기능을 호출 체인에 결합하면 코드의 가독성이 향상된다.

private fun insert(user: User) = sqlBuilder().apply{
    append("INSERT INTO USER (email, name, age) VALUES")
    append("(?", user.email)
    append(",?", user.name)
    append(",)", user.age)
}.also{
    println("Excuting SQL update: $it.")
}.run{
    jdbc.update(this) > 0
}

위 예시는 사용자를 데이터베이스에 삽입하기 위한 DAO 기능을 보여준다.

SQL 준비, SQL 로그 출력 및 SQL 실행과 같은 구현을 범위 지정 함수로 분리한다.
마지막으로 이 함수는 삽입 성공을 나타내는 boolean 값을 반환한다.

 

Reference

https://medium.com/@limgyumin/%EC%BD%94%ED%8B%80%EB%A6%B0-%EC%9D%98-apply-with-let-also-run-%EC%9D%80-%EC%96%B8%EC%A0%9C-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94%EA%B0%80-4a517292df29

 

코틀린 의 apply, with, let, also, run 은 언제 사용하는가?

원문 : “Kotlin Scoping Functions apply vs. with, let, also, and run”

medium.com

https://cbs5295.medium.com/kotlin-%EC%88%98%EC%8B%A0-%EA%B0%9D%EC%B2%B4-%EC%A7%80%EC%A0%95-%EB%9E%8C%EB%8B%A4-lambda-with-receiver-c2457252e9f1

 

[Kotlin] Scope functions

[Kotlin 수신 객체, Kotlin Receiver, Kotlin lambda with receiver, Kotlin with, Kotlin apply, Kotlin run, Kotlin also, Kotlin let] 에 관한 내용을 포함 중

medium.com

https://kotlinlang.org/kotlinlang.org/docs/scope-functions.html

 

Scope functions - Help | Kotlin

 

kotlinlang.org