- Scope Function의 정의
- Scope Function의 구조 및 사용목적
- Scope Function의 종류
- Scope Function의 비교 (feat. 표, 순서도)
- 특정 객체(Context Object)에 대하여, 람다를 사용해 임시 영역을 만듦으로써 코드 간결화, 메서드 체이닝 등으로 함수형 프로그래밍을 더욱 효과적으로 할 수 있게 도와준다.
- 이 영역에선 이름없이 객체에 접근할 수도 있다.
- 대표적으로 let, run, apply, also 등이 있다.
=> 공식 문서를 살펴보면 위와 같이 정의하고 있으나 곧장 와닿지 않을 수 있다. 직접 함수의 선언부를 살펴보며 분석해보고자 한다.
(1) 구조
public inline fun <T, R> T.let(block: (T) -> R): R
public inline fun <T> T.apply(block: T.() -> Unit): T
- Scope functions 중 let과 apply의 선언부이다.
- 임의의 객체(T)의 확장함수로 선언되어 있음을 알 수 있다.
- 다른 함수를 매개변수로 활용하는 고차함수의 형태임을 알 수 있다.
- apply처럼 자기 자신(T)을 반환하기도 하고, let처럼 람다식의 결과(R)를 반환하기도 한다.
(즉, 수신 타입과 반환 타입이 모두 T로 동일하기도 하고, T와 R로 구분되기도 한다.)
(2) 사용 목적
- 코드를 보다 간결하고 읽기 쉽게 만들 수 있다.
- 특정 객체에 어떤 함수를 적용하는지 알기 쉽고, 해당 함수를 적용하는 코드가 어디에서 시작해서 어디에서 끝나는지 알 수 있다.
=> 이를 확인할 구체적인 예시는, 3번 챕터에서 각각의 Scope function을 나열하며 살펴보겠다.
(1) let
public inline fun <T, R> T.let(block: (T) -> R): R
block: (T) -> R에서 볼 수 있듯이, let은 일반 함수를 인수로 받는다.- block 함수에 객체 컨텍스트(T)를 파라미터로 넘겨준다. 이렇게 넘겨진 객체는 it으로 접근 가능하다.
- 물론 it을 활용하지 않고, 임의의 네이밍으로 접근할 수도 있다.
- 그리고 반환 타입은 R로서, 람다식의 결과를 반환한다.
예시 1)
val str: String? = "Hello"; val length = str?.let { println("let() 호출 $it") }; println(length); // let 호출 Helloval str: String? = null; val length = str?.let { println("let() 호출 $it") }; println(length); // null
- let은 안전한 호출 연산자(?.) 사용이 가능하다.
- 즉, null이 아닌 값에 대해서만 코드 블록을 실행시키기 위해서 활용된다.
예시 2)
val fruits = listOf("BANANA", "MELON", "GRAPES"); val result = fruits.map { it.length } .filter { it > 5 } .let { lengths -> println(lengths) }; println(result); // [6, 6]
- 위처럼 하나 이상의 함수를 메서드 체이닝으로 연결하여 결과를 호출할 수 있다.
lengths라는 이름의 파라미터를 전달했다. 즉, it이 아닌 임의의 네이밍도 가능하다.
예시 3)
data class Person(var name: String, var age: Int) // data 클래스로val minsoo = Person("민수", 26); val result = minsoo.let { var temp = "young"; if (it.age >= 30) temp = "old"; "${it.name} is $temp"; }; println(result); // 민수 is young
- Scope 내에 지역변수를 만들어 일회성으로 활용할 수도 있다. (모든 Scope Function의 공통 기능)
- 얼마든지 반환 타입을 변형시킬 수 있다. 보통의 람다식이 그렇듯 마지막 라인이 반환 값이 된다.
(2) run
public inline fun <T, R> T.run(block: T.() -> R): R
block: T.() -> R에서 볼수 있듯이, run은 객체 컨텍스트(T)의 확장 함수를 인수로 받는다.- 따라서 객체 컨텍스트를 가리키는 키워드는 this이다. 물론 접근 시 this는 생략할 수도 있다.
- 그리고 반환 타입이 R이므로, 람다식의 결과를 반환한다.
예시 1)
val str: String? = "Hello" val length = str?.run { println("let() 호출 $this") }; println(length); // let 호출 Helloval str: String? = null val length = str?.run { println("let() 호출 $this") }; println(length); // null
- let과 마찬가지로 run도 안전한 호출 연산자(?.) 사용이 가능하다.
- 즉, null이 아닌 값에 대해서만 코드 블록을 실행시킨다.
예시 2)
data class Person(var name: String? = null, var age: Int? = null)val person = Person(); val result = person.run { name = "민수" age = 26 name }; println(result); // 민수
- this도 생략하고 곧장 프로퍼티에 접근하여 값을 세팅할 수 있다. (더욱 간결해지는 코드)
- 얼마든지 반환 타입을 변형시킬 수 있다. 보통의 람다식이 그렇듯 마지막 라인이 반환 값이 된다.
- 즉, run은 객체 초기화와 동시에 반환값의 변환이 필요할 때 사용하면 좋다.
(3) apply
public inline fun <T> T.apply(block: T.() -> Unit): T
block: T.() -> Unit에서 볼수 있듯이, apply는 객체 컨텍스트(T)의 확장 함수를 인수로 받는다.- 따라서 객체 컨텍스트를 가리키는 키워드는 this이다. 물론 접근 시 this는 생략할 수도 있다.
- 그리고 반환 타입이 자기자신 T(this)이다.
예시 1)
data class Person( var name: String? = null, var age: Int? = null, var hobby: String? = null, )val person = Person(); val hobby = "코딩"; val result = person.apply { name = "민수" age = 26 this.hobby = hobby // 반환 라인 필요 없음 }; println(result); // Person(name=민수, age=26, hobby=코딩)
- this도 생략하고 곧장 프로퍼티에 접근하여 값을 세팅할 수 있다. (더욱 간결한 코드 작성)
- 하지만 내부 프로퍼티와 외부 변수의 이름이 같을때는, 이를 구분 짓기 위해 내부 프로퍼티 접근에 this를 붙여줘야 한다.
- 반환값 및 반환타입은 항상 자기자신(this) 이다.
- 그래서 apply는 객체 초기화 시에 가장 많이 사용된다.
예시 2)
data class Person(var name: String? = null, var age: Int? = null) { fun setName(name: String): Person = apply { this.name = name } fun setAge(age: Int): Person = apply { this.age = age } }val result = Person() .setName("민수") .setAge(26); println(result); // Person(name=민수, age=26)
- 이처럼 Unit을 리턴하는 메소드를 래핑할 때도 사용하면 좋다.
(4) also
public inline fun <T> T.also(block: (T) -> Unit): T
block: (T) -> Unit에서 볼 수 있듯이, also는 일반 함수를 인수로 받는다.- block 함수에 객체 컨텍스트(T)를 파라미터로 넘겨준다. 이렇게 넘겨진 객체는 it으로 접근 가능하다.
- 물론 it을 활용하지 않고, 임의의 네이밍으로 접근할 수도 있다.
- 그리고 반환 타입이 자기자신인 T(this)이다.
예시 1)
val fruits = mutableListOf("BANANA", "MELON", "GRAPES"); val result = fruits.also { println("과일 추가 전 상태: $it") } .add("ORANGE"); println(result); /* 과일 추가 전 상태: [BANANA, MELON, GRAPES] [BANANA, MELON, GRAPES, APPLE] */
- 객체 변경 전에 단순히 상태를 확인하는 방식으로 사용한 예시다.
- 객체를 it으로 접근할 수 있다. (임의의 네이밍도 가능)
- 반환값 및 반환타입은 항상 자기자신(this) 이다.
예시 2)
data class Person(var name: String? = null, var age: Int? = null) val log = LoggerFactory.getLogger(javaClass)val result: Person = Person().also { log.debug("로깅 체크") } printlon(result) /* 16:56:39.047 [Test worker] DEBUG 클래스경로명 - 로깅 체크 Person(name=민수, age=26) */
- 기능상으론 apply처럼 객체 초기화 및 수정도 가능하지만, 보통은 객체를 전혀 사용 하지 않거나 객체의 속성을 변경하지 않고 사용하는 경우에 적합하다고 한다.
- 디버깅을 위한 로깅을 하거나, 유효성 검사 등의 추가적인 부가 작업을 하려고 할 때 주로 사용된다고 한다.
1) 비교
- Scope Function에 무엇이 있는지 살펴보았지만, 기능적으로 유사한 부분이 많기 때문에 각각의 시기적절한 활용 방법에 대해선 좀 더 경험과 연구가 필요하다.
- 다만
활용이 아닌구분에 목적이 있다면, 아래의 주요한 2가지 포인트를 통해 기억하면 쉽다.
- 객체 컨텍스트(Context Object)를 참조하는 방법 => this vs it
- 반환값(Return value) => 자기자신 vs 람다식 결과
- 더 직관적으로 인식되도록 표와 알고리즘 순서도로 정리해보았다.
2) 표
3) 순서도
=> 언제 어디서나 필요에 맞게 Scope Function을 선택할 수 있다면, 코틀린 활용이 능숙하다고 말할 수 있다. 코틀린을 자바처럼 사용하는 게 아니라 코틀린스럽게 이해하고 잘 활용하는 데 있어 Scope Function의 이해가 선행되면 유리할 것이다.
1 : https://kotlinlang.org/docs/scope-functions.html
2 : https://kotlinlang.org/docs/idioms.html