[빌드] 그래들 (Gradle)

그래들

그래들(Gradle)은 자바 빌드 도구이다. 자바 빌드는 클래스 파일 컴파일, 컴파일된 파일(jar 파일) 실행 두 가지 과정으로 이루어진다. 자바 애플리케이션은 런타임 시 컴파일된 파일을 실행한다.

그래들에서는 그루비 언어로 작성된 스크립트를 사용하여 빌드 정보를 기술한다. 그래들의 빌드 관련 주요 스크립트 파일은 다음과 같다.

  • build.gradle
    • 빌드 정보를 기술한다.
    • 정의된 스크립트에 의해 빌드가 실행된다.
  • setting.gradle
    • 설정 정보를 기술한다.
    • 빌드 실행 전에 처리된다. 프로젝트 구조를 정의한다.
    • 멀티 프로젝트 구성일 경우 프로젝트 계층 구조를 정의한다.


명령어

  • 프로젝트 구조 생성 (파일, 폴더 생성)
    gradle init
    
    • --type: 프로젝트 타입 지정
  • 자바 프로젝트 생성
    gradle init --type java-library
    


프로젝트 구성

build.gradle

  • 플러그인 설정
    plugins {
      id '플러그인명' version '플러그인버전'
    }
    
  • 그룹ID, 아티팩트ID, 버전 설정
    group = '그룹ID'
    version = '버전'
    
  • 자바 소스 코드 호환성 설정
    sourceCompatibility = 자바버전
    
  • 저장소 설정
    repositories {
      저장소 설정
    }
    
  • 의존성 설정
    dependencies {
      implementation '라이브러리명:버전'
      api '라이브러리명:버전'
      testImplementation '라이브러리명:버전'
      debugImplementation '라이브러리명:버전' 
    }
    
    • implementation, api: 프로젝트를 컴파일하기 위해 필요한 라이브러리에 대한 의존성 설정
      • implementation: 해당 라이브러리의 구성 설정에 정의된 내부 의존성은 라이브러리 사용자에게 노출되지 않는다. 따라서 라이브러리 사용자의 컴파일 클래스 경로에 노출되지 않으며 해당 모듈을 참조(사용)할 수 없다.
      • api: implementation과 달리 해당 라이브러리의 구성 설정에 정의되어 있는 내부 의존성이 라이브러리 사용자에게 전이적으로 노출된다. 따라서 사용자의 컴파일 클래스 경로에 나타나게 되며 해당 모듈을 참조(사용)할 수 있다.
    • testImplementation
    • debugImplementation
  • 태스크 정의


setting.gradle

  • 그래들의 Project API는 settings.gradle 스크립트 파일을 사용하여 Setting 객체를 생성하며, 이 객체를 사용하여 Project 인스턴스들의 계층 구조를 생성한다.
  • 루트 프로젝트 설정
    rootProject.name = '루트프로젝트명'
    
  • 서브 프로젝트 설정
    include '서브프로젝트명'
    


소스 셋

그래들은 자바 지원을 위해 소스 기반 프로젝트 빌드 개념인 소스 셋(source sets)을 처음으로 도입하였다. 소스 셋은 프로젝트의 소스 코드와 리소스가 애플리케이션 코드나 단위/통합 테스트와 같은 유형 별로 논리적으로 그룹화되는 경우가 많다는 것에 기반한다.

  • 애플리케이션 소스 코드 및 리로스 파일 위치 (디렉토리 경로): src/main/java, src/main/resources
  • 테스트 관련 소스 코드 및 리소스 파일 위치: src/test/java, src/test/resources


자바 프로젝트는 일반적으로 애플리케이션 소스 코드 뿐만 아니라 리소스 파일들을 포함하고 있으며 이러한 파일들은 빌드 결과 최종적으로 생성된 JAR 파일 내에 포함되어 있다.

각 논리적 그룹에는 일반적으로 고유한 파일 종속성, 클래스패스 등이 존재하며, 중요한 점은 소스 셋을 구성하는 파일들이 동일한 디렉토리에 있을 필요가 없다는 것이다.

