[Kotlin] Scope Funtions

Minji Jeong·2022년 6월 29일
post-thumbnail
자바로 개발을 하다가 코틀린으로 개발을 시작한지 얼마 안되었을 때 let, also, apply .. 등 자바에서는 보지 못했던 생소한 것들을 마주하게 되어서 머리가 어지러웠던 기억이 난다. 맨 처음 코틀린을 공부할 때 더욱 어렵게 느껴졌던 이유가 바로 범위지정함수(Scope Functions) 때문이 아니였을까 싶은데 😥 사실 이 함수들을 자유자재로 잘 쓰고 싶어도 코틀린 공식문서나 블로그나 개념이 어렵게 설명되어 있어서 그동안 잘 안쓰다가, 개인 프로젝트 하나를 마무리하면서 코드를 전체적으로 정리하는 겸, 범위지정함수에 대해 제대로 이해하고 적용해보고 싶어서 이렇게 포스팅을 남기게 되었다.

Scope Functions

1. Introduce

범위 지정 함수는 코틀린에서 제공하는 함수로, 특정 객체에 대한 작업을 코드 블록 내에서 실행할 수 있도록 한다. 이러한 코드 블록은 특정 객체에 대해 해야 할 일시적인 '작업의 범위'가 되기 때문에 범위 지정 함수라고 부르며, 이 범위 내에선 객체의 이름 없이 객체 내 변수에 접근할 수 있다. 코틀린에서 제공하는 범위 지정 함수로는 let, run, with, apply, also 총 5개가 있다.

이 5개의 함수들은 비슷한 기능을 하며, 공통적으로 다음 두 가지의 요소를 가진다.

  • 수신 객체 (Receiver)

  • 수신 객체 지정 람다 (수신 객체를 지정하는 람다식)

    FunctionObject referenceReturn valueIs extension function
    letitLambda resultYes
    runthisLambda resultYes
    run-Lambda resultNo: called without the context object
    withthisLambda resultNo: takes the context object as an argument.
    applythisContext objectYes
    alsoitContext objectYes

또한 이 비슷한 기능을 하는 함수들의 차이를 결정짓는 것은 다음 두 가지의 요소다.

  • Context object 참조 방법 (this or it)
  • Return value

this vs it

범위 지정 함수의 람다식 내에서는 실제 객체명 대신 this 또는 it 키워드로 접근할 수 있다.

run with apply : this
let also : it

1. this
run, with, apply는 람다식 내에서 this를 사용해서 객체에 접근할 수 있기 때문에, 객체의 필드에 바로 접근할 수 있다. this는 생략이 가능하지만 동일한 이름의 멤버가 있을 경우 구별하기가 어렵기 때문에 아래 예제처럼 this를 붙여서 사용하는 것이 좋다.

data class Person(var name: String, var age: Int = 0, var city: String = "")

fun main() {
    val age : Int = 22
    val city : String = "Seoul"
    val adam = Person("Sumin").apply {
        this.age = age
        this.city = city
    }
    println(adam)
}
Person(name=Sumin, age=22, city=Seoul)

2. it
letalso는 it를 사용해 객체에 접근하는데, it말고 따로 전달 인자명을 지정해서 접근할 수도 있다. 전달 인자명을 따로 지정하지 않으면 기본적으로 it으로 접근하게 된다.

fun writeToLog(message: String) {
    println("INFO: $message")
}

fun main() {
    fun getRandomInt(): Int {
        return Random.nextInt(100).also { value ->
            writeToLog("getRandomInt() generated value $value")
        }
    }

    val i = getRandomInt()
    println(i)
}

Return Value

apply also : Context Object 객체 자체를 반환하며, 따라서 체인 형식으로 연속적인 호출이 가능하다.

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()
    
//Result
Populating the list
Sorting the list
[1.0, 2.71, 3.14]

let run with : 람다식 결과를 반환하며, 따라서 결과를 변수에 할당하거나 결과에 대해 추가적인 작업 등을 수행할 때 사용할 수 있다.

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.")

//Result
There are 3 elements that end with e.

먼저 자세히 들어가기 전에 with 함수의 원형을 살펴보자. with 함수는 다음과 같이 정의되며, receiver가 수신 객체, block이 수신 객체 지정 람다이다.

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

