관리 메뉴

나만을 위한 블로그

[이펙티브 코틀린] 아이템 15. 리시버를 명시적으로 참조하라 본문

책/Effective Kotlin

[이펙티브 코틀린] 아이템 15. 리시버를 명시적으로 참조하라

참깨빵위에참깨빵_ 2022. 7. 18. 23:54
728x90
반응형

뭔가를 더 자세하게 설명하기 위해 명시적으로 긴 코드를 쓸 때가 있다. 대표적으로 함수, 프로퍼티를 지역 또는 톱레벨 변수가 아닌 다른 리시버로부터 가져온다는 걸 나타낼 때가 있다. 클래스의 메서드라는 걸 나타내기 위한 this가 그 예시다.

 

class User: Person() {
    private var beersDrunk: Int = 0
    
    fun drinkBeers(num: Int) {
        // ...
        this.beersDrunk += num
        // ...
    }
}

 

비슷하게 확장 리시버(확장 메서드에서의 this)를 명시적으로 참조하게 할 수도 있다. 비교를 위해 일단 리시버를 명시적으로 표시하지 않은 퀵소트 구현을 확인한다.

 

fun <T: Comparable<T>> List<T>.quickSort(): List<T> {
    if (size < 2) return this
    val pivot = first()
    val (smaller, bigger) = drop(1)
        .partition { it < pivot }
    return smaller.quickSort() + pivot + bigger.quickSort()
}

 

명시적으로 표현하면 아래와 같다.

 

fun <T: Comparable<T>> List<T>.quickSort(): List<T> {
    if (this.size < 2) return this
    val pivot = this.first()
    val (smaller, bigger) = this.drop(1)
        .partition { it < pivot }
    return smaller.quickSort() + pivot + bigger.quickSort()
}

 

두 함수 사용에 차이는 없다.

 

여러 개의 리시버

 

스코프 내부에 둘 이상의 리시버가 있는 경우 리시버를 명시적으로 나타내면 좋다. apply, with, run 함수를 사용할 때가 대표적인 예시다. 아래 코드를 확인한다.

 

class Node(val name: String) {
    fun makeChild(childName: String) =
        create("$name.$childName")
            .apply {
                print("Created $name")
            }
    
    fun create(name: String): Node? = Node(name)
}

fun main() {
    val node = Node("Parent")
    node.makeChild("child")
}

 

일반적으로 위 코드의 결과가 'Created parent.child'가 출력된다고 예상하지만 실제로는 'Created Parent'가 출력된다. 앞에 명시적으로 리시버를 붙이면 아래와 같다.

 

class Node(val name: String) {
    fun makeChild(childName: String) =
        create("$name.$childName")
            .apply {
                print("Created ${this.name}")
            }

    fun create(name: String): Node? = Node(name)
}

 

문제는 apply 함수 내부에서 this 타입이 Node?라서 이를 직접 사용할 수 없어 컴파일 에러가 난다. 이를 사용하려면 언팩하고 호출해야 한다. 이렇게 하면 일반적으로 생각하는 답이 나온다.

 

class Node(val name: String) {
    fun makeChild(childName: String) =
        create("$name.$childName")
            .apply {
                print("Created ${this?.name}")
            }

    fun create(name: String): Node? = Node(name)
}

fun main() {
    val node = Node("Parent")
    node.makeChild("child")
}

 

이건 apply의 잘못된 사용 예다. also 함수와 파라미터 name을 썼다면 이런 문제 자체가 일어나지 않는다.

also를 쓰면 이전과 마찬가지로 명시적으로 리시버를 지정하게 된다. 일반적으로 also 또는 let을 쓰는 게 nullable 값을 처리할 때 훨씬 좋은 선택지다.

리시버가 명확하지 않다면 명시적으로 리시버를 적어서 이를 명확하게 하라. 레이블 없이 리시버를 쓰면 가장 가까운 리시버를 의미한다. 외부에 있는 리시버를 쓰려면 레이블을 써야 한다. 둘 모두를 사용하는 예를 확인한다.

 

class Node(val name: String) {
    fun makeChild(childName: String) =
        create("$name.$childName")
            .apply {
                print("Created ${this?.name} in ${this@Node.name}")
            }

    fun create(name: String): Node? = Node(name)
}

fun main() {
    val node = Node("Parent")
    node.makeChild("child")
}

 

어떤 리시버를 사용하는지 의미가 더 명확해졌다. 이렇게 명확하게 작성하면 코드를 안전하게 사용할 수 있고 가독성도 향상된다.

 

DSL 마커

 

코틀린 DSL을 쓸 때는 여러 리시버를 가진 요소들이 중첩되더라도 리시버를 명시적으로 붙이지 않는다. DSL은 원래 그렇게 쓰도록 설계됐기 때문이다. 그런데 DSL에선 외부 함수를 사용하는 게 위험한 경우가 있다.

DSL 마커는 가장 가까운 리시버만을 사용하게 하거나 명시적으로 외부 리시버를 사용하지 못하게 할 때 활용할 수 있는 중요한 매커니즘이다. DSL 설계에 따라서 사용 여부를 결정하는 것이 좋으므로 설계에 따라서 사용하자.

반응형
Comments