소스 셋은 다음과 같이 컴파일의 여러 측면을 함께 결합시키는 강력한 개념이다.

  • 소스 파일과 이들의 위치
  • (그래들 구성에 의해 정의된) 필수적인 종속성을 포함한, 컴파일 클래스 경로
  • 컴파일된 클래스 파일의 위치


자바를 포함한 대부분의 언어 플러그인은 운영 프로젝트에서 사용되는 main이라는 소스 셋을 자동으로 생성한다. 이 main이라는 이름은 구성 및 태스크의 이름에 포함되지 않는다. 플러그인은 main 소스 셋 외에 프로젝트의 테스트 코드 및 리소스 파일들을 위한 test라는 이름의 소스 셋도 정의한다.

test 소스 셋은 test라는 태스크를 사용하여 사용자에 의해 별도로 정의된 애플리케이션 코드 테스트를 수행한다. 프로젝트는 일반적으로 단위 테스트 수행을 위해 test 소스 셋을 사용하며, 의도에 따라 통합 테스트, 인수 테스트와 같은 기타 유형의 테스트를 위해서도 사용할 수 있다.

서로 다른 테스트 유형 별로 새로운 소스 셋을 정의할 수도 있으며, 이는 시각적 또는 관리 용이성을 위해 테스트들을 서로 분리할 필요가 있는 경우, 서로 다른 테스트 유형에 대해 서로 다른 컴파일 또는 런타임 클래스 경로, 기타 설정의 차이가 필요한 경우가 해당된다.


프로젝트 의존성

프로젝트 의존성(project depencency)이란 빌드 실행 의존성(execution dependency)을 말한다. 프로젝트의 빌드를 위해 먼저 빌드되어야 할 프로젝트를 정의하는 것(프로젝트 빌드 순서)을 프로젝트 의존성이라고 한다.

프로젝트 의존성에 따라 특정 프로젝트가 먼저 빌드되면 해당 프로젝트의 클래스가 포함된 jar 파일과 의존성이 클래스 경로에 추가된다.

멀티 프로젝트 구성일 경우, 사용되는 모든 서브 프로젝트가 먼저 빌드된 후 해당 프로젝트를 사용하는(해당 프로젝트에 의존하는) 루트 프로젝트가 그 다음 빌드된다.

프로젝트 의존성은 멀티 프로젝트의 부분적 빌드를 가능하게 한다. 루트 프로젝트와 서브 프로젝트 간 의존성 뿐만 아니라 서브 프로젝트 간 의존성에 따른 빌드를 수행 가능하게 한다.


Project API

Project API링크는 그래들이 빌드 파일(settings.gradle, build.gradle)을 사용하여 빌드를 실행하는 주요 API이다. 이 인터페이스를 통해 그래들의 모든 기능에 프로그래밍 방식으로 액세스할 수 있다.

Project API가 제공하는 주요 메서드는 다음과 같다.

  • project(): 프로젝트 경로를 인자로 받은 후 해당 프로젝트의 Project 객체를 반환한다.
  • allprojects(): 현재 프로젝트와 모든 서브 프로젝트의 Project 객체를 반환한다.
  • subprojects(): 현재 프로젝트의 모든 서브 프로젝트의 Project 객체를 반환한다.


멀티 프로젝트

그래들은 멀티 프로젝트의 빌드 관리 기능을 제공한다. 기본적인 멀티 프로젝트 구조는 루트 프로젝트가 하나의 서브 프로젝트를 포함하는 것이다. 이 경우 프로젝트 레이아웃은 다음과 같다.

루트프로젝트
├── 서브프로젝트
│   ...
│   └── build.gradle
├── build.gradle
└── settings.gradle


루트 프로젝트의 build.gradle 파일과 서브 프로젝트의 build.gradle에 애플리케이션 빌드 스크립트를 각각 정의한다. 이 경우 루트 프로젝트가 빌드를 위해 서브 프로젝트 모듈을 사용한다.