우리는 with을 사용하여 코드를 더욱 간결하게 만들 수 있다. name, age라는 필드를 가진 Person 이라는 객체가 있고, 각 필드의 값을 출력하고자 할 때 with을 사용하지 않은 코드와 with을 사용한 코드는 어떤 차이가 있는지 보자.

// without 'with'
val person: Person = getPerson()
print(person.name)
print(person.age)

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

with을 사용했을 때, 기존 코드보다 person 변수가 덜 호출되어 사용된다는 것을 볼 수 있다. 만약 Person 클래스가 더 많은 필드들을 가진다면, 기존 코드로 작성했을 시 변수 호출이 잦아질 것이고 따라서 코드가 더욱 더 지저분해질 것이다.

여튼, 이러한 범위 지정 함수들을 제대로 사용할 줄 안다면 우리는 코드를 더 가독성있게 만들 수 있다. 물론 너무 남발해서 사용한다면 오히려 가독성이 저하되고 오류가 발생할 수 있기 때문에 주의해서 사용해야 한다(또한 중첩문 내에서 사용하는것도 좋지 않다). 그렇다면 5개의 범위 지정 함수들의 개념과 사용법에 대해 한번 알아보자.

코틀린 공식 문서에서는 5개의 함수들의 개념과 사용법을 아주 자세하게 기술해놓았다. 아래 링크에 해당하는 코틀린 공식 문서를 첨부해놓았으니, 원문을 확인하고 싶다면 아래 링크를 클릭하자.

👉 Kotlin Scope Functions


2. How to use

우리는 범위 지정 함수들을 언제, 어떻게 프로젝트에 적용해서 사용해야 할까? 사용 목적에 맞는 범위 지정 함수들을 선택할 수 있도록 코틀린에서는 각 함수들 간의 주요 차이점을 정리해놓은 표를 제공하고 있지만, 표만 보고 실제 코드에 적용하는 것은 어려울 수 있다. 따라서 이번엔 각 함수에 대한 개념과 사용 방법에 대해 알아보도록 하자.

1. let

fun <T, R> T.let(block: (T) -> R): R = block(this)	
val r: R = T().let { it.foo(); it.toR() }

let은 자기 자신을 인수로 전달하고 수행된 결과, 즉 블록의 마지막 값을 반환한다. 주로 주어진 객체에 대해 null check를 진행한 후 코드블록 실행하거나 특정한 nullable 객체를 다른 nullable 객체로 변환해야 하는 경우에 사용한다. 참고로 let으로 특정 객체에 대해 null check를 하고 싶다면 코틀린의 null check 연산자인 '?'와 함께 사용해야 한다.

data class Person(var name: String, var age: Int, var city: String) {
    fun moveTo(newCity: String) { city = newCity }
    fun incrementAge() { age++ }
}

fun main() {
    Person("Jaewoo", 20, "Seoul").let {
        println(it)
        it.moveTo("Busan")
        it.incrementAge()
        println(it)
    }
}

//Result
Person(name=Jaewoo, age=20, city=Seoul)
Person(name=Jaewoo, age=21, city=Busan)

let을 사용하지 않았을 때의 코드는 다음과 같다.

val jeowoo = Person("Jaewoo", 20, "Seoul")
println(jaewoo)
jaewoo.moveTo("Busan")
jaewoo.incrementAge()
println(jaewoo)
💡 ViewModel의 LiveData를 Observe 할 때, 람다 내부에서의 let 사용
var dataList = ArrayList<DataModel>()
...

viewModel.get().observe(viewLifecycleOwner, Observer { list ->
    list?.let { // list가 null이 아니라면 코드블록 실행
    	dataList = it as ArrayList<DataModel>
    }
})

2. run

fun <T, R> T.run(block: T.() -> R): R = block()
val r: R = T().run { this.foo(); this.toR() }			

run은 let과 마찬가지로 블록의 마지막 결과를 반환한다. 블록 안에 사용되는 변수는 모두 임시로 사용되는 변수로, 복잡한 계산이나 임시변수가 많이 필요할 때 유용하다. let과 거의 동일하지만 let은 주로 null check를 하기 위해 사용한다는 점, 또한 run은 let과 다르게 람다식 내에서 this를 사용해서 객체에 접근한다는 점에서 차이가 있다.

