본문 바로가기
Language/Kotlin

DSL (Domain-Specific Language)이란? | feat. Kotlin DSL, build.gradle.kts 예시 코드)

by 시니성 2023. 9. 13.

DSL은 Domain-Specific Language의 약자로 특정 도메인에 특화된 프로그래밍 언어를 의미합니다. 그렇다면 여기서 도메인(domain)이란 무엇일까요? 도메인은 특정 분야나 영역을 의미하는데, 예를 들면 웹 서버 구성, 데이터베이스 쿼리, 그래픽 디자인 등이 될 수 있습니다.

1. DSL의 특징

  1. 도메인 특화: DSL은 특정 분야나 문제 영역에 특화된 기능을 제공합니다.
  2. 직관적 표현: DSL을 사용하면 도메인의 문제나 로직을 간결하고 직관적으로 표현할 수 있습니다.
  3. 생산성 향상: 도메인에 특화된 기능을 제공하므로 개발 속도와 품질을 향상시킬 수 있습니다.
  4. 제한된 표현력: 일반적인 프로그래밍 언어(GPL: General-Purpose Language)에 비해 DSL은 제한된 표현력을 가집니다. 이는 DSL이 특정 도메인의 문제만을 해결하기 위해 설계되었기 때문입니다.

2. DSL의 종류

  1. 내부 DSL (Internal DSL):
    • 기존의 일반적인 프로그래밍 언어(GPL)를 기반으로 만들어진 DSL입니다.
    • 예: Kotlin의 Anko(안드로이드 UI DSL), ScalaTest(테스트 DSL)
  2. 외부 DSL (External DSL):
    • 특정 도메인을 위해 처음부터 새로 만들어진 언어입니다.
    • 예: SQL(데이터베이스 쿼리용), HTML(웹 페이지 마크업용)

3. DSL의 예

  1. SQL: 데이터베이스 질의를 위한 DSL. SELECT, INSERT, UPDATE 등의 키워드를 사용해 데이터베이스로부터 데이터를 조회, 삽입, 수정할 수 있습니다.
  2. Gradle: 빌드와 배포 자동화를 위한 도구에서 사용하는 Groovy 혹은 Kotlin DSL. dependencies, repositories, tasks 등의 키워드로 프로젝트 구성을 선언적으로 기술합니다.
  3. CSS: 웹 페이지 스타일링을 위한 DSL. color, margin, font-size 등의 속성으로 웹 요소의 스타일을 지정합니다.
  4. Regular Expression (Regex): 문자열 검색 및 조작을 위한 DSL. 다양한 패턴을 이용하여 문자열 내의 특정 패턴을 찾거나 대체합니다.

코틀린 DSL

Gradle은 초기에 Groovy 기반의 DSL을 주로 사용했으나, 최근에는 코틀린을 기반으로 한 DSL을 제공하고 있습니다. 코틀린 DSL은 코틀린의 강력한 타입 안전성과 간결한 문법 덕분에 점점 더 인기를 얻고 있습니다.

코틀린 DSL의 장점

  1. 타입 안전성: 컴파일 시점에 오류를 잡아낼 수 있습니다.
  2. 코드 자동완성: IDE의 지원을 받아 빌드 스크립트 작성 시 자동완성 기능을 활용할 수 있습니다.
  3. 읽기 쉬운 문법: 코틀린의 간결하고 명확한 문법으로 인해 빌드 스크립트가 읽기 쉬워집니다.

build.gradle.kts 예시

// plugins: 적용할 플러그인을 지정합니다. 여기서는 'kotlin' 플러그인을 적용하였습니다.
plugins {
    kotlin("jvm") version "1.5.0"
}

// repositories: 의존성을 어디서 가져올지 지정합니다. mavenCentral을 사용하면 Maven 중앙 저장소에서 의존성을 가져옵니다.
repositories {
    mavenCentral()
}