루트 프로젝트가 애플리케이션 빌드가 필요 없고, 단지 서브 프로젝트를 포함하는 구조일 경우 루트 프로젝트에 빌드 정보 파일인 build.gradle 파일이 없어도 된다. 이 경우 그래들은 서브 프로젝트가 존재하는 서브 디렉토리에서 build.gradle 파일을 찾는다. 따라서 서브 프로젝트의 build.gradle 파일에 애플리케이션 빌드 스크립트를 정의한다.

setting.gradle 파일에 포함시킬 서브 프로젝트를 정의한다.

rootProject.name = '루트프로젝트'
include '서브프로젝트'


루트 프로젝트가 두 개의 서브 프로젝트를 포함하는 경우 프로젝트 레이아웃은 다음과 같다.

루트프로젝트
├── 서브프로젝트1
│   ...
│   └── build.gradle
├── 서브프로젝트2
│   ...
│   └── build.gradle
├── build.gradle
└── settings.gradle


루트 프로젝트의 build.gradle 파일과 서브 프로젝트들의 build.gradle에 애플리케이션 빌드 스크립트를 각각 정의한다. 이 경우 루트 프로젝트가 빌드를 위해 서브 프로젝트들의 모듈을 사용한다.

프로젝트 구성을 멀티 프로젝트로 할 경우 서브 프로젝트에서 루트 프로젝트로 종속성 전이가 필요한 상황이 존재한다. 그래들의 프로젝트 종속성 관리 기능을 사용하여 이 문제를 해결할 수 있다.

모듈을 사용하는 프로젝트의 build.gradle 파일에 사용되는 프로젝트의 모듈 의존성을 정의한다.

dependencies {
  implementation ':서브프로젝트명'
  api ':서브프로젝트명'
}


루트 프로젝트나 서브 프로젝트의 build.gradle 파일에 서브 프로젝트 모듈의 의존성을 정의할 수 있다.

루트 프로젝트의 build.gradle에, 사용하는 서브 프로젝트 의존성이 정의된 경우 루트 프로젝트 빌드 수행 시 정의된 빌드 스크립트가 실행될 때 서브 프로젝트의 빌드가 먼저 수행된다. 루트 프로젝트가 실행 가능한 자바 애플리케이션을 빌드하고, 애플리케이션은 서브 프로젝트를 모듈로 사용하는 구성인 경우 해당 의존성 정의에 따라 순서대로 빌드가 수행된다.

멀티 프로젝트가 3-레벨 계층 구조인 경우 프로젝트 레이아웃은 다음과 같다.

루트프로젝트
├── 서브프로젝트1
│   ...
│   ├── 서브프로젝트1-1
│   │   ...
│   │   └── build.gradle
│   ├── 서브프로젝트1-2
│   │   ...
│   │   └── build.gradle
│   └── build.gradle
├── 서브프로젝트2
│   ...
│   ├── 서브프로젝트2-1
│   │   ...
│   │   └── build.gradle
│   ├── 서브프로젝트2-2
│   │   ...
│   │   └── build.gradle
│   └── build.gradle
├── build.gradle
└── settings.gradle


루트 프로젝트의 빌드 스크립트에 project() 메서드를 사용하여 특정 서브 프로젝트에 대한 빌드 정의가 가능하다.

allprojects()subprojects() 메서드를 사용하여 루트 프로젝트 및 서브 프로젝트를 포함한 모든 프로젝트 또는 모든 서브 프로젝트에 대한 빌드 정의가 가능하다.


서브 프로젝트 간 빌드 로직 공유

서브 프로젝트에 적용된 빌드 로직을 다른 서브 프로젝트 또는 루트 프로젝트와 공유하기 위해서 빌드 로직을 규칙 플러그인(convention plugin) 형태로 추출하고, 프로젝트에 해당 플러그인을 적용함으로써 빌드 로직을 프로젝트 간에 공유할 수 있다. 이는 빌드 로직을 재사용 가능한 플러그인 형태로 만드는 것이다.


교차 구성과 프로젝트 간 결합도