val date = run {
	// month & day -> 임시변수
    val month =  (LocalDateTime.now().month.value).toString()
    val day = (LocalDateTime.now().dayOfMonth).toString()
    "${month}${day}일"
}
println(date)

//Result
629

run을 사용하지 않았을 때의 코드는 다음과 같다.

val month =  (LocalDateTime.now().month.value).toString()
val day = (LocalDateTime.now().dayOfMonth).toString()
val date = "${month}${day}일"
println(date)

3. apply

fun <T> T.apply(block: T.() -> Unit): T { block(); return this}			
val t: T = T().apply { this.foo() }		

apply는 주로 객체를 초기화 할 때 사용된다. 수신 객체를 반환하기 때문에 블록 내에서 다른 값을 반환해야 하는 경우에 사용할 수 없다.

💡 DialogFragment 인스턴스 생성 시 apply 사용
class UserFragment() : DialogFragment() {

    var user_name: String = ""
    var user_age : Int = 0   
    ...  
}
var list = ArrayList<User>()

val dialog = UserFragment().apply {
	user_name = list[position].name
    user_age = list[position].age
}

apply를 사용하지 않았을 때의 코드는 다음과 같다.

var list = ArrayList<User>()

val dialog = UserFragment()
dialog.user_name = list[position].name
dialog.user_age = list[position].age

4. also

fun <T> T.also(block: (T) -> Unit):T{block(this); return this}			
val t: T = T().also { it.foo() }		

also는 기존 객체를 수정하거나 변경하지 않고, 데이터의 유효성을 검사하거나 디버깅, 로깅 등의 부가적인 작업을 해야할 때 사용한다. apply와 마찬가지로 수신 객체를 반환하기 때문에 블록 내에서 다른 값을 반환해야 하는 경우에 사용할 수 없다.

class Book(author: Person) {
    val author = author.also {
   	  //requireNotNull ->  
      //Throws an IllegalArgumentException if the value is null 
      requireNotNull(it.age)
      print(it.name)
    }
}

also를 사용하지 않았을 때의 코드는 다음과 같다.

class Book(val author: Person) {
    init {
      requireNotNull(author.age)
      print(author.name)
    }
}

5. with

fun <T, R> with(receiver:T,block: T.()->R): R = receiver.block()	
val r: R = with(T()) { this.foo(); this.toR() }			

with은 람다 내에서 작업을 수행한 후 마지막 라인을 반환하는데. 보통 결과가 필요하지 않은 경우에만 사용한다. run과 동일하게 작동하나, 확장함수로 사용되는 run과 달리 with은 수신객체를 파라미터로 받아 사용한다. 따라서 run이 사용되는 것이 더 가독성이 좋아서 실제로는 거의 사용되지 않는다.

class Person{
    val name = "Jisoo"
    val age = 22
}

fun main() {
    val person: Person = getPerson()
    with(person) {
        println(name)
        println(age)
    }
}

fun getPerson(): Person {
    return Person()
}

//Result
Jisoo
22

with을 사용하지 않았을 때의 코드는 다음과 같다.

fun main() {
    val person: Person = getPerson()
    println(person.name)
    println(person.age)
}

fun getPerson(): Person {
    return Person()
}

References

https://kotlinlang.org/docs/scope-functions.html
https://kotlinworld.com/255
https://docs.google.com/spreadsheets/d/1P2gMRuu36pSDW4fdwE-fLN9fcA_ZboIU2Q5VtgixBNo/edit?usp=sharing
https://medium.com/@fatihcoskun/kotlin-scoping-functions-apply-vs-with-let-also-run-816e4efb75f5
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
https://youngest-programming.tistory.com/578
https://velog.io/@ejjjang0414/%EC%BD%94%ED%8B%80%EB%A6%B0-%EB%8C%80%ED%91%9C%EC%A0%81%EC%9D%B8-%ED%91%9C%EC%A4%80%ED%95%A8%EC%88%98-let-also-apply-run-with-%EC%9D%98-%EC%B0%A8%EC%9D%B4
https://www.androidhuman.com/2016-07-06-kotlin_let_apply_run_with
https://0391kjy.tistory.com/25

profile
Software Engineer

0개의 댓글