본문 바로가기

Kotlin

[Kotlin] DSL (Domain Specific Language)

served by pixabay

안녕하세요.

오늘은 DSL에 대해 알아보도록 하겠습니다.


DSL

Domain Specific Language의 약자로,

특정 도메인에 맞춘 언어 스타일로 코드를 작성할 수 있도록 Kotlin의 기능을 활용하는 것입니다.

HTML이나 SQL처럼 특정 목적을 위해 존재하는 언어입니다.


Example

DSL을 활용하면 HTML 관련 코드를 훨씬 쉽게 작성할 수 있습니다.

fun main() {
    // DSL 사용 예제
    val htmlContent = html {
        tag("head") {
            tag("title") { +"My Page Title" }
        }
        tag("body") {
            tag("h1") { +"Welcome to Kotlin DSL" }
            tag("p") { +"This is a paragraph." }
        }
    }

    // 결과 출력
    println(htmlContent.toString())
}

 

결과

위 코드의 결과물은 아래와 같이 나옵니다.

<html><head><title>My Page Title</title></head><body><h1>Welcome to Kotlin DSL</h1><p>This is a paragraph.</p></body></html>

 

이를 html 형식으로 펼쳐보면 아래와 같이 나옵니다.

<html>
    <head>
    	<title>My Page Title</title>
    </head>
    <body>
        <h1>Welcome to Kotlin DSL</h1>
        <p>This is a paragraph.</p>
    </body>
</html>

구현

위 기능이 동작하기 위해서 어떤 단계를 거쳐 구현이 되는지 하나씩 알아보겠습니다.


Step 1 : Tag 클래스 만들기

Tag 클래스는 두 가지를 가집니다.

  • 태그 이름
  • 다른 태그들
  • 텍스트 컨텐츠
class Tag(val name: String) {
    private val children = mutableListOf<Tag>() // 자식 태그 리스트
    private var text: String? = null // 태그 내부의 텍스트
}

Step 2 : tag 메서드 정의하기

tag 메서드는 현재 태그에 자식 태그를 추가할 때 사용됩니다.

init 이라는 이름의 Tag 확장함수를 인자로 받습니다.

(확장함수 설명 : https://kdr06006.tistory.com/55)

class Tag(val name: String) {
    ...
    
    fun tag(name: String, init: Tag.() -> Unit = {}) {
    	val child = Tag(name) // 자식 태그 생성
    	child.init() // 자식 태그에 확장 함수 적용
    	children.add(child) // 자식 태그를 children 리스트에 추가
    }
}

 

이 메서드를 사용하면 다음과 같은 코드로 태그를 추가할 수 있습니다

val root = Tag("html")
root.tag("head") { /* head 태그 설정 */ }

 

중괄호 부분이 람다 함수로써 init에 들어갑니다.


Step 3 : String.unaryPlus 연산자 오버로딩

HTML 태그 내에서 텍스트를 쉽게 추가할 수 있도록 String의 unaryPlus 연산자를 오버로딩 합니다.

해당 메서드를 오버로딩 하면, + 연산자를 사용할 수 있습니다.

(unaryPlus 오버로딩 : https://kotlinlang.org/docs/operator-overloading.html#unary-operations)

class Tag(val name: String) {
    ...
    
    operator fun String.unaryPlus() {
    	text = this
    }
}

 

이를 통해 HTML DSL 태그 내부에 텍스트를 다음과 같은 형태로 추가할 수 있습니다.

val paragraph = Tag("p")
paragraph.apply { +"This is a paragraph." }

 

'+' 연산자를 통해 unaryPlus 메서드를 호출하고,

text = "This is a paragraph"을 실행하게 됩니다.


Step 4 : toString 메서드 구현

태그 구조를 문자열로 변환하기 위해 toString 메서드를 구현합니다.

class Tag(val name: String) {
    ...
    
    override fun toString(): String {
    	return "<$name>${text ?: ""}${children.joinToString("")}</$name>"
    }
}

Step 5 : html 함수 정의

DSL 사용을 더 간편하게 하기 위해 html 이라는 최상위 함수를 정의합니다.

html 함수는 Tag 객체를 생성하고, DSL 스타일로 태그를 추가할 수 있습니다.

fun html(init: Tag.() -> Unit): Tag {
    val root = Tag("html") // 최상위 html 태그 생성
    root.init() // 수신 객체 지정 람다를 사용하여 초기화 람다 실행
    return root
}

전체 코드

// HTML Tag 클래스 정의
class Tag(val name: String) {
    private val children = mutableListOf<Tag>()
    private var text: String? = null

    // 자식 태그를 추가하는 메서드
    fun tag(name: String, init: Tag.() -> Unit = {}) {
        val child = Tag(name)
        child.init() // 확장 함수 사용
        children.add(child)
    }

    // 텍스트 추가 메서드
    operator fun String.unaryPlus() {
        text = this
    }

    // HTML 구조 출력
    override fun toString(): String {
        return "<$name>${text ?: ""}${children.joinToString("")}</$name>"
    }
}

// DSL을 위한 확장 함수
fun html(init: Tag.() -> Unit): Tag {
    val root = Tag("html")
    root.init() // 수신 객체 지정 람다 사용
    return root
}

fun main() {
    // DSL 사용 예제
    val htmlContent = html {
        tag("head") {
            tag("title") { +"My Page Title" }
        }
        tag("body") {
            tag("h1") { +"Welcome to Kotlin DSL" }
            tag("p") { +"This is a paragraph." }
        }
    }

    // 결과 출력
    println(htmlContent.toString())
}

다른 예제

build.gradle

안드로이드 스튜디오에 보이는 build.gradle도 Kotlin DSL을 활용한 예제입니다.

 

위에서 본 예제와 비슷한 구조로, DSL을 활용한 것을 쉽게 알 수 있습니다.

plugins {
    kotlin("jvm") version "1.5.31"
    id("application")
}

dependencies {
    implementation("org.jetbrains.kotlin:kotlin-stdlib:1.5.31")
    testImplementation("org.junit.jupiter:junit-jupiter:5.8.1")
}

MockK

MockK도 DSL 형식으로 제공되는 테스트 라이브러리 입니다.

every { car.drive(Direction.NORTH) } returns Outcome.OK
verify { car.drive(Direction.NORTH) }

'Kotlin' 카테고리의 다른 글

[Kotlin] SAM & invoke  (1) 2024.10.30
[Kotlin] 함수형 프로그래밍  (2) 2024.10.29
[Kotlin] Scope Function  (2) 2024.10.11
[Kotlin] Sealed Class  (3) 2024.09.22
[Kotlin] Extension Function (확장 함수)  (0) 2024.09.10