본문 바로가기
프로그래밍/kotlin

[kotlin] 코틀린 차곡차곡 - 8. Scope Function

by 사바라다 2021. 6. 28.

안녕하세요. 오늘은 코틀린 Scope Function에 대해서 알아보도록 하겠습니다.

Scope Function 란

코틀린으 표준 라이브러리에서는 Scope Function을 지원하고 있습니다. Scope Fuction은 객체 컨텍스트 내에서 코드 블럭을 실행할 수 있도록 해줍니다. 즉, Scope Fuction을 이용하면 객체의 정보를 기본적으로 가지고 있는 코드 블럭을 만들어 사용할 수 있어 간결한 코딩을 가능하게 해줍니다. 아래 예제를 보시면 좀 더 이해가 되실 것입니다.

오늘 포스팅에서 사용할 클래스의 코드는 아래와 같습니다.

class MappingService(
    private var name: String,
    private var age: Int
) {

    fun changeName(name: String) {
        this.name = name
    }

    fun incrementAge() {
        age++
    }

    override fun toString(): String {
        return "이름 = $name, 나이 = $age"
    }
}

위 클래스의 인스턴스 하나 생성 한 후 출력, 그리고 수정하여 다시 출력하는 코드를 만들어 보겠습니다. 그러면 아래 처럼 코드를 작성할 수 있습니다.

val mappingService = MappingService("sabarada", 15)
println(mappingService) // 결과 : 이름 = sabarada, 나이 = 15
mappingService.changeName("karol")
mappingService.incrementAge()
println(mappingService) // 결과 : 이름 = karol, 나이 = 16

이것을 Scope Functions를 이용하여 작성해보도록 하겠습니다. 그러면 아래와 같이 작성할 수 있습니다. 아래 예제의 it 이라는 키워드는 객체 그 자신을 가리킵니다. 이렇게 봤을 때 Scope Functions를 사용했을 때 기존의 코드에 비해서 많이 가독성이 편해진 것을 느끼실 수 있으실 것입니다.

MappingService("sabarada", 15).let {
    println(it) // 결과 : 이름 = karol, 나이 = 16
    it.changeName("karol")
    it.incrementAge()
    println(it) // 결과 : 이름 = karol, 나이 = 16
}

용어 정리

Scope Functions은 let, run, with, apply, also 5가지의 종류를 가지고 있습니다. 이 5가지의 Scope Functions는 컨텍스트 객체를 참조하는 방법, 반환 값, 확장 함수 여부 여부에 따라 각각 다르게 사용되어집니다. 3가지 요소에 대해서 한번씩 알아보도록 하겠습니다.

컨텍스트 객체를 참조하는 방법(The way to refer to the context object)

scope function의 블럭은 람다(lambda) function 입니다. 이 람다의 파라미터로 2가지를 가지고 있는데요. thisit 입니다. 해당 키워드를 통해 객체를 이용가능하게 되는 것입니다. 둘이 커버할 수 있는 커버리지가 다르지는 않으며 단지 본인의 코딩 스타일에 따라서 선택하시면 좋을 것 같습니다.

let, also가 it을 통해서 context object를 참조합니다. itthis 보다 짧으며 조금 더 읽기 쉽다는게 장점이라고 합니다. 하지만 scope function block 안에 또다른 람다가 사용되어 it 이 사용된다면 두개의 it을 구분짖기가 힘듭니다. 그러므로 단일 block에서만 사용할 때 추천드립니다.

MappingService("sabarada", 15).let {
    println(it) // 결과 : 이름 = karol, 나이 = 16
    it.changeName("karol")
    it.incrementAge()
    println(it) // 결과 : 이름 = karol, 나이 = 16
}

run, with, apply는 this를 통해서 context object를 참조합니다. 그래서 사용할 때 class 내부에 있는 함수의 형식으로 사용하실 수 있습니다. 또한 this를 생략하는 것도 가능합니다. 하지만 이럴 경우 외부 함수와 context object 함수의 구분이 어렵기 때문에 this는 붙이는것을 추천드립니다.

MappingService("sabarada", 15).run {
    println(this) // 결과 : 이름 = karol, 나이 = 16
    this.changeName("karol")
    incrementAge() // this 생략
    println(this) // 결과 : 이름 = karol, 나이 = 16
}

scope function에서 사용하고 있는 람다 내부 파라미터로 구분 지으면 아래와 같습니다.

  • this : run, with, apply
  • it : let, also

반환 값(Return value)

Scope Funtions에서는 context object를 반환또는 lambda 결과를 반환하는 scope function으로 구분 지어집니다.

apply와 also는 context object를 반환합니다. 즉, scope function을 호출 시킨 자기자신을 반환하는 것입니다. 그러므로 호출 시 call chaning을 이용할 수 있습니다. 아래 예제가 also를 call chaining을 구현한 코드입니다.

MappingService("sabarada", 15)
    .also { println(it) } // 결과 : 이름 = karol, 나이 = 16
    .also { it.changeName("karol") }
    .also { it.incrementAge() }
    .also { println(it) } // 결과 : 이름 = karol, 나이 = 16

