코딩하는 오징어

Gradle Dependency Configuration 본문

알쓸신잡

Gradle Dependency Configuration

코딩하는 오징어 2021. 9. 26. 22:02
반응형

 컴파일 언어를 이용하는 개발자들은 코드를 작성하고 해당 코드를 컴퓨터에서 실행시키기 위해 빌드라는 작업을 한다. 빌드라는 작업은 소스 코드를 컴파일 할 뿐만아니라 dependency를 추가해주고 실행가능한 bundle(묶음 파일)을 생성한다. java에서는 jar파일 또는 war파일이 될 수 있다. 이러한 결과물을 우리는 Artifact(인공물)라고 부른다. 개발자가 필요한 라이브러리와 소스 코드의 classpath를 직접 명시해주고 Artifact를 생성할 수 있지만, 대개의 경우 Maven이나 Gradle 같은 빌드 툴을 이용하여 해당 작업을 자동화한다. Maven은 pom.xml에 Gradle은 build.gradle 파일에 빌드 작업을 각 빌드 툴에서 제공하는 명세 혹은 DSL로 작성한다. 그러면 빌드 툴은 해당 파일을 통해 어떻게 빌드 해야할지 파악한 후 Artifact를 생성한다. 그 과정에서 어떤 시점에 필요한 라이브러리들의 classpath를 추가해줘야 하는지에 대한 고민을 이번 글에서 다뤄보려고한다. Maven은 scope라는 개념으로 Gradle에서는 dependency configuration이라는 개념으로 해당 기능을 제공한다. 이 글에서는 Gradle의 dependency configuration을 다뤄 보려고한다. 이 글을 읽고나면 Maven의 scope라는 개념도 쉽게 이해할 수 있다. (컴파일만 잘 된다면 실행이 가능할 거라고 생각하면 큰 오산이다. runtime classpath와 compile classpath는 엄연히 다르다.)
 먼저 Gradle에서 dependencies block을 사용하려면 다음과 같이 java-library 혹은 java플러그인을 추가해야한다.

> java-library는 이후에 설명할 api configuration을 사용할 수 있다.

plugins {
    id 'java-library' // or id 'java'
}

kafka-clients, lombok, mysql-connector, h2database를 사용하여 프로젝트를 세팅한다고 가정해보자.

  • kafka-client를 이용하여 이벤트를 kafka 발행할 것이다. compile & runtime 모두 필요하다.
  • lombok은 compile시에만 사용된다.
  • mysql-connector를 직접 사용하는 코드는 없기(컴파일을 할 필요 없음) 때문에 runtime시에만 사용된다.
  • h2database는 테스트 runtime시에만 사용된다.

위의 라이브러리들이 사용되는 시점을 잘 기억하길 바란다.

dependencies {
    implementation 'org.apache.kafka:kafka-clients:3.0.0'
    implementation 'org.projectlombok:lombok'
    implementation 'mysql:mysql-connector-java'
    testImplementation 'com.h2database:h2'
}

 Gradle의 dependency configuration에 대한 개념이 부족하다면 모든 라이브러리들의 classpath 적용시점을 implementation으로 선언할 것이다. 물론 빌드는 잘 될 것이다. implemtation configuration은 runtime과 compile 모든 시점에 classpath를 추가해주기 때문이다. (gradle dependency configuration의 자세한 내용은 gradle document를 참고하자.) 하지만 이렇게 설정한다면 특정 시점에  특정 라이브러리가 필요하지 않음에도 불구하고 classpath를 추가하기 때문에 리소스 낭비이다. 위의  라이브러리들이 사용되는 시점을 토대로 개선해보자. 

