그레들DSL은 어떤것을 사용해야 할까?

반응형

최근에 입사한 회사에서 새로운 프로젝트를 도입하려 하였습니다. 기존의 단일 모듈 기반 controller - service - repository 구조가 아닌 멀티 모듈 구조로 도입하기로 결정하였습니다. 멀티 모듈로 결정한 이유는 역할별 책임을 분리하여 이후 변경 범위를 줄이고 유지보수를 용이하게 만들기 위함입니다. 또한 도메인과 인프라를 분리함으로써 구조적인 의존성을 조금 더 명확하게 관리할 수 있다고 여겨졌습니다.

빌드 툴은 메이븐이 아닌 그레들로 결정하였습니다. 그레들로 결정한 이유는 확장성과 유지보수성 때문입니다. 특히 멀티 모듈 환경에서는 공통 설정 관리와 유연한 빌드 구성이 중요하다고 판단하였고, 이러한 부분에서 그레들이 조금 더 적합하다고 느껴졌습니다.

그레들로 결정하고 보니 메이븐처럼 XML 하나로 관리되는 방식과 달리, 그레들은 빌드 스크립트를 작성하는 방식으로 코틀린 DSL과 그루비 DSL이 존재하였습니다. 그렇다면 어떤 방식이 좋고, 어떤 기준으로 결정해야 할까요?

이번 글에서는 그레들에서 제공하는 코틀린 DSL과 그루비 DSL을 비교해보며 어떤 방식이 조금 더 적절한지 고민해보겠습니다.

코틀린 DSL? 그루비 DSL? 왜 구분을 지을까?

들어가기 앞서 그레들은 어째서 DSL을 두 가지나 지원하는지에 대해 고민해봅시다.

기존에는 그루비 DSL만 존재하였습니다. 하지만 어느 순간부터 코틀린 DSL이 추가되었습니다. 그렇다면 어째서 그레들은 코틀린 DSL을 추가하였을까요? 단순히 새로운 기술이 등장했기 때문만은 아니라고 여겨집니다.

기존 그루비 DSL은 동적 타입 기반이다 보니 자유롭게 작성할 수 있다는 장점이 존재하였습니다. 하지만 반대로 IDE 자동완성이나 타입 안정성과 같은 부분에서는 아쉬움이 존재하였습니다.

반면 코틀린 DSL은 정적 타입 기반으로 동작합니다. 그렇기 때문에 IDE 지원이나 자동완성, 컴파일 단계에서의 검증과 같은 부분에서 조금 더 안정적으로 사용할 수 있습니다.

그렇다면 얼마나 효과가 있을까요?

확인해보니 코틀린 DSL이 무조건적으로 좋은 것은 아니었습니다. 오히려 빌드 성능 자체는 그루비 DSL이 유리한 경우도 존재하였습니다.대표적으로 Gradle 공식 이슈에서는 실제 성능 비교 자료를 공개한 적이 있습니다. 해당 자료는 Gradle 8.1 기준으로 작성된 결과이며, 현재 버전과는 약간의 차이가 존재할 수 있습니다..

https://github.com/gradle/gradle/issues/15886#issuecomment-1432923669

아쉽게도 현재 최신 버전인 Gradle 9.X 기준으로 직접 비교한 공식 성능 자료는 공개되어 있지 않습니다.
다만 Gradle 공식 자료에서는 코틀린 DSL recompilation avoidance와 관련하여 일부 상황에서 약 2.5배 정도 개선되었다고 설명하고 있습니다. (그루비 DSL와 코틀린DSL의 비교가 아닙니다. Gradle 8.2 -> Gradle 9.0에 대한 개선입니다.) 

 

Gradle | What's new in Gradle 9.0.0

Gradle 9.0.0 is a new major release which incorporates many features since the version 8.0. It makes Gradle configuration cache the preferred execution mode, provides more clean and actionable error reporting, and also updates Gradle to Kotlin 2 and Groovy

gradle.org

그럼에도 불구하고 현재도 순수 빌드 성능만 보면 상황에 따라 그루비 DSL이 조금 더 유리한 경우가 존재한다고 합니다.

그렇다면 어째서 많은 기업들이 코틀린 DSL을 선택하는 것일까요?

대표적으로 IDE 자동완성, 타입 안정성, 리팩토링 안정성, 대규모 멀티 모듈 유지보수와 같은 부분 때문이라고 합니다. 실제로 Gradle 역시 8.2 버전부터 코틀린 DSL을 기본 생성 방식으로 변경하였습니다.

즉 핵심은 단순합니다.

코틀린 DSL은 빌드 성능을 극적으로 개선하기 위한 목적보다는, 조금 더 안정적이고 유지보수하기 쉬운 빌드 스크립트를 만들기 위해 등장하였습니다.

