[TIL] Scope Function (1)

박봉팔·2024년 1월 11일
0

Scope Function 완전 정복

Scope Function에 대해 구글링을 해보면 정리해놓은 글마다 설명이 제각각이기 때문에 어떻게 사용해야하는지 혼동되기 쉽다.

또한 어떤 차이점이 있는지 정리해놓은 글은 많지만 정작 각각의 함수들이 어느상황에 어떻게 쓰이는지에 대해서는 제대로 알기가 힘들게 적어놓은 글들이 많다.

그래서 답답한 마음에 코틀린 공식 문서를 번역해서 이해해가며 정리해보았다.
(본 내용은 코틀린 공식문서의 내용을 번역한 번역본에 가까우며, 애매한 부분은 예시 코드를 보며 이해한대로 해석해서 적었다.)


Scope Function

kotlin의 표준 라이브러리에는 Object의 컨텍스트 내에서 코드들을 실행하기 위한 목적으로 만들어진 함수들이 몇가지 있다.

이러한 함수들은 람다표현식을 사용할 수 있는 객체에 대해서 호출되면 임시적인 Scope(범위)를 형성하게 된다. 이러한 Scope내에서 해당 함수를 사용한 객체의 이름을 명시하지 않아도 객체에 접근이 가능하다.

이렇게 Scope를 생성해주는 함수들을 Scope Function이라고 하며, Scope Function은 let, run, with, apply, also 총 5개가 있다.


기본적으로 이 함수들은 모두 하나의 객체에 대해서 코드블록의 내용을 실행하는 공통된 동작을 가지고있다.

차이점은 객체를 블록안에서 어떤 식으로 참조하는가코드블록이 실행되고 난 후 반환되는 결과는 무었인가이다.

Scope Function을 사용하는 예는 다음과 같다.

Person("Alice", 20, "Amsterdam").let {
    println(it)
    it.moveTo("London")
    it.incrementAge()
    println(it)
}

만약 위의 코드에서 let함수 없이 똑같이 작동하는 코드를 만들기 위해서는 변수를 새롭게 생성해야하며, 사용할 때 마다 변수명을 반복해야한다.

val alice = Person("Alice", 20, "Amsterdam")
println(alice)
alice.moveTo("London")
alice.incrementAge()
println(alice)

Scope Function은 새로운 기능이나 새로운 기술은 없지만 코드의 가독성을 더 높여준다.

Scope Function은 서로 비슷한 부분이 많아서 어떤 경우에 어떤 함수를 써야하는지 알기 힘든 경우가 많다.

어떤 함수를 사용하는게 적절한지는 전적으로 해당 프로젝트를 진행하는 팀이나 개발자 개인이 일관성있게 정하는게 적절하지만 이를 위해 기능과 차이점에 대한걸 알기위해 정리한다.


함수 선택하기

다음 표를 통해 목적에 맞는 Scope Function을 선택하는데 도움이 되었으면 좋겠다.

각 함수의 기능에 대해서는 아래에서 자세히 정리하기로 한다.

바쁜 사람들을 위해 각 Scope Function에 대한 기능을 간략하게 소개하자면

  • non-null객체에 대해서 람다 식을 실행 : let
  • Scope내에서 변수명을 따로 지정해서 사용할때 : let
  • 객체를 초기화 할때 : apply
  • 객체를 초기화 해서, 결과를 계산할때 : run
  • 표현식에 여러코드를 사용해야 할때 : 비확장 함수 run
  • 추가적인 행동이 필요할때 : also
  • 해당 객체에 대한 함수들을 그룹화 하고싶을때 : with

사실 Scope Function기능 간에 겹치는 부분이 있기때문에 프로젝트에 사용할때 규칙을 정해서 어떻게 쓸지 정하는게 가장 적절하다.

Scope Function을 사용하면 코드의 가독성이 좋아질 수 있지만 과도하게 사용할 경우 코드 가독성을 더 해칠수 있기때문에 적절하게 사용하는것이 좋다.

또한 Scope Function을 중첩해서 사용하거나 연속해서 사용할 경우 참조하고 있는 객체가 혼동 될 수 있으며, itthis에 대한 값 역시 혼동될 수 있기때문에 주의해서 사용해야한다.


함수별 차이점

