우테코 체스 미션
에서 데이터베이스를 사용할 때, Spring
을 사용하여 무엇을 만들어 볼 때 Gradle
이란 도구를 사용해봤다.
Gradle이란?
Maven
을 대체하고 개선하기 위해 설계되었다.Gradle은 외부 라이브러리를 편하게 가져와 사용할 수 있도록 해주는 기능을 제공한다.
그것을 종속성 관리라고 한다.
그 외 정보나 기능은 검색하면 잘 정리해놓은 글이 정말 많다.
따라서 Gradle
의 설명은 간단하게만 하고, 바로 본론으로 넘어가자.
[대충 자세한 설명은 생략한다 짤]
우선 밑의 코드는 build.gradle
의 내용이다.
Gradle
을 사용해서 종속성을 관리할 땐 3가지 블록으로 작성한다.
build.gradle
의 문법은 Kotiln
또는 Groovy
로 작성할 수 있다.
여기선 Groovy
를 사용했다.
plugins {
id 'java-library'
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.hibernate:hibernate-core:3.6.7.Final'
api 'com.google.guava:guava:23.0'
testImplementation 'junit:junit:4.+'
}
Gradle
이 내가 작성한 코드를 컴파일하고 빌드할 때 모두 plugin
에 의해 이루어진다.
따라서 내가 Java
를 사용하여 빌드를 할 때 Java
플러그인이 없으면 빌드를 할 수 없다.
플러그인 선언은 id '(플러그인 이름)'
으로 선언할 수 있다.
위의 코드 예시에선 id 'java-library'
로 플러그인이 선언되어 있다.
하지만 Spring
에서 제공되는 기본 build.gradle
에서는 id 'java'
로 플러그인이 선언된 것을 볼 수 있다.
둘의 차이가 뭘까?
기본적으로 빌드시 jar
파일을 생성한다.
단순한 JAR
파일 생성에 초점을 맞추고 있다.
jar
파일을 생성하지만, 추가적인 메타데이터와 함께 생성한다.
자바 라이브러리를 만들기 위해 초점을 맞추고 있다.
따라서 라이브러리를 만드는 목적이 아니라면 id 'java'
를 사용하자.
참고) https://discuss.gradle.org/t/when-should-we-use-java-plugin-and-when-java-library-plugin/25377
참고) https://docs.gradle.org/current/userguide/plugins.html
외부 라이브러리나 종속성을 다운로드하기 위한 위치를 적는다.
repositories {
mavenCentral() // 중앙 저장소
jcenter() // 지금은 운영이 중단됐다.
maven { // 사용자 정의 저장소
url "http://repo.mycompany.com/maven2"
}
}
이렇게 여러개를 선언할 수 있다.
참고) jcenter 는 운영이 중단됐으니 사용하지 않기를 바란다.
여러 개를 적는다면 선언한 순서대로, 즉 위에서부터 아래로 종속성 찾는다.
참고) https://docs.gradle.org/current/userguide/declaring_repositories.html
라이브러리 관리를 위한 핵심적인 부분이다.
바로 여기서 종속성을 선언한다.
종속성 선언은 (종속성 구성) (group):(name):(version)
으로 선언할 수 있다.
Java
라이브러리에서 주로 사용되는 종속성 구성은 4가지 이다.
1. api
2. implementation
3. compileOnly
4. runtimeOnly
중요한 부분이니 자세히 알아보자
제일 주의해서 사용해야 하는 구성이다.
공식 문서에서도 가능하다면 implementation
을 사용하라고 나와 있다.
그렇다면 왜 주의해야 하고 가능하면 implementation
을 사용해야 할까?
설명에는 This is where you declare dependencies which are transitively exported to consumers, for compile time and runtime.
이라고 나와 있다.
여기서 중요한 부분은 transitively
인데.
한글로 번역하면 전이적
이라는 뜻이다.
api
로 선언한 라이브러리가 다른 라이브러리에서 사용될 때 api
로 선언된 라이브러리를 사용할 수 있다.
즉, 라이브러리가 전이되며 불필요한 종속성이 추가로 생길 수 있다는 말이다.
말로는 크게 와닿지 않으니 예시 코드로 살펴보자
Internal 라이브러리
public class Internal {
public static String giveMeAString(){
return "hello";
}
}
Internal 라이브러리를 사용하는 MyLib 라이브러리
public class MyLib {
public String myString() {
return Internal.giveMeAString();
}
}
MyLib 라이브러리의 dependencies
dependencies {
api project(':internal')
}
MyLib 라이브러리를 사용하는 Application
public static void main(String[] args) {
MyLib myLib = new MyLib();
System.out.println(myLib.myString()); // 필요한 라이브러리
System.out.println(Internal.giveMeAString()); // 필요하지 않은 라이브러리
}
Application의 dependencies
dependencies {
api project(':mylib')
}
MyLib
의 종속성 구성은 api
이다.
여기서 Application
은 MyLib
의 기능만 필요하지, Internal
라이브러리의 기능은 필요하지 않다.
하지만 해당 라이브러리를 불러올 수 있고, 불필요하게 의존이 가능하다.
만약 사용자가 실수로 혹은 모르고 해당 라이브러리를 사용하게 된다면 불필요한 의존성이 생기게 된다.
어떻게 이것을 해결할 수 있을까?
바로 implementation
을 사용하면 된다.
하지만 Application만 implementation
을 사용한다고 MyLib
이 사용 중인 Internal
을 사용하게 만들 수는 없다.
MyLib 라이브러리를 사용하는 Application
public static void main(String[] args) {
MyLib myLib = new MyLib();
System.out.println(myLib.myString());
System.out.println(Internal.giveMeAString()); // 여전히 컴파일이 된다!
}
Application의 dependencies
dependencies {
implementation project(':mylib')
}
즉, 내부적으로 사용하는 라이브러리가 api
대신 implementation
을 종속성 구성으로 선언 해야한다.
MyLib 라이브러리의 dependencies
dependencies {
implementation project(':internal')
}
MyLib 라이브러리를 사용하는 Application
public static void main(String[] args) {
MyLib myLib = new MyLib();
System.out.println(myLib.myString());
System.out.println(Internal.giveMeAString()); // 컴파일 에러!
}
이렇게 해서 api
를 사용하면 불필요한 의존성이 발생할 수 있어 작업물의 유지보수를 힘들게 하고 추가적인 라이브러리 빌드가 필요하게 되어 컴파일 속도가 느려질 수 있다.
따라서 implementation
를 사용하도록 하자.
그리고 다행인 게 api
를 선언하려면 plugins
의 id
를 java-library
로 선언해야 하므로, 라이브러리를 만드는 것이 아닌 단순히 jar
파일을 만드는 것이 목적이면 크게 신경 쓸 일은 없을 것이다.
말 그대로 컴파일 시점에만 사용하겠다는 말이다.
즉, 런타임에서는 해당 라이브러리를 사용할 수 없다.
최종 빌드 파일에는 해당 선언이 된 라이브러리가 포함되지 않는다.
그렇다면 어떨 때 해당 구문을 사용할까?
가장 좋은 예는 lombok
라이브러리이다.
lombok
라이브러리는 컴파일 시점에 애너테이션이 선언된 클래스 파일을 분석하여 동적으로 코드를 생성해주는 라이브러리이다.
그러므로, 런타임 시점에는 해당 라이브러리가 필요 없으므로 compileOnly
를 선언하면 빌드시 용량을 줄일 수 있다.
compileOnly
가 컴파일 시점에만 사용할 수 있다면
runtimeOnly
는 컴파일 시점에는 라이브러리를 사용하지 못한다.
즉, 최종 빌드 파일에는 포함이 되지만, 코드를 작성할 때는 사용자가 굳이 알 필요가 없을 때 사용한다.
가장 좋은 예는 JDBC 구현체
라이브러리이다.
JDBC
를 사용할 때 우리는 JDBC
인터페이스를 사용하지, 구현체를 직접 다루거나 조작할 일은 없다.
DriverManager
를 사용하여 가져오는 Connection
과 PreparedStatement
, ResultSet
도 전부 인터페이스이다.
그러므로, 직접 라이브러리를 다루지 않고 인터페이스에 의존하는 경우 runtimeOnly
선언을 통해 의존성을 최소화해, 변경에 더욱 유연한 설계를 할 수 있다.
참고) https://docs.gradle.org/current/userguide/declaring_dependencies.html
참고) https://docs.gradle.org/current/userguide/java_plugin.html#sec:java_plugin_and_dependency_management
참고) https://docs.gradle.org/current/userguide/java_library_plugin.html#sec:java_library_configurations_graph
참고) https://stackoverflow.com/questions/44413952/gradle-implementation-vs-api-configuration
이렇게 간단하게 Gradle의 종속성 관리를 알아봤다.
평소에 검색을 통해 붙여넣기로 종속성을 추가해봤지, 종속성 구문에 대해 의구심을 가져본 적이 없었다.
이번 정리를 통해 다른 라이브러리를 사용하거나, 직접 라이브러리를 만들 일이 있을때 보다 효과적으로 설계를 할 수 있을 것 같다.