정리하자면, 상황에 따라 빌드 성능은 그루비 DSL이 유리한 경우도 존재하였습니다. 반면 코틀린 DSL은 IDE 지원과 타입 안정성을 기반으로 유지보수에 조금 더 초점을 맞춘 방식이라고 느껴졌습니다.

그렇다면 실제로는 어떤 차이가 존재할까요?

개념적으로 보면 유지보수성과 타입 안정성이라는 차이로 정리할 수 있었습니다. 하지만 단순히 설명만으로는 크게 와닿지 않았습니다. 아무래도 실제 코드로 확인하지 못했기 때문이라고 여겨졌습니다. 이번에는 조금 더 문법적인 부분에서 차이를 확인해보겠습니다.

문법적인 차이는?

위에서 그루비 DSL과 코틀린 DSL을 비교하였을 때, 자동완성, 타입 안정성, 리팩토링 안정성, 대규모 멀티 모듈 유지보수와 같은 부분들이 코틀린 DSL의 강점이라고 하였습니다.

하지만 자동완성의 경우에는 IDE의 발전으로 인해, 이제는 코틀린 DSL만의 강점이라고 말하기에는 애매하다고 느껴졌습니다.
실제로 그루비 DSL 역시 IDE에서 자동완성을 어느 정도 지원하고 있기 때문입니다.
그렇다면 코틀린 DSL의 장점은 어디에서 더 명확하게 드러날까요?

개인적으로는 타입 안정성, 리팩토링 안정성, 그리고 대규모 멀티 모듈 유지보수 측면에서 차이가 조금 더 드러난다고 느껴졌습니다.

그루비 DSL은 동적 타입 기반이기 때문에 유연하게 작성할 수 있다는 장점이 존재합니다. 하지만 그만큼 잘못된 설정이 작성되었을 때 실행 시점에서 확인되는 경우가 존재할 수 있습니다.

반면 코틀린 DSL은 정적 타입 기반으로 동작하기 때문에, 잘못된 설정을 작성했을 때 IDE나 컴파일 단계에서 더 빠르게 확인할 수 있습니다. 또한 타입 기반으로 코드 탐색이나 리팩토링이 가능하기 때문에, 모듈 수가 많아지고 설정이 복잡해질수록 유지보수 측면에서 조금 더 안정적으로 사용할 수 있다고 여겨졌습니다.

코틀린 DSL의 타입 안정성

플러그인 선언

그루비 DSL은 플러그인을 지정할 때 id를 이용하여 문자열 형태로 등록합니다. 
반면 코틀린 DSL은 다음과 같이 바로 사용할 수 있습니다.

처음에는 단순히 문법 차이처럼 보였습니다. 하지만 자세히 보면 코틀린 DSL의 java는 단순 문자열이 아니라 타입 안전한 accessor로 동작합니다. 즉 문자열 기반으로 처리되는 것이 아니라, IDE와 Gradle이 인식할 수 있는 형태로 제공된다는 차이가 존재하였습니다.
그렇기 때문에 잘못된 값을 사용하거나 존재하지 않는 설정을 작성했을 때 IDE나 컴파일 단계에서 조금 더 빠르게 확인할 수 있습니다.

java toolChain

이 부분에서 코틀린 DSL의 장점이 드러난다고 합니다. 그루비 DSL에서는 다음과 같이 설정할 수 있습니다.

반면 코틀린 DSL은 다음과 같이 작성합니다.

처음 보았을 때는 오히려 코틀린 DSL쪽이 더 복잡하게 느껴졌습니다. set()과 같은 코드가 추가되다 보니 뭔가 덕지덕지 붙은 느낌도 존재하였습니다. 그렇다면 어째서 이것이 코틀린 DSL의 장점이라고 말하는 것일까요?

가독성만 보면 오히려 그루비 DSL이 더 좋아 보였습니다. 하지만 차이는 값을 어떻게 다루는지에 존재하였습니다.

그루비 DSL은 동적 타입 기반이다 보니 비교적 자유롭게 값을 설정할 수 있습니다. 반면 코틀린 DSL은 languageVersion이 어떤 타입인지 알고 있으며, 정해진 API를 통해 값을 설정하도록 구성되어 있습니다.

즉 단순히 문법이 길어진 것이 아니라, 그레들 API를 타입 기반으로 조금 더 안전하게 다루도록 변경된 것입니다.

gradle api

즉, 코틀린 DSL은 단순 문자열 기반으로 동작하는 것이 아니라, Gradle API를 타입 기반으로 직접 활용하고 있다는 것을 알 수 있습니다.

Task 타입 지정