// dependencies: 프로젝트에 필요한 의존성을 지정합니다.
dependencies {
    // 'implementation': 컴파일과 런타임 모두에 필요한 의존성입니다.
    implementation(kotlin("stdlib-jdk8"))

    // 'api': 모듈간 가시성을 제한하지 않으며, 컴파일과 런타임 모두에 필요한 의존성입니다.
    api("org.some.library:library:1.0.0")

    // 'runtimeOnly': 런타임시에만 필요한 의존성입니다. 컴파일에는 포함되지 않습니다.
    runtimeOnly("org.another.library:runtime-lib:2.0.0")
}

// tasks: 특정 작업을 정의하거나 커스터마이징합니다.
tasks {
    // 'register': 새로운 작업을 등록합니다.
    register<Copy>("copyDocs") {
        // from: 복사할 위치를 지정합니다.
        from("src/docs")
        // into: 복사할 대상 위치를 지정합니다.
        into("$buildDir/docs")
    }

    val myTask = register("myTask") {
        doLast {
            println("Executing myTask")
        }
    }

    // 'dependsOn()': 작업간 의존성을 나타냅니다. myTask가 실행되기 전에 copyDocs가 먼저 실행됩니다.
    myTask {
        dependsOn("copyDocs")
    }
}

각 키워드 설명

  • plugins: 프로젝트에 적용할 플러그인들을 지정합니다.
  • repositories: 의존성을 어디서 가져올 것인지 지정하는 곳입니다. Maven, JCenter, Google 등 다양한 저장소를 지정할 수 있습니다.
  • dependencies: 필요한 라이브러리의 의존성을 지정하는 곳입니다.
  • implementation: 프로젝트의 컴파일과 런타임 클래스 경로에 의존성을 추가합니다. 그러나 이 의존성은 해당 모듈을 사용하는 다른 모듈에는 전달되지 않습니다. 이는 캡슐화를 유지하기 위해 사용됩니다. 예를 들어, 모듈 A가 implementation을 사용하여 모듈 B에 의존하면, 모듈 C가 모듈 A에 의존할 때 모듈 B의 API를 사용할 수 없습니다.
  • api: implementation과 비슷하게 의존성을 추가하지만, 이 의존성이 해당 모듈을 사용하는 다른 모듈에도 전달됩니다. 즉, 모듈간 가시성을 제한하지 않습니다.
  • runtimeOnly: 이 의존성은 런타임에만 필요하며, 컴파일 시점에는 필요하지 않습니다. 예를 들면, 로그 라이브러리나 JDBC 드라이버와 같이 실행 시에만 필요한 라이브러리에 주로 사용됩니다.
  • tasks: Gradle 빌드 작업을 커스터마이징하거나 새로운 작업을 정의할 때 사용합니다.
  • register: tasks 내에서 새로운 작업을 등록할 때 사용하는 함수입니다.
  • dependsOn(): 특정 작업이 다른 작업에 의존하게 만듭니다. 의존하는 작업이 먼저 실행되고, 그 다음에 해당 작업이 실행됩니다.

4. DSL 설계 시 고려사항

  • 도메인 이해: DSL을 만들려는 도메인에 대한 깊은 이해가 필요합니다.
  • 간결성: DSL 사용자가 문제를 간결하게 표현할 수 있도록 해야 합니다.
  • 확장성: 도메인이 발전함에 따라 DSL도 쉽게 확장 가능해야 합니다.
  • 유지 보수: 변경이나 확장이 용이하도록 DSL을 설계해야 합니다.

결론

DSL은 특정 도메인에 특화된 언어로, 해당 도메인의 문제나 요구 사항을 효과적으로 해결할 수 있습니다. 일반적인 프로그래밍 언어와 달리 제한된 표현력을 가지지만, 그 덕분에 도메인 문제를 더 간결하고 직관적으로 표현할 수 있습니다. 따라서 도메인의 특징과 요구 사항에 따라 적절한 DSL을 선택하거나 설계하는 것이 중요합니다.