코틀린을 활용하면 DSL(Domain Specific Language)을 직접 만들 수 있습니다.
DSL은 복잡한 객체, 계층 구조를 갖고 있는 개체들을 정의할 때 굉장히 유용합니다. DSL을 만드는 것은 힘든일지만, 한 번 만들고 나면 보일러플레이트와 복잡성을 숨기면서 개발자의 의도를 명확하게 표현할 수 있습니다.
DSL은 자료 또는 설정을 표현할 때도 활용될 수 있습니다. 다음 코드는 Ktor를 활용해서 만든 API 정의 예입니다.
fun Routing.api() {
route("news") {
get {
val newData = NewsUseCase.getAcceptdNews()
call.respond(newsData)
}
get("propositions") {
requireSecret()
val newsData = NewsUseCase.getPropositions()
call.respond(newsData)
}
}
// ...
}
DSL을 활용하면 복잡하고 계층적인 자료 구조를 쉽게 만들 수 있습니다. 참고로 DSL 내부에서도 코틀린이 제공하는 모든 것을 활용할 수 있습니다. 코틀린 DSL은 type-safe이므로, 여러 가지 유용한 힌트를 활용할 수 있습니다. 이미 존재하는 코틀린 DSL을 활용하는 것도 좋지만, 사용자 정의 DSL을 만드는 방법도 알아두면 좋습니다.
사용자 정의 DSL 만들기
사용자 정의 DSL을 만드는 방법을 이해하려면, 리시버를 사용하는 함수 타입에 대한 개념을 이해해야 합니다.
함수 타입을 만드는 기본적인 방법은 다음과 같습니다.
- 람다 표현식
- 익명 함수
- 함수 레퍼런스
예를 들어 아래와 같은 함수가 있다고 가정해보면
fun plus(a: Int, b: Int) = a + b
유사 함수(analogical function)는 다음과 같은 방법으로 만듭니다.
val plus1 = (Int, Int)->Int = {a, b -> a + b}
val plus2 = (Int, Int)->Int = fun(a, b) = a + b
val plus3 = (Int, Int)->Int = ::plus
위의 예에서는 프로퍼티 타입이 지정되어 있으므로, 람다 표현식과 익명 함수의 아규먼트 타입을 추론할 수 있습니다.
함수 타입은 '함수를 나타내는 객체'를 표현하는 타입입니다. 익명 함수는 일반 적인 함수처럼 보이지만, 이름을 갖고 있지 않습니다. 람다 표현식은 익명 함수를 짧게 작성할 수 있는 표기 방법입니다.
확장 함수도 아래처럼 익명 함수로 나타낼 수 있습니다. 그리고 이를 '리시버를 가진 함수 타입'이라고 부릅니다.
(일반적으로 함수 타입과 비슷하지만, 파라미터 앞에 리시버 타입이 추가되어 있으며, 점(.)기호로 구분되어 있습니다. )
val myPlus1 = fun Int.(other: Int) = this + other
val myPlus2: Int.(Int)->Int = fun Int.(other: Int) = this + other
val myPlus2: Int.(Int)->Int = { this + it }
리시버를 가진 익명 확장 함수와 람다 표현식은 다음과 같은 방법으로 호출할 수 있습니다.
- 일반적인 객체처럼 invoke 메서드를 사용
myPlus.invoke(1, 2)
myPlus(1, 2)
1.myPlus(2)
리시버를 가진 일반 함수 타입은 코틀린 DSL을 구성하는 가장 기본적인 블록입니다. 그럼 이를 활용해서 HTML 표로 표현하는 간단한 DSL을 아래 처럼 만들어 볼 수 있습니다.
fun table(init: TableBuilder.()->Unit): TableBuilder {
val tableBuilder = TableBuilder()
init.invoke(tableBuilder)
return tableBuilder
}
class TableBuilder {
var trs = listOf<TrBuilder>()
fun tr(init: TrBuilder.() -> Unit) {
val trBuilder = TrBuilder()
init.invoke(trBuilder)
trs = trs + trBuilder
}
}
class TrBuilder {
var tds = listOf<TdBuilder>()
fun td(init: TdBuilder.() -> Unit) {
val tdBuilder = TdBuilder()
init.invoke(tdBuilder)
tds = tds + tdBuilder
}
}
class TdBuilder
fun createTable(): TableDsl = table {
tr {
for (i in 1..2) {
td {
+"This is column $i"
}
}
}
}
언제 사용해야 할까?
DSL에 익숙하지 않은 사람에게 DSL은 혼란을 줄 수 있고, DSL을 정의한다는 것은 개발자의 인지적 혼란과 성능이라는 비용이 모두 발생할 수 있습니다. 또한 유지보수에도 비용이 발생합니다. 다라서 DSL은 아래와 같은 상황일 때 유용합니다.
- 복잡한 자료 구조
- 계층적인 구조
- 거대한 양의 데이터
DSL없이 빌더 또는 생성자만 활용해도 원하는 모든 것을 표현할 수 있습니다. 하지만 DSL은 많이 사용되는 구조의 반복을 제거할 수 있게 해주고, 많이 사용되는 반복되는 코드가 있다면 이를 간단하게 만들 수 있습니다.
'Kotlin' 카테고리의 다른 글
[Effective Kotlin] 37 - 데이터 집합 표현에 data 한정자를 사용하라 (0) | 2023.10.08 |
---|---|
[Effective Kotlin] 36 - 상속보다는 컴포지션을 사용하라 (0) | 2023.10.02 |
[Effective Kotlin] 33 - 생성자 대신 팩토리 함수를 사용하라 (0) | 2023.09.10 |
[Effective Kotlin] 32 - 추상화 규칙을 지켜라 (0) | 2023.09.02 |
[Effective Kotlin] 31 - 문서로 규약을 정의하라 (0) | 2023.08.26 |