이 글에서는 직접 간단한 DBMS 프로그램을 만들며 Kotlin DSL을 활용하는 방법을 다룹니다.
Kotlin
을 활용해, 데이터 세트에서 일정 조건을 만족하는 데이터를 반환하는 작업을 수행하는 프로그램을 만들었다고 가정하겠습니다.
우선 간단히 데이터를 가져오는 작업을 작성해보도록 하겠습니다.
// main 함수 내부
val query = QueryBuilder()
query.select(listOf("id", "name", "age"))
query.from("users")
val whereBuilder = WhereBuilder()
whereBuilder.equalsCondition("age", 28)
whereBuilder.likeCondition("name", "%Kame%")
query.where(whereBuilder.build())
일반적인 Kotlin 구문을 활용하여, 객체를 직접 생성하고 메서드를 직접 호출하는 방식으로 세부 조건을 설정해주고 있습니다. 보기에는 익숙하지만, 한 눈에 어떤 작업을 하는지 알아보기는 힘듭니다.
이를 해결하기 위해, 같은 작업을 다른 방식으로 구현해보도록 하겠습니다.
// main 함수 내부
query {
select("id", "name", "age")
from("users")
where {
"age" equals 28
"name" like "%Kame%"
}
}
첫 코드와 비교해 보았을 때, 가독성이 획기적으로 올라갔습니다.
우리가 알고있는 일반적인 Kotlin 구문의 모습과는 다르지만, 이것은 Kotlin
으로 작성한 코드입니다.
일반적인 Kotlin 구문에만 익숙한 입장이라면 위의 코드는 이질적인 동시에, 굉장히 직관적으로 느껴질 것입니다. 특히 쿼리 작업에 익숙한 사람이라면 SQL과 유사하다고 느낄 것입니다.
이렇게 직관적인 코드로 탈바꿈할 수 있었던 이유는 DSL(Domain-Specific Language)
을 만들어 적용하였기 때문입니다. Kotlin의 문법적 특성을 활용하면, 특정 도메인을 작업하는 입장에서 읽고 쓰기 좋게 새로운 표현을 만들어 구현할 수 있습니다.
DSL의 약자가 Domain-Specific Language인 만큼, Domain
이 무엇인지 이해가 필요합니다.
일반적으로 도메인
은 실생활에서의 특정 분야를 의미합니다. 상당히 포괄적인 개념인데, 어떤 문제나 일이 발생하는 모든 분야
를 도메인이라고 부를 수 있습니다. 심지어 일상적으로 하는 식사 메뉴 고민
역시 도메인이 될 수 있습니다.
흔히 '대상을 치료한다'라는 문제를 다루는 분야인 의료
도메인, '대상을 가르친다'라는 문제를 다루는 분야인 교육
도메인 등을 생각해볼 수 있습니다. 병원에 가면, 의료 도메인에 종사하는 의사와 간호사들이 의료 전문 용어를 통해 소통하며 정해진 절차에 따라 환자를 치료합니다. 교육 도메인에 종사하는 교사, 교수, 강사들도 학생들에게 지식을 전달하기 위해 교과서, 수업 자료, 시험이라는 수단을 사용합니다.
이처럼 각 도메인에는 특정 문제를 해결하기 위해 사용하는 특화된 수단이 존재함을 알 수 있습니다. 이것들은 해당 도메인에 속해있는 주체들이 문제를 해결하는 데 도움을 줍니다.
개발 역시 실생활에 포함되는 개념이기에, 개발에서도 당연히 특정 목적을 위해 문제를 정의할 수 있을 것입니다.
예를 들어 전자상거래 시스템을 개발할 때는 상품 관리, 주문 처리, 결제 시스템과 같은 문제에 직면하게 됩니다. 이 때, 개발자들은 전자상거래 시스템 개발에 특화된 규칙과 절차 등을 이용해 문제를 효율적으로 해결할 수 있을 것입니다. 주문 시스템에서는 특화된 알고리즘과 절차를, 결제 시스템에서는 결제 게이트웨이 API와 암호화 기술 등을 이용할 수 있을 것입니다.
이처럼 도메인은 특정 기능이나 요구사항을 해결하기 위한 핵심적인 역할을 합니다. 다른 여러 분야들처럼, 소프트웨어 개발에서도 해당 도메인에 특화된 도구 혹은 언어를 활용하여 그 도메인 내에서 발생하는 다양한 요구사항을 보다 쉽게 처리할 수 있습니다.
앞서 도메인
에는 문제 해결을 돕는 특화된 수단
이 존재한다고 설명했습니다.
소프트웨어 개발에서는 특정 문제를 해결하기 위한 수단들 중 하나로, 언어
가 존재합니다.
도메인 특화 언어
(Domain-specific language)는 특정한 도메인을 적용하는데 특화된 컴퓨터 언어이다. 이는 어느 도메인에서나 적용 가능한 범용 언어(General-purpose language)와는 반대되는 개념이다.위키피디아, Domain-specific language
이때의 언어는 여러 가지가 될 수 있습니다. 단순히 프로그램을 개발한다
는 포괄적인 범위의 문제를 해결하기 위한 언어뿐만 아니라, 개발 중 마주치게 되는 더 세부적인 문제를 해결
하기 위해 특화된 언어도 존재할 수 있습니다. 여기서 전자를 범용 언어, 후자를 도메인 특화 언어(DSL)라고 부릅니다.
DSL 활용의 효과
는 개발자 경험 향상을 통한 개발 과정의 효율화로 설명할 수 있습니다. 세부적인 개발 사항에서 발생하는 문제를 해결하는 데 최적화된 방식으로 설계된 언어이기 때문입니다. 사실 처음에 살펴보았던 코드를 생각해보면, 그 효과가 쉽게 와닿을 것이라 생각합니다. 일반적인 구문으로 작성한 코틀린 코드(범용 언어)와는 달리, 데이터 처리를 위한 DSL을 만드니 복잡한 데이터 처리 로직을 개발자가 쉽게 작성하고 읽을 수 있게 되었습니다.
그 외에도, 우리가 웹 페이지를 만들 때 사용하는 언어인 HTML과 CSS, 그리고 데이터베이스 작업을 위해 사용하는 SQL 역시 DSL의 예시가 될 수 있습니다. 예를 들어, HTML과 CSS는 웹 페이지의 구조와 디자인을 정의하는 데 특화되어 있으며, SQL은 데이터베이스에서 데이터를 조회하고 조작하는 데 최적화되어 있습니다.
특정 개발 상황(도메인)에서, 기존의 범용 언어로는 해결하기 힘들었던 문제를 개발자가 더 쉽게 해결할 수 있도록 하기 위하여 최적화된 문법을 제공하는 것이 DSL의 핵심입니다.
한편 DSL 도입에는 새로운 언어 학습, 프로젝트 복잡도 증가 등의 단점도 존재합니다. 따라서 DSL의 본질과 목적, 팀의 역량 등을 잘 고려하여 도입 여부를 판단하는 것이 필요합니다.
Kotlin DSL은 Kotlin 언어를 기반으로 설계하는 내부 DSL입니다.
Kotlin으로 작성한 프로그램의 일부로 동작하기 때문에, 새로운 언어를 배울 필요 없이 기존 Kotlin 문법을 기반으로 DSL을 사용할 수 있습니다. 이를 통해 DSL의 단점인 새로운 언어를 배워야 한다는 부담을 극복할 수 있습니다.
참고) DSL은 어떤
문법 구조
를 갖느냐에 따라 두 가지로 분류됩니다.①
외부 DSL
독립적인 문법 구조를 가진 DSL
ex) HTML, CSS, SQL 등②
내부 DSL
범용 언어로 작성된 DSL로, 범용 언어와 동일한 문법 사용
ex) Kotlin DSL - Gradle, Jetpack Compose 등
Kotlin의 특징들 중 하나는, 함수형 프로그래밍을 쉽게 할 수 있도록 여러 문법적인 지원을 제공한다는 점입니다. Kotlin으로 DSL을 만들면 Kotlin의 이러한 장점들을 자연스레 활용할 수 있습니다. 함수형 프로그래밍의 특성을 살려 얻을 수 있는 이점들은 다음과 같습니다.
// 명령형 프로그래밍
val query = QueryBuilder()
query.select(listOf("id", "name", "age"))
query.from("users")
val whereBuilder = WhereBuilder()
whereBuilder.equalsCondition("age", 28)
whereBuilder.likeCondition("name", "%Kame%")
query.where(whereBuilder.build())
// 선언형 프로그래밍
query {
select("id", "name", "age")
from("users")
where {
"age" equals 28
"name" like "%Kame%"
}
}
처음에 살펴보았던 두 코드의 차이가 선언형 프로그래밍의 장점을 잘 보여주고 있습니다. 범용 언어를 사용할 때는 복잡한 로직을 구현하기 위해 여러 단계의 절차적 코드를 요구하는 반면, DSL은 절차적 코드를 최소화하고 원하는 사항을 위주로 작성할 수 있도록 유도하고 있습니다. 따라서 Kotlin DSL을 활용하면 함수형, 선언형 프로그래밍의 특성을 활용해 개발자가 문제를 해결하는 데 집중할 수 있게 된다는 이점이 있다고 볼 수 있습니다.
지금부터 Kotlin
으로 간단한 DBMS 툴을 만들어보겠습니다. 이 과정에서, Kotlin에서 코드를 간결하고 직관적으로 작성할 수 있도록 하는 다양한 문법을 함께 소개하도록 하겠습니다.
본 설명에서는 예제 코드를 활용하여 실습을 진행해보겠습니다. step 별로 설명이 진행되며, 각 step에 맞는 브랜치로 이동하며 코드를 따라 작성해보시면 좋습니다. 총 4개의 step으로 구성되며, 이 글에서는 step2 까지만 설명할 예정이고 나머지는 스스로 구현해보시면 됩니다.
또한 DSL 실습을 위주로 제공되는 코드인 만큼, 세부적인 로직 구현 사항은 미리 구현되어 있으며 여기서 다루지 않겠습니다.
테이블 명 설정 -> 필드명 설정 -> 레코드 초기 설정(생략 가능)
형태로 호출되어야 한다.변경
되어도 원하는 결과를 얻을 수 있어야 한다.select
보다 from
쿼리를 먼저 호출 가능PersonFinder.kt
에서 실제로 사용자가 데이터베이스 시스템을 사용하는 코드를 정상적으로 동작시켜, 콘솔에 실행 결과를 출력할 수 있어야 합니다.
fun main() {
create {
table("Person")
attributes("name" to "string", "age" to "int")
values("Kame" x 28, "Km" x 17, "Kate" x 25)
}
select {
columns("name", "age")
from("Person")
where {
"name" like "Ka"
}
}
}
Table Person created successfully.
Table attributes : {name=STRING, age=INTEGER}
Table records : [{name=Kame, age=28}, {name=Km, age=17}, {name=Kate, age=25}]
Selection result : [{name=Kame, age=28}, {name=Kate, age=25}]
예제 코드의 step1
브랜치에서, 일부 기능이 미리 구현된 프로젝트를 확인할 수 있습니다.
프로젝트 구조는 다음과 같습니다.
QueryFunctions.kt
의 create
, select
함수들을 사용PersonFinder.kt
: dbms 모듈을 활용하여 작성된 프로그램dbms
모듈 > ui
패키지 > QueryFunctions.kt
fun create(block: TableBuilder.() -> Unit) {
// TODO 1. DatabaseManager의 메서드를 활용하여 create 메서드 완성
}
fun select(block: SelectQueryBuilder.() -> Unit): List<Map<String, Any>> {
// TODO 2. DatabaseManager의 메서드를 활용하여 select 메서드 완성
return emptyList()
}
Kotlin에서는 함수의 마지막 매개변수가 람다라면, 람다를 괄호 밖으로 전달할 수 있습니다. 아래 두 코드는 같은 역할을 수행하는데, 확실히 마지막 람다 함수를 괄호 바깥으로 빼낸 코드가 가독성이 좋아보입니다.
fun doubleNumber(n: Int, operation: (Int) -> Int): Int {
return operation(n)
}
fun main() {
val number = 5
// 내부에서 람다 정의
// 10
val result1 = doubleNumber(number, { it * 2 })
// 외부에서 람다 정의
// 10
val result2 = doubleNumber(number) { it * 2 }
}
이 특성은 DSL 스타일 코드 작성 시 가독성을 높이는 데 유용하게 활용됩니다. 특히, 두 코드 간의 종속 관계를 표현하는 탁월한 방법이 됩니다. 아래 create
함수에서도, 동작 간의 포함관계를 나타내는 데 유용하게 사용되고 있는 것을 확인할 수 있습니다.
create({
table("Person")
attributes("name" to "string", "age" to "int")
values("Kame" x 28, "Km" x 17, "Kate" x 25)
})
create {
table("Person")
attributes("name" to "string", "age" to "int")
values("Kame" x 28, "Km" x 17, "Kate" x 25)
}
dbms
모듈 > ui
패키지 > QueryFunctions.kt
fun create(block: TableBuilder.() -> Unit) {
// TODO 1. DatabaseManager의 메서드를 활용하여 create 메서드 완성
}
fun select(block: SelectQueryBuilder.() -> Unit): List<Map<String, Any>> {
// TODO 2. DatabaseManager의 메서드를 활용하여 select 메서드 완성
return emptyList()
}
dbms 모듈
> database
패키지 > DatabaseManager.kt
DatabaseManager
는 데이터베이스의 테이블을 생성하고 관리하는 핵심 객체로, 테이블 추가, 데이터 조회 등 모든 작업의 중앙 관리 지점을 제공합니다. 이 클래스는 create와 select 같은 외부 API를 내부적으로 실행하고, 데이터를 처리하는 역할을 합니다.internal object DatabaseManager {
private val tables = mutableMapOf<String, Table>()
internal fun createTable(block: TableBuilder.() -> Unit) {
val queryBuilder = TableBuilder()
queryBuilder.block()
val createdTable = queryBuilder.table()
addTable(createdTable)
}
// ...
}
이미 구현되어 있는 DatabaseManager
객체를 활용하여, 사용자 인터페이스를 구현해보겠습니다.
여기서 매개 변수 곳곳에 아래와 같은 형태의 이질적인 메서드 타입이 보입니다.
// block: TableBuilder.() -> Unit
// block: SelectQueryBuilder.() -> Unit
일반적인 함수 타입 앞에 객체명이 붙어있는 형태입니다.
이 형태를 이해하기 위해서는 수신 객체 지정 람다
의 이해가 필요합니다.
수신 객체 지정 람다
를 활용하면, 해당 람다 함수의 수신 객체를 특정 타입으로 지정할 수 있습니다. 특정 타입을 수신 객체로 지정하여, 람다 블록 내에서 해당 타입으로 선언된 객체의 메서드나 프로퍼티를 직접 사용할 수 있게 됩니다.
개인적으로는 수신 타입의 인스턴스가 있으면, 수신 객체 지정 람다 함수는 해당 타입을 가진 인스턴스가
확장 함수
처럼 쓸 수 있는 함수라고 생각하니 이해가 편했습니다. 수신 객체 지정 람다 함수를 활용하면, 궁극적으로는 외부에서 정의된 람다를 함수 내부에서인스턴스.람다함수명()
형태로 호출하는 형태로 사용할 수 있기 때문입니다.
configureFile
메서드를 정의할 때 필요한 모든 정보들을 매개변수
형태로 지정해줘야 합니다. 만약 매개변수의 개수가 많아진다면, 가독성이 떨어질 수 있을 것입니다.
class FileConfig {
var filePath: String = ""
var fileExtension: String = ""
}
fun configureFile(fileConfig: FileConfig, path: String, extension: String) {
fileConfig.filePath = path
fileConfig.fileExtension = extension
}
fun main() {
val config = FileConfig()
// 수신 객체 지정 람다 없이 객체의 속성에 접근
configureFile(config, "C:/Documents", ".txt")
println("File Path: ${config.filePath}")
println("File Extension: ${config.fileExtension}")
}
객체를 람다 블록 내에서 바로 설정할 수 있습니다. 객체를 암묵적으로 this로 전달하여, 해당 객체의 메서드나 프로퍼티를 람다 블록 내에서 간편하게 설정할 수 있어 가독성이 더 좋습니다.
class FileConfig {
var filePath: String = ""
var fileExtension: String = ""
}
fun configureFile(configure: FileConfig.() -> Unit): FileConfig {
val config = FileConfig()
config.configure() // 람다 블록 내에서 FileConfig의 프로퍼티나 메서드를 간편하게 사용
return config
}
fun main() {
// 수신 객체 지정 람다 사용
val config = configureFile { // this: FileConfig
filePath = "C:/Documents" // this.filePath = ...과 동일
fileExtension = ".txt" // this.fileExtension = ...과 동일
}
println("File Path: ${config.filePath}")
println("File Extension: ${config.fileExtension}")
}
이 형태의 람다는 객체의 메서드나 프로퍼티에 접근할 때, this 키워드를 통해 해당 객체를 참조할 수 있게 합니다. 이 람다 함수는 코드의 가독성 및 유지보수성을 높이는 데 유용하게 사용되며, DSL을 구현할 때 유용합니다.
이제 본격적으로 QueryFunctions
의 메서드를 완성시켜보겠습니다.
DatabaseManager
내부에서 block() 함수는 수신 객체 지정 람다의 형태로 사용됩니다.
이 특성을 활용해 createTable
메서드 내부에서는 tableBuilder
인스턴스를 생성하고 그 인스턴스에 넘겨받은 block() 함수를 호출하는 방식으로 TableBuilder
설정값을 쉽게 초기화할 수 있게 되었습니다. (PersonFinder.kt 참고)
Table을 초기화하기 위해 필요한 테이블명, 속성, 레코드를 명시적인 매개변수를 사용하지 않고 정의할 수 있게 된 것입니다.
internal object DatabaseManager {
private val tables = mutableMapOf<String, Table>()
internal fun createTable(block: TableBuilder.() -> Unit) {
val tableBuilder = TableBuilder()
queryBuilder.block() // 외부 람다 호출 (TableBuilder의 컨텍스트에서 실행)
// ...
}
internal fun selectRecords(block: SelectQueryBuilder.() -> Unit): List<Map<String, Any>> {
val queryBuilder = SelectQueryBuilder()
queryBuilder.block() // SelectQueryBuilder의 컨텍스트에서 실행
// ...
}
}
이렇게 구현된 DatabaseManager
를 활용해, QueryFunctions
의 create, select에 다음과 같이 구현해볼 수 있습니다.
fun create(block: TableBuilder.() -> Unit) {
DatabaseManager.createTable {
block()
println("Table ${table().name} created successfully.")
println("Table attributes : ${table().currentAttributes()}")
println("Table records : ${table().currentRecords()}")
}
}
fun select(block: SelectQueryBuilder.() -> Unit): List<Map<String, Any>> {
return DatabaseManager.selectRecords(block).also { println(it) }
}
DatabaseManager
의 메서드를 활용하여, PersonFinder로부터 넘겨받은 수신 객체 지정 람다 함수를 다시 DatabaseManager로 전달하였습니다.
또한 요구사항에 따라, 성공적으로 작업을 완료하면 결과를 출력하도록 하는 코드까지 작성하였습니다.
이어서 create
의 매개 변수인 람다 함수의 수신 객체 TableBuilder
의 메서드를 구현해보겠습니다.
create { // this: **TableBuilder**
table("Person")
attributes("name" to "string", "age" to "int")
values("Kame" x 28, "Km" x 17, "Kate" x 25)
}
class TableBuilder {
private val table = Table()
// TODO 1. table 함수 정의 - 테이블 명 설정
// TODO 2. attributes 함수 정의 - 테이블 속성 추가
// TODO 3. values 함수 정의 - 테이블에 레코드 추가
// TODO 4. x 함수 정의 - 레코드 하나에 들어가는 값들을 하나의 리스트 형태로 변환
// build()
}
Table
객체 내부의 함수를 활용하여, 쉽게 Table 객체의 이름(table)과 속성(attributes), 레코드 값들(values)을 정의할 수 있습니다.
class TableBuilder {
private val table = Table()
// TODO 1. table 함수 정의 - 테이블 명 설정
fun table(tableName: String) {
table.changeName(tableName)
}
// TODO 2. attributes 함수 정의 - 테이블 속성 추가
fun attributes(vararg attributes: Pair<String, String>) {
table.initializeAttributes(attributes.map { it.first to SupportedType.from(it.second) })
}
// TODO 3. values 함수 정의 - 테이블에 레코드 추가
fun values(vararg records: List<Any>) {
table.insertRecords(records)
}
// ...
이어서 여러 필드 값들을 하나의 리스트 형태로 만들어주는 함수를 구현해보도록 하겠습니다. 이 함수의 반환값은 테이블의 레코드로 사용될 예정입니다. 여기서는 더 나은 가독성을 위하여 함수를 정의할 수 있는 다른 방법들을 사용해보고자 합니다.
class TableBuilder {
private val table = Table()
// ...
// TODO 4. x 함수 정의 - 레코드 하나에 들어가는 값들을 하나의 리스트 형태로 변환
internal fun table(): Table {
check(table.name.isNotEmpty()) { "Table name should not be empty." }
check(table.currentAttributes().isNotEmpty()) { "Table fields should be set." }
return table
}
}
Kotlin에서는 확장 함수(Extension Function)
를 정의할 수 있습니다. 확장 함수는 기존 클래스의 코드를 수정하지 않고도 새로운 동작을 추가할 수 있는 유연함을 제공합니다. 확장 함수는 특정 클래스의 인스턴스에서 호출할 수 있는 함수를 외부에 정의하는 방식으로 구현됩니다.
fun List<Int>.sumUp(): Int {
return sum() // this.sum()과 같은 코드
}
이 함수는 수신 객체와 연결되며, 호출되는 객체에 접근할 수 있습니다. this 키워드는 생략 가능하므로, 객체를 명시하지 않고도 수신 객체의 퍼블릭 프로퍼티나 메서드에 접근할 수 있습니다. 위 예시에서는 함수 블록 내부에서 List<Int>
의 기능을 별도의 변수 선언 없이 바로 활용하고 있습니다.
확장 함수의 형태를 적절히 사용하면, 코드의 가독성 등 여러 측면에서 이득을 볼 수 있습니다.
fun List<Int>.sumUp(): Int {
return this.sum()
}
fun sumUp(nums: List<Int>): Int {
return this.sum()
}
fun main() {
val numbers = listOf(1, 2, 3, 4)
// 1. 일반 함수
sumUp(numbers)
// 2. 확장 함수
numbers.sumUp()
}
주의) 확장 함수는 특정 클래스의 멤버 메서드처럼 호출할 수 있지만, 실제로는 수신 객체의 멤버 메서드로 추가되는 것은 아님에 유의해야 합니다.
확장 함수를 활용하여, x
함수를 구현해보겠습니다.
x
함수는 다양한 필드 값을 하나의 리스트로 결합하여 리스트 형태를 반환하는 함수로, 반환된 리스트는 테이블의 레코드로 활용됩니다.
// TODO 4. x 함수 정의 - 레코드 하나에 들어가는 값들을 하나의 리스트 형태로 변환
fun Any.x(other: Any): List<Any> {
return when (this) {
is List<*> -> {
require(!this.contains(null)) { "Record should not contain null value." }
val currList = this.filterNotNull()
currList + other
}
else -> listOf(this, other)
}
}
이렇게 구현한 함수는 다음과 같이 사용될 수 있습니다.
create {
table("Person")
attributes("name" to "string", "age" to "int")
values("Kame".x(28), "Km".x(17), "Kate".x(25))
}
확장 함수를 통해 레코드의 필드값들을 하나의 리스트로 만들어주었습니다.
확장함수만을 활용하면, 하나의 레코드를 형성할 때 아래와 같이 활용할 수 있게 됩니다.
values("Kame".x(28), "Km".x(17), "Kate.x(25))
하지만 사용자는 아래와 같이 사용하기를 원합니다.
values("Kame" x 28, "Km" x 17, "Kate" x 25)
위의 방식과 비교해보았을 때 확실히 이 방식을 사용했을 때 필드 값들이 x
라는 기호로 엮여있는 것처럼 보이며 더욱 직관적인 느낌이 듭니다. Kotlin에서는 중위 함수
를 활용하면 함수명을 두 변수(수신객체와 매개변수) 사이에 끼워 활용할 수 있게 됩니다.
사실 이전 구현에서, 이미 중위 함수를 사용하고 있었습니다. 바로 속성을 정의하면서 Pair 객체를 생성할 때였습니다.
infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)
Kotlin에서는 to
라는 중위 함수를 제공하는데, 이를 통해 두 값을 간단히 Pair 객체로 묶을 수 있었습니다.
attributes("name" to "string", "age" to "int")
중위 함수를 정의하는 방법은 매우 간단합니다. 기존에 정의해 둔 확장 함수 앞에 infix
키워드를 붙이기만 하면 됩니다. 함수 호출을 위한 군더더기같은 .
및 괄호가 사라져, 더욱 읽기 좋은 코드가 되었습니다.
// infix 키워드 추가
infix fun Any.x(other: Any): List<Any> {
// ...
}
values("Kame" x 28, "Km" x 17, "Kate" x 25)
참고) infix 함수를 사용할 때 유의해야 할 두 가지
1.확장함수
에만 infix 키워드를 사용할 수 있다
2. 매개변수는1개만
정의 가능하다.
이제 테이블을 create하는 쿼리를 완성했습니다.
PersonFinder
의 create
함수 주석을 해제하면, 다음과 같은 결과를 콘솔에서 확인해볼 수 있습니다.
Table Person created successfully.
Table attributes : {name=STRING, age=INTEGER}
Table records : [{name=Kame, age=28}, {name=Km, age=17}, {name=Kate, age=25}]
Select 쿼리 역시, 같은 방식으로 DSL을 만들어볼 수 있습니다. 유사한 방식으로 구현이 가능하므로, 본 글에서 다루지는 않겠습니다. 예시 답안은 Repository의 example-answer
브랜치에 있습니다. 직접 구현해보며 DSL에 익숙해져보는 기회로 삼으시길 추천드립니다.
PersonFinder
주석을 해제했을 때, 모든 코드가 정상적으로 동작해야 한다.지금까지 DSL의 개념과 필요성을 알아보고, DSL을 활용해 간단한 프로그램을 구현해보았습니다.
지금까지 다뤄본 키워드들을 정리해보겠습니다.
DSL
가독성을 높이는 Kotlin의 Syntax
이 글에서 소개된 Kotlin의 특성들 말고도, DSL 스타일 코드를 작성할 때 유용하게 사용될 수 있는 다른 문법들이 존재합니다. 필요 시 함께 활용하면 더욱 직관적인 코드를 작성할 수 있을 것입니다.
안드로이드 개발에서 Kotlin DSL은 사실 굉장히 여러 곳에서 사용되고 있습니다. 여러 라이브러리를 사용할 때 DSL 방식으로 코드를 작성해야 하는 경우가 많기에, 개인적으로는 DSL을 다루고 싶었습니다. 특히 최근 선언형 UI 라이브러리인 Jetpack Compose가 많이 사용되는데, DSL을 잘 이해한다면 더욱 선언형 UI의 이해도가 높아지지 않을까 생각해봅니다.
라이브러리 활용 뿐만 아니라, 자체적으로 프로젝트를 할 때도 DSL이 필요한 순간이 있을 것입니다. 개발자로서 적재적소에 DSL을 도입 여부를 판단할 수 있는 능력을 가지고 싶다는 생각이 듭니다. 그러기 위해서는 코드의 가독성, 유지보수성을 크게 향상시킬 수 있을 것 같은 상황이 오게 될 때마다 시도해보며 시행착오를 겪어야 할 것 같습니다. 꾸준히 학습을 진행하면서 DSL 도입이 필요한 순간을 잘 판단할 수 있는 개발자가 되어야겠다고 다짐해봅니다.