DSL은 Domain Specification Language(영역 규정 언어)의 약자입니다. 이름만으로는 선뜻 이해하기 어렵죠. 간단히 DSL은 "언어"입니다. DSL이 언어이지만, c나 파이썬 같은 일반 프로그래밍 언어가 아니라, 응용 분야가 제한된(domain-specific) 언어입니다. 그런 언어가 있나요? HTML이나 SQL이 바로 DSL입니다. HTML은 웹 페이지를 만들 때, SQL은 데이터베이스 질의어(query)로 사용하죠. 그런데, 엄밀히 따지면 HTML이나 SQL은 여기서 다루려는 코틀린 DSL과는 차이가 있습니다. 코틀린 DSL은 주로 빌더(builder) 역할을 하기 때문입니다.
아래 예를 볼까요. HTML로 웹 페이지를 만들 때 기본 구조입니다.
<html> <head>...</head> <body> ... </body> </html> |
DSL은 코틀린 함수를 사용해 읽기 쉽고 간편하게 웹 페이지 구조를 표현할 수 있으며, 이를 실행하면 HTML로 변환합니다. 이렇게 HTML을 만드는 DSL을 HTML 빌더라고 부릅니다.
fun createWebpage() = createHTML().html { head { } body { } } |
그냥 HTML로 웹 페이지를 만들면 되는데, 이렇게까지 해서 만들 필요가 있나요? 좋은 지적입니다. DSL을 사용하면 객체 계층 구조를 선언적(declarative)으로 정의할 수 있습니다 (안드로이드 앱 개발에 사용되는 jetpack compose가 선언형 UI임에 주목하세요). DSL은 기능(function)이 아니라 구조(structure)를 나타내는 데에만 초점을 맞출 수 있습니다. 따라서, DSL은 구조를 분명히 알 수 있도록 간결하게 표현할 수 있습니다. 이 점이 실행이 주목적인 범용 프로그래밍 언어와 DSL이 다른 점입니다.
DSL을 만드는 방법은 어려울까요? 전혀 그렇지 않습니다. 확장 함수와 수신 객체를 갖는 함수 타입(1, 2, 3)을 먼저 보기 바랍니다. 이 내용만 잘 알고 있으면 충분합니다. 또 한 가지. 대상 언어의 구조적 특징을 잘 알고 있어야 합니다(HTML 빌더의 대상 언어는 HTML입니다).
■ CSV 빌더를 만들어 봅시다.
여기서는 대상 언어가 CSV인 CSV 빌더를 만들어 보겠습니다. CSV는 comma-separated values의 약자이며, 표(table) 형태의 데이터를 저장하기 위한 가장 간단한 파일 형식입니다. CSV에서 원소와 원소는 쉼표(,)로 구분합니다.
■ 일반 클래스 RowCSV
먼저 각 행(row)의 원소를 저장하기 위한 클래스 RowCSV를 만듭니다. 속성 columns는 행(row)의 원소인 열(column)을 저장하기 위해 가변 리스트 객체로 선언합니다. 원소(=열) 타입은 String입니다. 메소드 makeColumns()는 원소를 리스트 객체 columns에 추가합니다. 또, 원소를 보기 좋게 출력하기 위해 toString()을 재정의했습니다.
class RowCSV {
private val columns: MutableList<String> = mutableListOf<String>()
fun makeColumns(value: String) {
columns.add(value)
}
override fun toString(): String =
columns.joinToString(", ") { "$it" }
}
fun main() {
val csv = RowCSV()
csv.makeColumns("홍길동")
csv.makeColumns("둔갑술")
csv.makeColumns("의적")
println(csv.toString()) // 홍길동, 둔갑술, 의적
}
■ DSL로 만들기 위해 클래스 RowCSV 수정
위 코드를 보며 이게 뭐지? 생각했겠죠... 코드를 조금 고쳐 보겠습니다.
1. 클래스 RowCSV 에서 메소드 makeColumns()를 column()으로 바꿨습니다.
2. 함수 row()를 추가했습니다. 함수 row()의 형식인자 make의 타입은 확장 함수 타입입니다. 클래스 RowCSV의 객체 csvRow는 확장 함수 make()를 호출합니다.
3, main()에서 행의 원소인 열을 추가하는 방식이 위 코드와 다릅니다.
뭔가 느낌이 오나요?
class RowCSV {
private val columns: MutableList<String> = mutableListOf<String>()
fun column(value: String) {
columns.add(value)
}
override fun toString(): String =
columns.joinToString(", ") { "$it" }
}
fun row(make: RowCSV.() -> Unit): RowCSV {
val csvRow = RowCSV()
csvRow.make()
return csvRow
}
fun main() {
val myRow = row {
column("홍길동")
column("둔갑술")
column("의적")
}
println(myRow.toString()) // 홍길동, 둔갑술, 의적
}
■ 함수 row()를 간단한 코드로 수정
물론 이 코드는 아직 다 완성하지 않았습니다. 함수 row()를 설명을 위해 여러 줄로 작성했지만, 1줄로 고칠 수 있습니다.
fun row(make: RowCSV.() -> Unit): RowCSV =
RowCSV().apply(make)
혹시 지금까지 설명한 내용이 잘 이해가 안 되면 확장 함수와 수신 객체를 갖는 함수 타입(1, 2, 3)을 다시 침착하게 보기 바랍니다. 이제 CSV 빌더를 완성해 봅시다.
'코틀린' 카테고리의 다른 글
코틀린: DSL(2/2) - CSV 빌더를 만들어 봅시다. (0) | 2025.01.09 |
---|---|
4장 - (아주 쉽게 설명한) 다양한 sealed 클래스를 만들어 봅시다. (0) | 2025.01.09 |
3장 - (아주 쉽게 설명한) 함수를 람다 식으로 변환해 봅시다(2/2). (0) | 2025.01.09 |
3장 - (아주 쉽게 설명한) 함수를 람다 식으로 변환해 봅시다(1/2). (0) | 2025.01.09 |
3장 - (아주 쉽게 설명한) 조합을 계산하는 함수를 만들어 봅시다. (0) | 2025.01.09 |