Kotlin ‐ 복잡한 객체 생성을 위해 DSL 정의를 고려하라[Effective Kotlin Item 34] - thought-corner/Backend-PlayGround GitHub Wiki

복잡한 객체 생성을 위해 DSL 정의를 고려하라

  • 코틀린 기능을 활용하여 도메인 특화 언어(DSL)를 구성할 수 있다.
  • DSL은 더 복잡한 객체나 객체의 계층 구조를 정의할 때 유용하다.
  • 정의하기는 쉽지 않지만 일단 완료되면 보일러플레이트 코드와 코드의 복잡성이 숨겨진다.
  • 그래서 개발자는 자신의 의도를 명확하게 표현할 수 있다.
val page = html {
    body {
        p {
            attr("class", "intro")
            +"안녕하세요"        // unaryPlus로 텍스트 추가
        }
        p {
            +"DSL로 만든 문단"
        }
    }
}

자신만의 DSL 정의하기

  • 함수타입에 수신 객체를 붙인 Type.() -> Unit 형태이다. 블록 안에서 그 객체가 this가 되어 멤버를 직접 부를 수 있다.
// ⭕ Good - 수신 객체 지정 람다
class TableBuilder {
    fun row(block: RowBuilder.() -> Unit) { /* ... */ }
}

// TableBuilder.() -> Unit : 블록 안에서 TableBuilder가 this
fun table(block: TableBuilder.() -> Unit): TableBuilder =
    TableBuilder().apply(block)   // apply가 수신 객체 람다를 실행하고 객체를 반환
  • apply()로 객체를 만들고 그 객체를 수신 객체로 람다를 실행한 뒤, 객체를 돌려준다. DSL 진입 함수의 표준 패턴이다.
// ⭕ Good - 연산자 오버로딩
class TextBuilder {
    var content = ""
    // +"문자열" 문법을 가능하게 함
    operator fun String.unaryPlus() {
        content += this
    }
}
  • +, in, invoke같은 연산자를 오버로딩하면 자연스러운 문법이 된다.
// ⭕ Good - 람다의 마지막 인자 관례 + 확장 함수
fun build(block: Builder.() -> Unit) { }
  • 코틀린은 함수의 마지막 인자가 람다면 괄호 밖으로 뺄 수 있다.
// ⭕ Good - @DslMarker (스코프 안전성)
@DslMarker
annotation class MyDsl
  • 중첩 시 안쪽에서 바깥 수신 객체를 실수로 부르지 않도록 막는다.

DSL은 언제 사용해야 하는지?

  • DSL을 사용하여 모든 정보를 명확하게 표현하고 구조화할 수 있다.
  • DSL을 사용하면 익숙하지 않은 사람들에게 혼란을 일으킬 수 있는데, 유지 관리까지 고려하면 더욱 그렇다.
  • 또한 DSL을 어떻게 정의하느냐에 따라 성능과 개발자의 혼란이라는 측면에서 부담이 될 수 있다.
  • DSL을 대신할 기능이 있다면 DSL은 과한 선택이다. 그러나 다음과 같은 것을 표현해야 할 때는 매우 유용하다.
    • 복잡한 데이터 구조
    • 계층구조
    • 엄청난 양의 데이터
  • DSL과 같은 구조는 빌더나 생성자를 사용해 모든 것을 표현할 수 있다.
  • DSL은 보일러플레이트 코드를 제거할 수 있는 방법 중 하나이다.
  • 반복 가능한 보일러플레이트 코드가 보이고 이를 해결할 수 있는 코틀린 기능이 없다면 DSL 사용을 고려해야 한다.