본문 바로가기
Android.Kotlin

[Kotlin] 범위 지정 함수(scope function) 란? apply, run, let, with, also

by 동동하다 2023. 9. 19.
반응형

Kotlin에서 말하는 범위 지정 함수는 특정 객체 (수신 객체)에 대한 작업을 블록 안에서 실행할 수 있도록 하는 함수입니다. 블록은 해당 작업의 범위를 지정하기에 범위 지정 함수라고 부릅니다.

범위 지정 함수를 사용하게 되면 코드의 가독성이 증가하고 유지보수에 유리한 면이 생깁니다. 

그럼 범위 지정 함수의 구성 요소 및 종류에 대해 알아보도록 하겠습니다.

구성요성

범위 지정 함수의 구성 요소는 아래 2가지가 있습니다.

  • 수신 객체
  • 수신 객체 지정 람다

수신 객체는 범위 지정 함수에서 작업을 수행하는 타깃 객체입니다. 그리고 수신 객체 지정 람다는 해당 수신 객체로 수행하게 될 블록의 함수를 말합니다. 

범위 지정 함수에는 apply, run, let, with, also 등 5가지가 존재하는데 이 5가지의 구분은 수신 객체의 파라미터 유무 및 해당 객체 반환 여부에 따라 구분되어집니다.

apply

public inline fun <T> T.apply(block: T.() -> Unit): T

apply는 수신 객체의 확장 함수이며, 블록 안에서 별도의 return 값을 지정하지 않고 수신 객체 본인을 반환하는 함수입니다.

수신 객체의 확장 함수이기에 주로 수신 객체의 프로퍼티를 변경할 때 사용합니다. 주로 객체 생성 후 초기화를 할 때 사용합니다.

 

val person = Person().apply {
    name = "Eric"
    age = 30
}

run

public inline fun <T, R> T.run(block: T.() -> R): R

run 은 apply와 마찬가지로 수신 객체의 확장 함수이기에 별도의 it 이나 this 키워드로 파라미터를 받을 필요가 없습니다.

apply 와 차이점은 return 되는 값을 블록 내에서 지정할 수 있다는 점입니다. 그렇기에 수신객체의 특정한 동작을 수행한 결과 값을 리턴 받아서 사용해야 할 때 사용합니다.

val isDeveloper: Boolean = Person().let {
    canDevelop()	// canDevelop 의 경우 Person 의 내부 함수
}

let

public inline fun <T, R> T.let(block: (T) -> R): R

let 은 수신 객체 자체를 파라미터로 받아서 사용합니다. 그리고 블록에서 해당 수신 객체의 특정 작업 이후 결과 값을 return을 하게 됩니다. 

let 은 다음과 같은 경우 주로 사용합니다.

  • Null 체크 이후 코드를 실행해야 하는 경우
  • Nullable 한 수신 객체를 다른 Nullable 객체로 변환하는 경우
  • 단일 지역 변수의 범위를 제한하는 경우
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 의 범위는 이 블록 안 으로 제한 됩니다.
    dao.insert(person)
}

with

public inline fun <T, R> with(receiver: T, block: T.() -> R): R

with는 Nullable 하지 않은 수신 객체를 파라미터로 전달받아서 특정 작업을 수행하고 return 값이 필요하지 않을 때 사용합니다.

그래서 주로 객체의 함수를 여러 개 호출할 때 그룹화하는 용도로 사용합니다.

val person: Person = getPerson()
with(person) {
    print(name)
    print(age)
}

also

public inline fun <T> T.also(block: (T) -> Unit): T

 수신 객체를 파라미터로 받기는 하지만 수신 객체 자체를 사용하지 않거나, 수신 객체의 프로퍼티를 변경하지 않을 때 주로 사용합니다.

apply와 차이점은 return 값이 없기에, 객체 선언 시 로깅 또는 유효성 검사, 사이드 이펙트 검사 용도로 사용됩니다.

class Book(author: Person) {
    val author = author.also {
      requireNotNull(it.age)
      print(it.name)
    }
}

중첩 및 결합

범위 지정 함수의 경우 중첩해서 사용할 수 있지만 권장되어지지 않습니다. 중첩이 되면 코드의 가독성이 떨어지고 유지보수하기 힘들어지기 때문입니다.

특히 수신 객체가 암시적으로 전달되는 apply, run, with는 중첩하지 않는 것이 좋습니다.

만약 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 {
  print("Executing SQL update: $it.")
}.run {
  jdbc.update(this) > 0
}

위 코드는 DB에 user 정보를 입력하는 SQL을 수행하는 코드입니다. 해당 SQL 을 수행, 로깅, 성공 여부를 반환하여 줍니다.

반응형