코틀린 - Scope Function

Bakumando·2022년 8월 8일
post-thumbnail

0. 블로깅 목적

  • Scope Function의 정의
  • Scope Function의 구조 및 사용목적
  • Scope Function의 종류
  • Scope Function의 비교 (feat. 표, 순서도)

1. Scope Function 정의

  • 특정 객체(Context Object)에 대하여, 람다를 사용해 임시 영역을 만듦으로써 코드 간결화, 메서드 체이닝 등으로 함수형 프로그래밍을 더욱 효과적으로 할 수 있게 도와준다.
  • 이 영역에선 이름없이 객체에 접근할 수도 있다.
  • 대표적으로 let, run, apply, also 등이 있다.
    => 공식 문서를 살펴보면 위와 같이 정의하고 있으나 곧장 와닿지 않을 수 있다. 직접 함수의 선언부를 살펴보며 분석해보고자 한다.

2. Scope Function의 구조 및 사용목적

(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을 나열하며 살펴보겠다.

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 호출 Hello
val 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 호출 Hello
val 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처럼 객체 초기화 및 수정도 가능하지만, 보통은 객체를 전혀 사용 하지 않거나 객체의 속성을 변경하지 않고 사용하는 경우에 적합하다고 한다.
  • 디버깅을 위한 로깅을 하거나, 유효성 검사 등의 추가적인 부가 작업을 하려고 할 때 주로 사용된다고 한다.

4. Scope Function의 비교 (feat. 표, 순서도)

1) 비교

  • Scope Function에 무엇이 있는지 살펴보았지만, 기능적으로 유사한 부분이 많기 때문에 각각의 시기적절한 활용 방법에 대해선 좀 더 경험과 연구가 필요하다.
  • 다만 활용이 아닌 구분 에 목적이 있다면, 아래의 주요한 2가지 포인트를 통해 기억하면 쉽다.
    • 객체 컨텍스트(Context Object)를 참조하는 방법 => this vs it
    • 반환값(Return value) => 자기자신 vs 람다식 결과
  • 더 직관적으로 인식되도록 표와 알고리즘 순서도로 정리해보았다.

2) 표

3) 순서도


=> 언제 어디서나 필요에 맞게 Scope Function을 선택할 수 있다면, 코틀린 활용이 능숙하다고 말할 수 있다. 코틀린을 자바처럼 사용하는 게 아니라 코틀린스럽게 이해하고 잘 활용하는 데 있어 Scope Function의 이해가 선행되면 유리할 것이다.


5. 참고자료

1 : https://kotlinlang.org/docs/scope-functions.html
2 : https://kotlinlang.org/docs/idioms.html

profile
그렇게 바쿠만도는 개발에 퐁당 빠지고 말았답니다.

0개의 댓글