let, run, with는 람다(lambda) 결과를 반환합니다. 즉, scope function block에서 마지막 명령어의 결과를 반환합니다. 아래 예제를 보도록 하겠습니다. also로 call chaning을 하면서 마지막에 let을 통해서 println은 반환값을 가지지 않기때문에 전체 call chaining 도 반환 값을 가지지 않도록 합니다. 불필요한 객체 반환을 막은 것입니다.

MappingService("sabarada", 15)
    .also { println(it) } // 결과 : 이름 = karol, 나이 = 16
    .also { it.changeName("karol") }
    .also { it.incrementAge() }
    .let { println(it) } // 결과 : 이름 = karol, 나이 = 16

반환 값을 기준으로 구분지으면 아래와 같습니다.

  • context object : apply, also
  • lambda result : let, run, with

확장 함수 여부(Is extension function)

마지막으로 비교해볼 수 있는 점은 확장함수로 사용하는지 아니면 별도로 선언해서 사용하는지 구분해 볼 수 있습니다. 기본적으로 scope function은 확장함수 형식으로 사용합니다. 하지만 with는 확장함수형식으로 사용하는 것이 아닌 context object를 파라미터로 받는 함수 형식으로 사용할 수 있습니다. 사용방법은 아래와 같습니다.

with(MappingService("sabarada", 15)) {
    println(this) // 결과 : 이름 = karol, 나이 = 16
    this.changeName("karol")
    this.incrementAge()
    println(this) // 결과 : 이름 = karol, 나이 = 16
}

구현체

위에서 Scope Functions의 구현이 컨텍스트 객체를 참조하는 방법, 반환 값, 확장 함수 여부에 의해서 구분되어지는 것에 대해서 알아보았습니다. 이제 각 구현체들이 어떻게 구현하고 있는지, 그리고 어디에서 사용하면 좋을지에 대해서 알아보는 시간을 가져보도록 하겠습니다.

let

letit을 파라미터로 사용하고 있습니다. 그리고 람다(lambda) 결과를 반환하는 scope function 입니다. let은 non-null 일 경우 특정 로직을 실행하도록 많이 사용합니다. 아래 예제를 보도록 하겠습니다.

val mappingService: MappingService? = createService() // nullable 한 함수 !

mappingService?.let { // mappingService가 null이 아닐 경우에만 let scope Funtion 실행
    println(it) // 결과 : 이름 = karol, 나이 = 16
    it.changeName("karol")
    it.incrementAge()
    println(it) // 결과 : 이름 = karol, 나이 = 16
}

run

runthis를 이용하여 접근이 가능합니다. 그리고 람다 결과를 반환합니다. run은 객체 환경설정과 연산된 결과를 반환하는데 사용할 수 있습니다. 또한 run을 확장함수가 아닌 단독으로 사용하여 block으로 값을 만들어 내는데도 사용할 수 있습니다.

val objectString: String = MappingService("sabarada", 15).run {
            this.incrementAge()
            this.toString()
        }

println(objectString) // 결과 : 이름 = karol, 나이 = 16
val resultString: String = run {
    val first: String = "sabarada"
    val second: String = "Karol"

    "$first is $second"
}

println(resultString) // 결과 : sabarada is Karol

with

with는 확장 함수가 아닙니다. with는 객체를 파라미터로 받습니다. lambda fucntion 내부에서는 this를 파라미터로 이용합니다. with는 collection 에서 별도의 그룹핑에 많이 사용된다고 합니다.

val mappingList: List<MappingService> =
    listOf(MappingService("sabarada", 15), MappingService("karol", 15))

val sumValue = with(mappingList) {
    var sumValue = 0
    for (value in mappingList) {
        sumValue += value.getAge()
    }
    sumValue
}

println(sumValue)

apply

apply는 this를 이용해서 해당 객체에 접근할 수 있습니다. 그리고 객체 자체를 반환하는 특성을 지니고 있습니다. 이런 apply는 주로 설정할 때 많이 사용됩니다.

val mappingService: MappingService = MappingService("sabarada", 15)
    .apply {
        this.incrementAge()
        this.incrementAge()
    }

println(mappingService) // 결과 : 이름 = sabarada, 나이 = 17

also

마지막으로 알아볼 scope function은 also 입니다. also는 it을 이용하여 객체접근이 가능하며 객체 자체를 반환합니다. 일반적으로 call chaning에 많이 사용합니다.

MappingService("sabarada", 15)
    .also { println(it) } // 결과 : 이름 = karol, 나이 = 16
    .also { it.changeName("karol") }
    .also { it.incrementAge() }
    .also { println(it) } // 결과 : 이름 = karol, 나이 = 16

정리

함수 객체 참조 방법 반환 값 확장 함수 여부
let it Lambda result Yes
run this Lambda result Yes
run this Lambda result No: called without the context object
with this Lambda result No: takes the context object as an argument.
apply this Context object Yes
also it Context object Yes

마무리

오늘은 이렇게 kotlin의 scope function에 대해서 알아보는 시간을 가져보았습니다.

감사합니다.

참조

kotlinlang_scope-function

댓글