라이브러리를 설계하는 일은 사실 언어를 설계하는 일이다 (벨 연구소 격언)
C# 언어에는 LINQ 라는 문법이 존재한다. SQL의 Query 문과 유사한 문법으로 map/reduce/filter 를 수행 할 수 있으며, ORM (엔티티 프레임워크) 에서도 지원되서 진짜 쿼리짜듯이 C# 코드내 작성이 가능하다.
당연히 언어에 내장된 DSL 이기 떄문에, 컴파일체크도 되고, IDE 의 도움도 받을수 있고, 원하는 대로 조작할수도 있다. (함수 조합이 가능하다.)
C# 으로 일하다가 Kotlin (JAVA 계열) 로 넘어와서 느끼는 가장 불편한 점중 하나는
이런 SQL 을 위한 DSL 이 없다는점이다.
Spring boot 의 JPA 방식인 function name 으로 sql 을 생성하는 기괴한 방식은 말할것도없고.. 이런 문제를 해결하기위하여 JAVA 시절부터 자주 사용하는 라이브러리인 QueryDsl 나 JOOQ 가 있는데
- 어노테이션 기반의 메타프로그래밍의 한계.. 미리 컴파일 해야한다. 언어 내장적인 자연스러운 연결이 되지 않는다
- Java 언어에 한계내에서 구축한 DSL 이라 JAVA 식 DSL 형태를 띈다 (플루언트 API 와 자바식 Builder 패턴등)
그 이유는 Kotlin 이 워낙 JAVA 와 통합이 잘 되어 있다보니 (그게 목표이기도 하고) 그냥 JAVA 라이브러리를 고대로 가져다 쓰는 경향이 크기때문이다.
그래도 Line kotlin-jdsl 이나 JetBrains/Exposed 같이 Kotlin 을 타케팅으로 하는 DSL 이 존재하기도 하지만, 둘다 국내 Top 기업인 Line 과 Kotlin 을 만든 JetBrains 에서 만든 훌륭한 DSL 임에도 불구하고 아직까지는 QueryDSL 이 SQL DSL 을 구축할때 가장 널리 사용되는 방식인듯 하다. (필자도 회사에서는 JPA 랑 QueryDSL 사용)
어쨋든 C# 의 LINQ, Clojure 의 honeysql , Scala의 Quill 처럼 언어의 특징을 십분 활용해서 우아하게 짜는 DSL 을 찾고 있던중 발견한게 앞서 말한 [Kotlin JDSL: Kotlin을 이용해 좀 더 쉽게 JPA Criteria API를 작성해 봅시다 (linecorp.com)] 이다.
링크를 통해서 살펴보면 알수 있듯이, 어노테이션 기반 메타프로그래밍이 아니기 때문에 사용하기 훨씬 수월하다. 지원하는 Endpoint 도 많은듯 하고..
다만 다만 다만.... 한가지 아쉬운 부분이 DSL 의 형태인데, Fluent API 형태로 구축되어 있다는 것이다. 사용성만 보면 QueryDsl 과 비슷하다.
Fluent API 가 나쁜것도 아니고 세상에서 아마 가장 많이쓰이는 DSL 형태 겠지만... 필자는 Fluent API 가 DSL 이라는 것을 머리로는 이해해도 가슴속에서 인정하고 있지 않기때문에..
아마
- 기존 DSL 형태와 가장 유사한 형태를 채용함으로써 접근성을 높임
- 리플렉션과 같은 방법을 최대한 배재하여 런타임 성능을 최대화
- 일관된 DSL 형식 제공
등등.. 의 이유가 있지 않을까 싶다.
어쨋든, 이런 SQL DSL 을 보다가, 과연 어디까지 SQL 을 흉내 낼수 있을것인가 에 대한 호기심과 좀더 멋들어진 DSL 을 구축하는 다른 Kotlin 의 Feature 가 뭐가 있을까 싶어서 이것저것 실험 해 보았고
Lee-WonJun/klos: kotlin layer of sql (github.com)
요런식으로 활용해보았다.
단순하게 DSL 형태로 표현되는것에만 집중했기 때문에 현재 아무 쓰달데기 없는 레포지만
과연 Kotlin 스타일의 DSL은 어디까지 가능할것인가.. 를 내손으로 확인해보기 위한 레포지토리 이다.
확장 가능한 DSL을 구축하는 방법중 유명한 방법은 다음과 같다.
- 나만의 언어 (즉 DSL) 의 ADT 를 설계하고,
- 해당 ADT 를 구축할수 있는 Helper 함수등을 모아서 DSL Builder를 제공한뒤
- 해당 ADT 는 이제 그냥 데이터들의 집합이기 떄문에, 이를 해석하는 인터프리터를 만들어서 실제 동작을 하도록 하는것
이다
즉 [라이브러리를 설계하는 일은 사실 언어를 설계하는 일이다] 라는 벨 연구소의 격언처럼 실제 언어 만들때 사용한 방법을 거의 그대로 사용할 수 있다.
필자는 이전에 [LolChatLang] 롤챙 프로그래밍 언어를 만들어보자 (tistory.com) 에서 언어를 구축하는 것을 해봤으므로 비슷한 방식으로 ADT 를 구성 할 수 있다.
다만 DSL 표현에 대한 포스팅 이기 때문에 이번에는 인터프리터는 생성하지 않았고, DSL Builder를 구성하였다.
ADT 는 다음과 같다. (딱 눈에보이는 "select distinct * from table where cond1 and cond2" 이정도까지만 구축할수있는 ADT이다 , SQL 문 워낙 요소가 방대해서 전부 할수는 없었다 ㅜㅜ)
sealed interface Expr
sealed interface Comparable : Expr
sealed class ColumnExpr<T> : Comparable {
data class Column<T>(val col:KProperty1<T,Any>) : ColumnExpr<T>()
data class Columns<T>(val cols: List<KProperty1<T, Any>>) : ColumnExpr<T>()
data class Asterisk<T : Any>(val entity:KClass<T>) : ColumnExpr<T>()
}
data class LiteralExpr(val value: Any) : Comparable
enum class CompareOperator : Expr {
EQ, NEQ, GT, GTE, LT, LTE
}
enum class LogicalOperator: Expr {
AND, OR
}
sealed class BinaryOp : Expr {
data class Compare(val left: Comparable, val op: CompareOperator, val right: Comparable) : BinaryOp()
data class Logical(val left: BinaryOp, val op: LogicalOperator, val right: BinaryOp) : BinaryOp()
}
sealed class WhereExpr : Expr {
data class Where(val expr: BinaryOp) : WhereExpr()
object Empty : WhereExpr()
}
sealed class DistinctExpr : Expr {
object Distinct : DistinctExpr()
object All : DistinctExpr()
}
data class FromExpr<T:Any>(val entity: KClass<T>)
data class SelectExpr<T:Any>(val columns: ColumnExpr<T>, val distinct: DistinctExpr, val from: FromExpr<T>, val where: WhereExpr)
해당 ADT 를 수동으로 구축하면 다음과 같이 구출 할 수 있다.
SelectExpr<Person>(
columns = ColumnExpr.Asterisk(Person::class),
distinct = DistinctExpr.Distinct,
from = FromExpr(Person::class),
where = WhereExpr.Where(
BinaryOp.Logical(
left = BinaryOp.Logical(
left = BinaryOp.Logical(
left = BinaryOp.Compare(
left = ColumnExpr.Column(Person::name),
op = CompareOperator.EQ,
right = LiteralExpr("John")
),
op = LogicalOperator.AND,
right = BinaryOp.Compare(
left = ColumnExpr.Column(Person::age),
op = CompareOperator.GT,
right = LiteralExpr(10)
)
),
op = LogicalOperator.AND,
right = BinaryOp.Compare(
left = ColumnExpr.Column(Person::age),
op = CompareOperator.LT,
right = LiteralExpr(20)
)
),
op = LogicalOperator.OR,
right = BinaryOp.Compare(
left = ColumnExpr.Column(Person::name),
op = CompareOperator.EQ,
right = LiteralExpr("Jane")
)
)
)
)
보시다시피 ADT 를 한땀 한땀 만드는것이기 때문에 굉장히 장황하다.
아래 SQL 을 표기한것이다.
SELECT DISTINCT *
FROM Person
WHERE
(name = 'John' AND age > 10 AND age < 20)
OR name = 'Jane'
그 다음은 저런 형태의 ADT 를 구축하는것을 도와주는 Helper 를 짜면, DSL 완성이다.
DSL Builder Code 는 지금 아주 길고, 좀 억지로 구현한 부분이 있어서, 링크로만 삽입.
DSL Builder 에서는 크게 kotlin 의 labmda + receiver 기능, infix 연산자, 클래스와 함수를 적절하게.. 조합하여 구현 하였고,
위의 코드는 아래와 같이 생성 할 수 있다.
Query(Person::class) {
Select Distinct (Person::class)
From (Person::class)
Where {
((col(Person::name) `==` lit("John")) And
(col(Person::age) gt lit(10)) And
(col(Person::age) lt lit(20))) Or
(col(Person::name) `==` lit("Jane"))
}
}
와 진짜 SQL 문과 아주 유사하게 작성되어있다! 깔끔 깔금
저기서 Distinct 를 빼고 싶으면, 그냥 빼면 된다.
Query(Person::class) {
Select (Person::class)
From (Person::class)
Where {
((col(Person::name) `==` lit("John")) And
(col(Person::age) gt lit(10)) And
(col(Person::age) lt lit(20))) Or
(col(Person::name) `==` lit("Jane"))
}
}
이게 가능한 이유는 좀 트리키한 구현 방법이긴 하지만 QueryBuilder 내에 아래와 같이 구현되어 있기 때문이다.
fun Select(entity: KClass<T>) {
//블라 블라
}
val Select: SelectBuilder<T> = SelectBuilder(entity)
class SelectBuilder<T : Any>(private val entity: KClass<T>) {
infix fun Distinct(entity: KClass<T>) {
// 블라블라
}
}
Builder 내에서 Select (Person::class) 하면 그냥 (Person::class) 을 받는 function 이 불리는것이고
Select Distinct (Person::class) 하면 Select 맴버 변수를 사용해서 Infix 연산자인 Distinct 가 불리고 Distinct 가 (Person::class) 을 받는 함수가 되는것이다.
좀 고민하다가 이방법 말고는 없는것 같아서 이렇게 했다, 이름이 곂쳐서 될지 몰랐는데 되더라, Kotlin 컴파일러 가 잘 짜여있는듯..
kotin은 JAVA 와 달리 DSL 을 만드는데 필요한 Feature 가 풍부한 편 이기 때문에, 이런 방식을 십분 활용하는게 보다 자연스러운 DSL 을 구축 할수있다고 생각한다.
다만 kotlin 에서 DSL 을 만들어보며서 느낀 한계는 다음과 같다.
- 오퍼레이션 오버로딩이 가능하지만, 좀 제한적이다. 그래서 그냥 새 infix 연산자를 정의했다.
- 한계라고 하기는 좀 그렇긴 한데. < 랑 > 가 함수명으로 불가능하더라 (그래서 >= 대신 `gt=` 식으로 했다)
- 메타프로그래밍이 제한적임, Java 와 비슷하게 어노테이션정도. Macro는 없고, Moand 문법이나 빌더도 제공되지 않는다.
SQL 은 좀 방대한 DSL 이고, 내가 원하던 방식은 Monad 문법으로 커버되지 않았을것 같기는 한데, 간단한 DSL 은 Monad 문법으로 하는게 제일 심플하기 때문에 아쉬워서 적어봄.. - 커링 안됨 + 언어 style 상 당연하지만 괄호 생략 당연히 안됨 (람다/리시버인 경우만 {} 으로 대체 가능)
- HKT 안됨 (여기서 쓸 필요는 없었지만..)
- Sealed class 로 ADT 구현하는게 좀 장황한 편이다, 자바와 비교하면 선녀인데, F# 같은 ML 계열 언어가 이런건 최고인듯
여담으로 kotest 에서 컴파일되는지 안되는지를 확인하는 테스트가 되길래 사용해봣다.
'프로그래밍 언어 노트 > JAVA | Kotlin' 카테고리의 다른 글
Kotlin ServerResponse 에 대한 DSL 을 구축해보자! (0) | 2023.03.10 |
---|---|
[Spring Boot] + Kotlin 에서 Route + Beans DSL 을 사용해보자 (0) | 2023.02.25 |
Kotlin 의 Lambda 문법으로 DSL 을 구축해보자 (0) | 2022.11.18 |
[Jackson] Subtype 별 Polymorphic De/Serialization 을 제공하는 Deduction (0) | 2022.05.01 |
[JVM] Typereference 와 Type Erasure (타입 소거) (0) | 2021.12.07 |