안녕하세요.
오늘은 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 |