본문 바로가기

Reading Record/이펙티브 코틀린

[이펙티브 코틀린] Item 35. 복잡한 객체를 생성하기 위한 DSL을 정의하라

아이템 35

복잡한 객체를 생성하기 위한 DSL을 정의하라

DSL을 만들려면 리시버 개념을 이해해야 한다.

val myPlus: Int.(Int) -> Int = fun Int.(other:Int) = this + other

fun main() {
    myPlus.invoke(1,2)
    myPlus(1,2)
    1.myPlus(2)
}

위 코드는 확장 함수를 나타내는 특별한 타입으로 리시버를 가진 함수 타입이다. 이처럼 리시버를 가진 함수 타입의 가장 중요한 특징은 this의 참조 대상을 변경할 수 있다는 것이다.

위 개념을 사용해 DSL을 구성해보면

fun createTable(): TableBuilder = table {
    tr {
        for (i in 1..2) {
            td {
                +"This is column$i"
            }
        }
    }
}

fun table(init: TableBuilder.() -> Unit): TableBuilder {
    ....
}

class TableBuilder {
    fun tr(init: TrBuilder.() -> Unit) {...}
}

class TrBuilder {
    fun td(init: TdBuilder.() -> Unit) {...}
}

class TdBuilder {
    var text = ""
    operator fun String.unaryPlus() {
        text += this
    }
}

위 코드는 리시버를 가진 함수타입을 활용해 dsl을 작성했다. tr,td는 this키워드가 생략되어 각 스코프 별 Bulder를 가르킨다.

DSL을 사용할때 좋은 경우

  • 복잡한 자료구조
  • 계층적인 구조
  • 거대한 양의 데이터

DSL 없이 빌더 또는 단순하게 생성자만 활용해도 원하는 모든 것을 표현할 수 있다.

DSL은 많이 사용되는 구조의 반복을 제거할 수 있게 해주고, 반복되는 코드를 간단하게 만들 수 있다.

하지만 사용자의 입장에서는 DSL이 내부적으로 어떻게 동작하는지 파악하기 어렵다. 이같은 단점을 고려해 꼭 필요한 경우에 DSL을 사용하자