멀티 프로젝트 구조인 경우 한 빌드 스크립트에서 모든 프로젝트에 접근 가능하다. 이러한 특성으로 인해 루트 프로젝트에서 서브 프로젝트의 빌드 스크립트를 정의할 수도 있고, 한 서브 프로젝트에서 다른 서브 프로젝트의 빌드 스크립트를 정의할 수도 있다. 이를 교차 프로젝트 구성(cross project configuration)이라고 한다.

Project API가 제공하는 allprojects()subprojects() 메서드를 사용하여 루트 프로젝트를 포함한 모든 프로젝트 또는 모든 서브 프로젝트에 대한 빌드 스크립트를 정의 함으로써 교차 구성을 통한 프로젝트 빌드 스크립트 정의도 가능하다. 하지만 이 경우 독립적인 서브 프로젝트를 서로 결합시키고, 특정 서브 프로젝트의 빌드 로직을 확인하기 힘들다는 단점이 있다.

교차 구성은 또한 프로젝트 간 구성 시점(configuration time) 결합을 일으켜 주문형 구성(configuration on demand) 기능의 올바른 동작을 방해할 수도 있다.

위와 같은 단점을 보완하기 위해 교차 구성을 사용할 경우 특정 유형의 프로젝트에만 플러그인 및 별도 구성을 적용하는 방법을 사용할 수 있다.

교차 구성 방법을 사용하는 대신 각 서브 프로젝트의 build.gradle 파일에 서브 프로젝트 별로 필요한 의존성을 정의하는 방법을 택할수 있다.


테스트 프레임워크 사용을 위한 빌드 구성

테스트 프레임워크를 사용할 경우 그래들이 해당 테스트 코드를 찾고 실행하도록 하기 위해 프로젝트에 java 플러그인을 적용하고 test 태스크에 대한 설정이 필요하다.

plugins {
  id 'java'
}

test {
  ...
}


testImplementation을 사용하여 테스트 프레임워크의 테스트 의존성을 추가한 후 test 태스크 블록 내에 사용하고자 하는 프레임워크 플랫폼 지정 메서드를 호출한다. JUnit5를 사용하는 경우 해당 플랫폼을 사용하도록 설정하기 위해 useJUnitPlatform() 메서드를 호출하여 적절한 테스트 엔진이 제공될 수 있도록 한다.

// groovy
tasks.named('test') {
  useJUnitPlatform()
}

// kotlin
tasks.withType<Test> {
  useJUnitPlatform()
}


테스트 코드 작성 시 정해진 규칙을 따라야 한다. 그렇지 않으면 테스트 프레임워크가 테스트 클래스 및 테스트 메서드를 찾고 실행하는 과정에서 오류가 발생한다. 예를 들어 다음과 같은 오류가 발생할 수 있다.

Execution failed for task ':api:test'.
> No tests found for given includes:


따라서 테스트 코드 작성 시 해당 프레임워크가 정한 코드 작성 규칙을 따라야 한다. 또한 프레임워크를 사용한 테스트 클래스 및 메서드를 작성하고 실행하려고 하였지만 테스트 태스크 블록 내에서 useJUnitPlatform()를 호출하여 해당 플랫폼을 사용하도록 설정되어 있지 않은 경우에도 동일한 오류가 발생한다.

멀티 프로젝트 구성일 경우 각각의 서브 프로젝트에 테스트 코드를 작성하게 된다. 이 때 각각의 서브 프로젝트에 대해 그래들의 테스트 태스크가 정상 동작하여야 한다. 따라서 빌드 스크립트에서 Project API의 subprojects()useJUnitPlatform() 메서드를 호출하여 해당 서브 프로젝트에 적절한 테스트 엔진이 제공될 수 있도록 한다.

subprojects {
    // groovy
    tasks.named('test') {
      useJUnitPlatform()
    }
 
    // kotlin
    tasks.withType<Test> {
        useJUnitPlatform()
    }
} 


서브 프로젝트에서 테스트 코드 실행 시 다음 에러가 발생할 경우 위 설정이 되어 있는지 확인한다.

Execution failed for task ':api:test'.
> No tests found for given includes: ...