간략하게 task가 어떤것인지 부터 이해를 해봅시다. 

그레들에서 Task는 하나의 작업 단위를 의미합니다. 쉽게 말해 Gradle이 수행하는 기능 하나하나를 Task라고 볼 수 있습니다.
예를 들어 프로젝트 컴파일, 테스트 실행, JAR 생성, 애플리케이션 실행등과 같은 작업들이 모두 Task로 동작합니다.
실제로 build 역시 내부적으로 여러 Task를 실행하면서 동작합니다. 
즉, 그레들은 단순히 코드를 빌드하는 도구가 아니라, 여러 작업(Task)을 조합하여 실행하는 구조라고 볼 수 있습니다. 

그루비 DSL에서는 다음과 같이 작성합니다.

테스트 실행 환경 공통 설정

반면 코틀린 DSL은 다음과 같이 작성합니다.

테스트 실행 환경 공통 설정
 

처음 보았을 때는 단순히 문법 차이처럼 느껴졌습니다. 하지만 코틀린 DSL은 Test 타입의 task만 대상으로 지정하고 있다는 차이가 존재하였습니다. 즉, Gradle이 현재 블록이 Test 타입이라는 것을 명확하게 알고 있기 때문에, 해당 타입에서 사용할 수 있는 API들을 IDE가 조금 더 안정적으로 인식할 수 있었습니다.
결국 코틀린 DSL은 단순히 문법이 다른 것이 아니라, Gradle API를 명확한 타입 기반으로 직접 다루고 있다는 특징이 존재하였습니다.

의존성 선언은 타입 안정성

이 부분은 다른 분들과 달리 타입 안정성이 떨어지는 편입니다. 왜냐하면

각기 다른 프로젝트입니다.

여전히 문자열 기반으로 의존성을 선언하고 있다는 점도 확인할 수 있었습니다. 그렇다는 것은 의존성 이름이나 버전에 오타가 발생하더라도 컴파일 단계에서 바로 확인되는 것이 아니라, 의존성을 해석하는 시점에서 실패할 수 있다는 의미입니다.

물론 이를 보완하기 위한 다양한 방법들도 존재하였습니다. 하지만 이번 글은 코틀린 DSL과 그루비 DSL의 차이를 확인하는 것이 목적이기 때문에, 해당 내용은 여기까지만 보고 넘어가도록 하겠습니다.

코틀린 DSL이 타입 안정성 측면에서 어떤 장점이 있는지 살펴보았습니다. 그렇다면 대규모 멀티 모듈 유지보수 측면에서는 어떤 점이 더 좋을까요?

대규모 멀티 모듈 유지보수 측면에서는 어떤점이 더 좋을까?

위에서 그루비 DSL은 동적 타입 구조라고 설명하였습니다.

반면 코틀린 DSL은 정적 타입 기반으로 동작합니다. 즉, Gradle 설정을 타입 기반으로 관리할 수 있다는 의미입니다.
그렇기 때문에 프로젝트 규모가 커지고 멀티 모듈 환경처럼 공통 설정과 build logic이 증가할수록, IDE 지원이나 리팩토링, 설정 관리 측면에서 조금 더 안정적으로 유지보수할 수 있다고 여겨졌습니다. 

하지만 타입이 정적이라고 해서 대규모 프로젝트에 무조건 적합하다고 보기는 어려워 보였습니다. 언어 관점에서 보면 일반적으로 동적 타입 언어보다 정적 타입 언어가 대규모 환경에서 유지보수 측면에 조금 더 유리하다고 알려져 있습니다. 컴파일 단계에서 타입 오류를 조금 더 빠르게 확인할 수 있고, IDE 지원이나 리팩토링 역시 조금 더 안정적으로 동작하기 때문입니다.

실제로 Kotlin 공식 문서에서도 정적 타입 기반 구조가 IDE 리팩토링과 자동완성 지원에 도움을 준다고 설명하고 있습니다.


 

Gradle best practices | Kotlin

 

kotlinlang.org



또한 Gradle 공식 문서에서는 Kotlin DSL이 기존 그루비 DSL의 동적 접근 방식 대신, type-safe model accessor를 제공한다고 설명하고 있습니다.

 

Gradle best practices | Kotlin

 

kotlinlang.org

다만, 이것이 빌드 툴에서도 완전히 동일하게 적용되는지는 아직 확실하지 않았습니다. 실제로 빌드 성능만 보면 상황에 따라 그루비 DSL이 조금 더 유리한 경우도 존재하였기 때문입니다.

또한 실제 대규모 멀티 모듈 환경에서도 여전히 그루비 DSL을 사용하는 사례들 역시 존재하였습니다. 결국 어떤 방식이 무조건적으로 정답이라기보다는, 프로젝트 규모나 팀의 경험, 유지보수 방향에 따라 선택이 달라질 수 있다고 여겨졌습니다.

