[Kotlin] Scope functions

dEpayse
dEpayse_publication
8 min readAug 20, 2020

Kotlin의 Scope functions 는 어떤 문맥(context) 안에서 코드를 실행할 수 있게 해준다. ‘문맥(context)’이라는 것은 내가 원하는 객체가 코드 내에서 this 혹은 it으로 사용되는 것을 뜻한다. Kotlin에서는 with, apply, run, also, let이라는 함수들이 scope function에 해당한다. 이 함수들을 통해 코드를 간결하게 만드는 것이 가능하다. 그러나 이를 이해하기 위해서는 ‘수신 객체(receiver)’를 이해하는 것이 좋다고 생각하여, 수신 객체 지정 람다로 본 포스트를 시작하려 한다.

수신 객체(receiver)

수신 객체란 무엇일까? 우선 수신 객체라는 용어는 Kotlin의 확장 함수에서 등장한다. 수신 객체 이해를 위해 먼저 확장함수에 대해 간단히만 알아보자.

확장함수(Extension function)

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

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

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

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

(일반 함수 정의와 확장 함수에 대한 자세한 내용은

를 참고하자.)

이제 String 객체의 마지막 문자를 얻어보자.

Example1-2.정의한 확장함수의 사용
println("Kotlin".lastChar())
결과 : n

아주 편리하게 String 객체의 마지막 글자를 얻을 수 있다!

수신객체(Receiver object)

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

Fig1. 수신 객체(Receiver object)란?

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

Scope functions- with, apply, run, also, let

Kotlin에서 제공하는 scope function에는 대표적으로 with, apply, run, also, let이 있는데 이들 함수들이 코드를 간결하게 하고 유지, 보수를 쉽게 만들 수 있는 유용한 함수이기에 이 함수들의 차이점과 사용 예시를 정리해보고자 한다.

Fig2. Kotlin의 scope functions

Fig2 는 Kotlin의 scope functions 5개를 비교, 정리해놓은 표이다. scope function을 이용하면 특정 객체에 대한 작업을 블록 안에 넣어 보기 더 편한 코드를 만들고, 유지.보수를 쉽게한다. 예를 들어, 아래의 코드를 run과 with을 사용한 경우와 비교해보면 이해할 수 있다.

Example2-1. String 객체에서 여러 정보 얻기1(scope function 미사용)
val s = "Hello Kotlin!"
println(s.length)
println(s.first())
println(s.last())
Example2-2.String 객체에서 여러 정보 얻기2(with 사용)
val s = "Hello Kotlin!"
with(s){
println(length)
println(first())
println(last())
}
Example2-3. String 객체에서 여러 정보 얻기3(run사용)
val s = "Hello Kotlin!"
s.run{
println(length)
println(first())
println(last())
}

또는 아래의 코드를 apply를 사용한 경우와 비교해보자.

class Person(val name:String="", val age:Int =-1)Example3-1. 커스텀 객체 초기화 하기1(scope function 미사용)
val p:Person = Person()
p.name = "Vincent Van Gogh"
p.age = 30
Example3-2. 커스텀 객체 초기화 하기2(apply 사용)
val p:Person = Person()
p.apply{
name = "Vincent Van Gogh"
age = 30
}

물론 Example3에서 Person 클래스의 생성자를 통해 초기화하여 간단히 할 수 있지만, apply의 사용을 보여주기 위한 예시이다. 이와 같이 scope function를 이용하면 코드를 더 보기 좋고 간결하게 작성할 수 있다!

scope function 동작 이해

scope function의 쓰임을 이해했지만, 코드 내부가 어떻게 동작하는 지는 아직 어려울 수 있다. Fig3을 참고하여 차근차근 살펴보자.

Fig3. scope function 동작 이해

Fig3 은 scope function의 with과 also만을 설명하고 있지만, 다른 함수들의 개념 이해도 포함되어 있기 때문에 차근차근 따라가보면 scope function이 어떻게 동작하는지 이해할 수 있을 것이다! with, apply, run은 인자인 block 함수의 수신 객체가 정해졌다고 하여 ‘수신 객체 지정 람다(lambda with receiver)’라고도 볼 수 있다.

또한 apply와 also의 차이는 람다 안에서 context object가 this로 쓰이냐, it으로 쓰이냐 뿐이고, run과 let의 차이도 마찬가지이다. 결국 scope function은 context object를 정해 그 객체를 한 블럭 안에서 다룰 수 있고 객체의 변수명을 간결하게 혹은 가독성이 좋게 만들어 코드의 유지 보수를 용이하게 한다.

scope function이 이해가 됐다면, Kotlin의 scope function을 이용할 때도 더 원활한 이용이 가능할 것이라고 생각한다.

Reference

Overall part

  1. Kotlin documentshttps://kotlinlang.org/docs/reference/
  2. Dmitry Jemerov and Svetlana Isakova. (2017). Kotlin in Action. USA: Manning

수신 객체 지정 람다 part

3. [커니의 안드로이드 이야기] ‘코틀린의 유용한 함수들-let, apply, run, with’ — https://www.androidhuman.com/lecture/kotlin/2016/07/06/kotlin_let_apply_run_with/

4. [stack overflow] ‘What does a Kotlin function signature with T.() mean?’ — https://stackoverflow.com/questions/33190613/what-does-a-kotlin-function-signature-with-t-mean

--

--

dEpayse
dEpayse_publication

나뿐만 아니라 다른 사람들도 이해할 수 있도록 작성하는, 친절한 블로그를 목표로.