Scope Function의 기능은 본질적으로 비슷하기 때문에 각 함수간의 차이점을 알아야 한다. 모든 Scope Function간에는 두 가지의 큰 차이점이 존재한다.

  • 객체를 참조하는 방식
  • 반환하는 반환 값

객체를 참조하는 방식 : this, it

객체에 Scope Function을 선언 할 경우 해당 객체는 Scope내에서 객체의 이름 대신 람다 수신자인 this나 람다 인수인 it을 사용해 참조할 수 있다.

두 경우 모두 동일안 기능을 제공하지만 각각의 장단점에 따라 적절하게 사용할 수 있는 방법이 다르기 때문에 적절한 방법을 알아보자.

fun main() { // 둘 다 기능은 똑같다.
    val str = "Hello"
    // this
    str.run {
        println("The string's length: $length")
        //println("The string's length: ${this.length}") // this.붙인거랑  안붙인거랑 똑같음
    }

    // it
    str.let {
        println("The string's length is ${it.length}")
    }
}

this

run, with, applythis를 사용해 객체를 람다 수신자로 참조한다. 그래서 해당 함수들의 블록 내에서는 클래스의 메서드 처럼 객체에 접근하는게 가능하다.

객체의 멤버에 접근할 때 this를 생략할 수 있기떄문에 코드를 더욱 간결하게 만들 수 있다. 하지만 this를 생략했을때 외부 변수와 구분이 어려워 지는 경우가 생길 수 있기때문에
주로 객체 내부의 메서드를 호출하거나, 프로퍼티에 값을 할당하는 경우에 사용하는것이 권장된다.

val adam = Person("Adam").apply {  // Person으로 생성된 객체의 프로퍼티에 값을 할당
    age = 20                       // this.age = 20 랑 똑같음
    city = "London"
}
println(adam)

it

letalso는 객체를 람다식의 인자로 참조한다. 만약 인자의 이름을 명시하지 않을 경우 기본적으로 it으로 접근할 수 있다. itthis보다 짧기 때문에 it을 사용한 코드는 좀 더 읽기 쉽다.

하지만 itthis처럼 생략이 불가능 하기 때문에 객체의 메서드나 프로퍼티에 접근할 경우 항상 it을 사용해서 객체를 지정해줘야 한다. 따라서 객체가 함수의 매개변수로 전달되거나 코드블록 내에서 여러 변수를 사용하는 경우 it을 통해 객체에 접근하는것이 더 효과적이다.


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

val i = getRandomInt()
println(i)

객체를 it이 아닌 다른 이름으로 지정할 경우 아래와 같이 작성이 가능하다.

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

val i = getRandomInt()
println(i)

반환값

Scope Function은 다른 반환 값을 가진다.

  • applyalso는 객체 자체를 반환한다.
  • let, run, with는 람가 결과값을 반환한다.

코드내에서 다음에 실행할 작업이 무언인지에 따라서 반환해야 하는 값이 달라져야 하기 때문에 이에따라 가장 적합한 Scope Function을 선택할 수 있다.


객체 반환

applyalso는 참조한 객체 자체를 반환 한다. 따라서 이 함수들은 연속적으로 함수가 호출될때 부수적으로 사용할 수 있다. 즉 동일한 객체에 대해서 여러가지 함수를 연속적으로 호출하는게 가능하다.

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() // 정렬
    //numberList를 출력할경우 apply에서 값을 추가해서 객체를 반환했고, .sort에서 정렬 됐기 때문에
    // [1.0, 2.71, 3.14]로 나온다.

또한 어떤 함수가 객체를 반환하는 경우 return값에 함께 사용할 수 있다.

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

val i = getRandomInt()

람다 결과값

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

또한 반환값은 무시한채 Scope내부에 지역 변수를 선언해 해당 변수에 대한 임시적인 Scope를 지정할 수 있다.

val numbers = mutableListOf("one", "two", "three")
with(numbers) {
    val firstItem = first()
    val lastItem = last()        
    println("First item: $firstItem, last item: $lastItem")
}

내용이 길어져 각 함수에 대한 자세한 내용은 Scope Function (2) 에서 설명하도록 하겠다.

Scope Function (2) - 보러가기


오늘은 어땠나요?

profile
개발 첫걸음! 가보자구!

0개의 댓글