플러그인

빌드 자동화의 모든 기능들이 그래들의 기본적인 태스크에 의해서 제공되지는 않으며 자바 코드 컴파일과 같은 기능은 그래들 플러그인(plugin)에 의해 제공된다.

플러그인은 태스크, 도메인 객체, 규칙(컨벤션)을 추가하고 핵심적인 객체나 다른 플러그인에 의해 정의되는 객체를 확장하는 역할을 한다. 플러그인 마다 빌드를 위해 소스 파일을 찾는 위치인 프로젝트 레이아웃 규칙이 다를 수 있다.

자바 언어로 작성된 프로젝트를 독립 실행형 애플리케이션 또는 라이브러리 형태로 빌드하기 위한 플러그인은 다음과 같다.

  • 자바 플러그인 (java): 자바 코드를 컴파일 및 테스트한 후 실행 가능한 jar 파일을 생성한다.
  • 자바 라이브러리 플러그인 (java-library): 자바 플러그인의 기능을 확장하는 플러그인이다. 이 플러그인은 자바 라이브러리를 사용(소비)하는 프로젝트에게 API를 노출한다.

이 외에 자바 웹 애플리케이션 빌드를 위한 war 플러그인, 스프링 부트 애플리케이션 빌드를 위한 org.springframework.boot 플러그인 등이 존재한다. 프로젝트의 빌드 목적과 기능 사용 필요성에 따라 적절한 플러그인을 프로젝트에 적용하여 사용하면 된다.


자바 프로젝트 빌드

자바 플러그인(java)은 자바 애플리케이션의 테스트 및 번들링 기능과 자바 컴파일 기능을 프로젝트에 추가하여 자바 프로젝트를 빌드할 수 있게 한다.

자바 플러그인에 의해 확장되는 대상의 예는 다음과 같다.

  • 태스크: JavaCompile, ProcessResources, Jar, Javadoc, Test, Delete
  • 도메인 객체: SourceSet
  • 프로젝트 레이아웃 규칙: 자바 소스 코드는 src/main/java에 위치해야 한다.


이러한 확장 기능은 자바 외 다른 JVM 언어(코틀린, 그루비, 스칼라 등)의 그래들 플러그인의 기반이 되며 언어 및 프로젝트 유형에 따라 JVM 기반 애플리케이션을 빌드하는 데 필요한 다양한 기능들을 제공한다.

그래들은 아파치 메이븐이 JVM 기반 프로젝트를 빌드하기 위한 여러 규칙들을 차용함으로써, 소스 파일 및 리소스의 기본 디렉토리 구조를 동일하게 사용하며 메이븐 리포지토리와 호환 가능하다.


소스 셋

자바를 포함한 대부분의 언어 플러그인은 운영 프로젝트에서 사용되는 main이라는 소스 셋을 자동으로 생성한다. 이 main이라는 이름은 구성 및 태스크의 이름에 포함되지 않는다. 플러그인은 main 소스 셋 외에 프로젝트의 테스트 코드 및 리소스 파일들을 위한 test라는 이름의 소스 셋도 생성한다.

그래들 플러그인은 프로젝트에 구성된 각각의 소스 셋에 대해 태스크를 추가하는 기능을 수행할 수 있다. 자바 플러그인은 정의된 모든 소스 셋에 대해 다음 태스크를 자동으로 생성한다.

  • processSourceSetResources: 해당 소스 셋의 리소스를 리소스 폴더로 복사한다.
  • compileSourceSetJava: JDK 컴파일러를 사용하여 해당 소스 셋의 자바 소스 파일을 컴파일한다.


자바 라이브러리 빌드

자바 라이브러리 프로젝트는 특징적으로 다른 자바 프로젝트에 의해 사용되는 프로젝트를 말한다. 라이브러리 프로젝트는 다른 프로젝트에 의해 사용되므로 종속성 메타데이터가 중요하다. 이러한 메타데이터는 보통 메이븐 POM 형식으로 존재한다.

