DSL은 Domain Specific Language 의 약자로 특정 도메인을 처리하기 위한 특화된 언어입니다.
CSS, XML, HTML 등등 우리는 이미 여러부분에서 사용하고 있는데, 앞서 살펴본 문법적인 특성을 이용하여 코틀린 언어 자체에서 DSL 구성이 가능합니다.
일반적인 DSL(외부 DSL)은 해당 언어를 파싱하기 위한 파서가 필요합니다. json, xml 등 우리가 자주 사용하는 이러한 언어(혹은 표시방식)등도 결국 파싱을 통해 실제 내부에서 처리 하기 좋은 데이터로 변환해야 합니다. 우리는 이를 외부 DSL 이라고 부릅니다.
반대로 코틀린을 기준으로 코틀린 내부에서 실행되는 DSL 은 내부 DSL 이라고 합니다. 기본적으로 코틀린 컴파일러가 파서 역할을 해주기 때문에(실제로는 일반 자바코드와 같다고 볼 수 있습니다.) 별도의 컴파일러를 구현할 필요는 없습니다만 자유롭게 원하는 표현식을 구현하는 외부 DSL 과는 다르게 컴파일러가 파싱을 하기 때문에 코틀린에 대한 문법적 제한은 있습니다. 간단한 예로 kotlin 으로 작성한 gradle 스크립트가 있습니다.
이번 글에서는 코틀린 문법을 활용한 내부 dsl을 설계하는 법을 공유드릴 예정입니다.
내부 DSL 작성을 위한 코틀린 문법
세미콜론
코틀린은 기본적으로 문장 마지막에 세미콜론으로 구분하지 않습니다. 이러한 특성은 표현력이 강한 DSL에서는 필수로 필요한 부분입니다.
infix
중위 연산자의 도입으로 메소드를 호출하는데에 .과 괄호가 필요하지 않게 되었습니다. 이는 코드를 표현하는데에 있어 불필요한 내용을 줄이고 좀 더 가독성이 좋은 코드를 작성하는데에 도움이 됩니다.
1 | name shouldBe "sam" |
위 코드는 코틀린의 테스트 프레임워크인 kotest 의 assertion 예제중 하나입니다.
확장함수
확장함수를 통해 기존에 정의된 클래스에 원하는 형태의 메소드가 추가 가능합니다. 이는 자바기반의 라이브러리라고 하더라도 코틀린을 통해 DSL 형태로 개량할 수 있다는 이야기 입니다.
람다
코틀린에서는 함수의 마지막 파라메터가 람다일 경우 소괄호로 감쌀 필요없이 바로 람다만 선언해도 됩니다. 이는 역시 DSL 의 내용을 축약하고 가독성 좋게 표현할 수 있게 하는 요소 중 하나입니다.
그리고 중요한 점은 람다를 파라메터로 받을 때 일반 람다가 아닌 암시적 리시버를 사용해야 한다는 점입니다. 리시버를 통해 컨텍스트를 각 람다로 공유 할 수 있기 때문에, 이는 역시 DSL를 설계하는 데에 있어 중요한 역할을 합니다.
Type-safe builders
리시버를 참조한 함수들의 조합으로 빌더 함수들을 통해 빌더를 구현 할 수 있습니다. 그리고 이렇게 작성된 코드는 컴파일러에 의해 타입체크가 이루어지기 때문에 이를 type safe builder라고 합니다. 이로 인해 작성된 코드가 유효한지 검증할 필요가 없습니다.
예제
위 내용을 가지고 DSL을 작성해봤습니다.
복제 구성을 DSL 로 표현하는 코드입니다.
어떤 데이터를 동기형태로 복제하거나 비동기행태로 복제하게 정의합니다.
1 |
|
replicaMap 함수의 람다 내부에서 복제 구성을 담기 위해 mutable 한 ReplicationMapBuilder 클래스를 통해 replication map 을 먼저 구성하고 replicaMap 함수 리턴 전에 immutable 한 ReplicationMap으로 변환하여 리턴하는 형태로 구현했습니다.
ReplicationMapBuilder에서는 sync, async 메소드를 통해 동기형태로 복제 할지 비동기로 복제할지 정하게 됩니다. 이 두 메소드는 infix로 선언되어 있기 때문에 중위연산자 형태로 호출이 가능합니다. 그리고 add 라는 변수는 this 라는 객체 포인터를 참조하는데, 이는 람다 내부에서 메소드를 호출 할 때 기본적으로 리시버의 이름을 this외로 지정하기 위해 추가했습니다. 이로 인해 좀 더 명시적으로 표시되었습니다.
스코프 제어를 통한 접근 제한
내부 DSL은 별도의 파서가 필요 없이 컴파일어에 의존하여 파싱을 하게 해주는 장점이 있습니다. 다만 언어에서 제공하는 기능을 DSL에서도 사용 할 수 있기 때문에, 이런 부분에 대해 제한을 하고 싶은 경우 스코프 컨트롤 어노테이션을 이용해야 합니다.
@DslMarker 는 컴파일어에게 중첩된 람다의 부모 리시버의 멤버를 접근할 수 없게 요청합니다.
출처
다재다능 코틀린 프로그래밍