dependencies {
    implementation 'org.apache.kafka:kafka-clients:3.0.0'
    compileOnly 'org.projectlombok:lombok'
    runtimeOnly 'mysql:mysql-connector-java'
    testRuntimeOnly 'com.h2database:h2'
}

 위와 같이 개선할 수 있다. kafka-clients를 runtimeOnly로 설정하게 되면 kafka-client를 사용한 KafkaProducer 클래스를 컴파일시점에 찾지 못해 compile이 실패할 것이고, compileOnly로만 설정하였다면 runtime시 해당 클래스를 참조할 classpath를 찾지 못하여 ClassNotFoundException와 NoClassDefFoundError가 발생할  것이다. (intellij로 실행할 경우 intellij가 classpath를 자동으로 추가하여 정상적으로 실행될 수 있다. 제대로 테스트를 해보려면 build 후 jar파일을 직접 실행하면 ClassNotFoundException와 NoClassDefFoundError가 발생할  것이다.)

 지금 까지는 단순히 실행가능한 Artifact를 생성하는 관점에서 gradle dependency configuration을 살펴보았다. 이번에는 라이브러리로 제공하기 위해 생성되는 Artifact관점에서 살펴보자. 우리는 다음과 같은 상황을 가정해 볼 수 있다.

공통적으로 사용할 common이라는 library가 있고 storage 접근을 위한 repository library가 있다. repository library는 common library에대한 의존성 추가하여 개발되었고 api project는 repository library에대한 의존성을 추가 하여 비즈니스 로직을 개발하고 있다고 가정해보자. 우리는 repository 프로젝트의 build.gradle을 다음과 같이 작성하였다.

plugins {
    id 'java-library'
}

dependencies {
    implementation 'org.codingsuqid:common:1.0.0'
    ...
}

위와 같이 dependency configuration을 작성한 후 api 프로젝트의 build.gradle을 다음과 같이 작성하면 api 프로젝트에서는 common library를 사용할 수 없다.

plugins {
    id 'java-library'
}

dependencies {
    implementation 'org.codingsuqid:repository:1.0.0'
    ...
}

하지만 다음과 같이 repository의 dependency configuration을 implementation대신 api(java-library plugin에서만 사용할 수 있다.)를 이용한다면 api 프로젝트에서도 common을 사용할 수 있게 된다.

plugins {
    id 'java-library'
}

dependencies {
    api 'org.codingsuqid:common:1.0.0'
    ...
}

즉, implementation과 api의 차이는 transitive 정도의 차이이다. api로 dependency를 추가하게되면 repository를 의존하고 있는 다른 프로젝트에서도 common에 대한 api들이 노출되어 사용할 수 있게 되는 것이다. 따라서 api는 신중하게 사용하자. 

위의 그림을 보면 common이 수정되면 implementation configuration같은 경우에는 repository만 rebuild하면 되지만 api configuration같은 경우에는 api 프로젝트도 rebuild되어야 한다. 따라서 정말 필요한 상황이 아니라면 implementation로 configuration을 설정하는 것이 빌드 속도가 더 향상될 수 있다. gradle 6.x까지 지원되던 compile configuration은 api와 동일하며 7.x부터 사용할 수 없기 때문에 더 이상 사용하지 말자.

 

참고: 

 

[Gradle] implementation vs compile

서론 Gradle dependency 관련해서 검색을 하다보면, 어떤 글에서는 implementation을 사용하고 어떤 글에서는 compile을 사용하는 경우가 있다. 사실 어떻게 사용해도 돌아가긴 해서, 음... 무슨 차이지?하고

bluayer.com

 

Maven Scopes? Gradle Configurations?

대부분의 내용은 reflectoring.io/maven-scopes-gradle-configurations/ 개인적으로 의미를 해석한 것입니다. 빌드 툴은 의존성(dependency)을 관리한다. 우리가 어떠한 라이브러리를 프로젝트에서 사용하고 싶을.

olivejua-develop.tistory.com

 

The Java Library Plugin

The Java Library plugin expands the capabilities of the Java plugin by providing specific knowledge about Java libraries. In particular, a Java library exposes an API to consumers (i.e., other projects using the Java or the Java Library plugin). All the so

docs.gradle.org

 

반응형

'알쓸신잡' 카테고리의 다른 글

JVM의 종료와 Graceful Shutdown  (2) 2021.05.19
우리는 테스트 코드를 왜 짜야할까??  (3) 2020.11.28
Redirect 와 Forward  (2) 2020.04.27
OLTP와 OLAP  (1) 2020.01.05
데이터 모델링에서 정규화의 의의와 성능 논쟁  (0) 2020.01.05
Comments