아쉽게도 어떤 방식을 더 선호하는지에 대한 명확한 통계 자료는 찾아볼 수 없었습니다. 하지만 여러 사례들을 종합해보면, 코틀린 DSL을 선호하는 입장과 그루비 DSL을 선호하는 입장 두 가지 모두를 확인할 수 있었습니다.
실제로 Gradle 공식 블로그에서는 신규 프로젝트에 대해 Kotlin DSL을 기본 생성 방식으로 변경하였다고 설명하고 있습니다.

 

Kotlin DSL is Now the Default for New Gradle Builds

Kotlin DSL is Now the Default for New Gradle Builds Calendar April 13, 2023 Introduction Kotlin DSL for Gradle was introduced in version 3.0 of the Gradle Build Tool in August 2016 and released as 1.0 in Gradle 5.0. Since then, it’s been growing in popul

blog.gradle.org

반면 복잡한 build logic을 가진 대규모 레거시 프로젝트에서는 마이그레이션 비용이나 기존 생태계 문제로 인해, 여전히 그루비 DSL을 유지하는 경우도 존재한다는 것을 확인할 수 있었습니다.

즉 실제 현업에서는 단순히 "무조건 코틀린 DSL이 좋다"기보다는, 신규 프로젝트에서는 코틀린 DSL을 선택하는 흐름이 증가하고 있었고, 대규모 레거시 프로젝트에서는 여전히 그루비 DSL을 유지하는 사례들도 존재하였습니다.

거두 절미하고 빌드 툴을 사용하는 이유가 무엇일까?

대표적으로 외부 라이브러리나 프레임워크를 가져오고, 프로젝트를 빌드하고 실행하기 위함입니다. 결국 build.gradle 역시 의존성을 관리하고 프로젝트를 실행하기 위한 설정 파일이라고 볼 수 있습니다. 그렇다면 외부에서 정보를 가져오는 작업에서도 정적인지 동적인지가 그렇게까지 중요한 것일까요?

개인적으로는 이 부분에서 조금 의문이 들었습니다. 실제로 의존성 선언만 보더라도 여전히 문자열 기반으로 작성되는 부분이 존재하였기 때문입니다.

즉 코틀린 DSL이라고 해서 모든 부분이 완전한 타입 안정성을 가지는 것은 아니었습니다.

오히려 실제 차이는 단순 의존성 선언보다는, 공통 build logic이나 task 설정, 멀티 모듈 환경에서의 설정 관리 측면에서 조금 더 드러나는 것으로 보였습니다. 결국, 빌드 툴 관점에서는 단순히 정적 타입인지 동적 타입인지 자체보다, 프로젝트 규모가 커졌을 때 build logic을 얼마나 안정적으로 관리할 수 있는지가 더 중요한 부분처럼 느껴졌습니다.

마무리

정확하게 어떤 DSL을 사용하는 것이 무조건 더 좋다는 내용은 찾을 수 없었습니다. 다만 여러 자료들을 확인해보면서, 단순히 문법 차이가 아니라 어떤 방향으로 build logic을 관리할 것인지에 대한 고민이 담겨있다는 것을 느낄 수 있었습니다.

기존에는 그루비 DSL만 사용하였습니다. 그러다 코틀린 DSL 역시 알게 되었고, 어떤 DSL을 사용하는 것이 더 적절한지에 대해 여러 자료들을 찾아보게 되었습니다.

실제로 현재 흐름을 보면 신규 프로젝트에서는 코틀린 DSL을 사용하는 사례가 증가하고 있었고, 레거시 프로젝트에서는 여전히 그루비 DSL을 유지하는 경우도 존재하였습니다. 하지만 이 역시 프로젝트 규모나 팀 경험, migration 비용 등에 따라 선택이 달라질 수 있다고 느껴졌습니다.

또한 성능만 보면 상황에 따라 그루비 DSL이 조금 더 유리한 경우도 존재하였습니다. 반면 코틀린 DSL은 정적 타입 기반 구조를 통해 IDE 지원이나 리팩토링, build logic 관리 측면에서 조금 더 안정적으로 사용할 수 있다는 평가들이 존재하였습니다.

결국 어떤 DSL이 절대적으로 더 좋다기보다는, 프로젝트 환경과 유지보수 방향에 따라 선택이 달라질 수 있다고 여겨졌습니다. 다만 신규 프로젝트를 시작하는 입장이라면, 개인적으로는 코틀린 DSL을 사용하는 방향이 조금 더 좋아 보였습니다.

반응형

댓글

Designed by JB FACTORY