라이브러리를 사용하는 쪽에서는 다음 두 가지 유형의 종속성을 구분할 수 있어야 한다.

  • 라이브러리를 컴파일하기 위해서만 필요한 종속성
  • 라이브러리를 사용하는 프로젝트를 컴파일하는 데에도 필요한 종속성


그래들은 자바 라이브러리 플러그인을 통해 이러한 종속성 구분을 관리한다. 자바 라이브러리 플러그인은 자바 플러그인과 달리 구현(implementation) 이외에 API(api)라는 개념을 도입하였다.

프로젝트가 라이브러리의 public 클래스의 public 필드 또는 메서드를 사용하는 종속성 유형인 경우 해당 종속성은 라이브러리의 공개 API를 통해 노출되므로 해당 종속성을 api 구성에 추가해야 한다. 그렇지 않다면 해당 종속성은 공개 API가 아닌 내부적 구현 상세이므로 implementation 구성에 추가되어야 한다.


빌드 초기화

자바 라이브러리 플러그인은 베이스 플러그인을 적용하여 프로젝트에 clean이라는 태스크를 추가한다. clean 태스크는 $buildDir 디렉토리에 존재하는 모든 파일들을 단순히 제거하는 작업을 수행하므로 clean 태스크를 해당 목적으로 수행하려는 경우 빌드 작업이 생성하는 파일들은 항상 해당 디렉토리에 위치시켜야 한다.

clean 태스크 작업은 Delete의 인스턴스이며 dir 속성을 설정함으로써 삭제할 디렉토리 경로를 변경할 수 있다.


자바 웹 애플리케이션 빌드

자바 웹 애플리케이션은 사용하는 기술에 따라 다양한 방식으로 패키징 및 배포할 수 있다. 예로 uber jar와 스프링 부트를 사용한 시스템, 네티에서 동작하는 리액티브 기반 시스템 등이 있다.

코어 그래들은 war 파일로 배포되는 기존 서블릿 기반 웹 애플리케이션만 직접적으로 지원한다. War 플러그인(war)을 사용하면 자바 플러그인이 자동으로 적용되며 war 태스크에 의해 다음과 같은 추가적인 패키징 단계를 수행한다.

  • 정적 리소스를 src/main/webapp 경로에 복사
  • 컴파일된 클래스들을 WEB-INF/classes 경로에 복사
  • 라이브러리 종속성을 WEB-INF/lib 경로에 복사


스프링 부트 플러그인

스프링 부트 플러그인(org.springframework.boot)은 그래들의 스프링 부트 애플리케이션 빌드 지원을 제공한다. 이 플러그인은 실행 가능한 jar 파일 또는 war 파일을 패키징, 스프링 부트 애플리케이션 실행, spring-boot-dependencies가 제공하는 종속성 관리 사용 등의 기능을 제공한다.

bootJar 태스크는 실행 가능한 스프링 부트 애플리케이션 jar 파일을 생성하며 메인 메서드를 필요로 한다. 애플리케이션 컴포넌트가 아닌, 메인 메서드가 존재하지 않는 라이브러리 컴포넌트의 경우 프로젝트에 대한 실행 가능한 jar 파일을 빌드하지 않도록 별도의 설정이 필요하다. 실행 가능한 jar 파일 생성이 필요하지 않다면 스프링 부트 플러그인은 사용하지 않는다. 따라서 프로젝트를 실행 가능하게 만드는 스프링 부트 그레이들 플러그인을 빌드 구성에서 비활성화한다.

스프링 부트 플러그인의 의존성 관리 기능만 사용하고자 한다면 스프링 부트 플러그인 대신 스프링 부트 의존성 관리 플러그인(io.spring.dependency-management)을 사용할 수 있다. 이 경우 다음과 같이 빌드 스크립트를 정의하여 스프링 부트 플러그인에서 BOM으로 의존성 관리를 가져오도록 한다.

plugins {
  id 'org.springframework.boot' version '2.5.2' apply false
  id 'io.spring.dependency-management' version '1.0.11.RELEASE'
}

dependencyManagement {
  imports {
    mavenBom org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES
  }
}


참고

Comments