눈가락

Gradle 공부 필기

눈가락 2024. 4. 1. 21:38

 


Gradle 은 application 의 빌드와 배포 문제를 해결하기 위한 framework 임
Java 의 경우, java class files, doc files, XML files, property files, 그 외 resources 등
하나의 프로젝트(application)에 굉장히 다양한 파일들을 담고 있음
게다가 각 프로젝트마다 각기 다른 구조 및 버전을 갖고 있어서 빌드하기가 굉장히 까다로움
Gradle 이 이러한 문제 해결에 도움을 줌

Gradle 이 하는 것
- 프로젝트 빌드하고 배포(jar 파일 만들어 줌)
- 외부 lib 의존성 관리

 

Gradle 이 사용하는 build file 의 default 명은 "build.gradle"

Gradle 은 Domain-Specific Language (DSL) 을 정의함 (DSL : 어떤 도메인에서 특정 목적만을 달성하기 위해 사용되는 언어)
예를 들어 프로젝트는 Java 지만, build.gradle 은 Groovy 나 Kotlin 으로 쓰여질 수 있음

Gradle 이 진행하는 작업 단위 하나하나를 "task" 라고 부름
build.gradle 파일을 파싱해서 task 를 뽑아내고 작업을 진행함

task 들은 DAG 를 구성함.
따라서 모든 task 는 실행의 순서가 있으며 반복 실행되지 않음

먼저 실행된 task 의 output 이 나중에 실행된 task 의 input 이 되기도 함

Gradle 은 각 task 의 output 을 저장함
어떤 task 의 input 및 output 이 동일하다면, 다음에 동작할 때 해당 task 는 실행하지 않음으로써 리소스를 아낄 수 있음


Gradle 은 자기 자신을 자동으로 업데이트 할 수 있음
Gradle 은 의존성을 야기하는 lib 들 또한 자동으로 업데이트 할 수 있음


Gradle 의 핵심 구조는 다음과 같음

- Project : 소프트웨어 요소들의 능력(?) 이나 범위를 정의 
- build script : "build.gradle" 이름을 갖는 스크립트 파일. build 하기 위한 지시사항들(task)을 갖추고 있음
- task : 실질적인 작업 사항


hello world 를 프린트하는 굉장히 간단한 build.gradle 예제 (Groovy)

< build.gradle >
task helloWorld {
    doLast {
        println "Hello World"
    }
}

위 내용을 갖는 build.gradle 을 만든 후,
"gradle helloWorld" 명령어를 실행하면, gradle 이 실행되며 Hello World 가 실행됨


"gradle wrapper" 명령어를 실행하면, 기본적인 gradle 구조를 만들어 줌

- build.gradle
- gradle
- gradlew
- gradlew.bat

build.gradle 은 task 들을 모아둔 명령 파일이고, gradlew 로 시작하는 것들은 실행 파일임

각 OS 마다 사용되는 gradle 실행 파일이 다름.
- gradlew : unix(MacOS) 에서 사용 가능한 gradle 실행파일
- graldew.bat : Window 에서 사용 가능
- gradle/wrapper/gradle-wrapper.properties : gradle 버전 정보와 다운로드 url 정보 포함
- gradle/wrapper/gradle-wrapper.jar : gradle wrapper 실행에 필요한 jar 파일

이렇게 생성된 gradlew 를 갖게 되면
MacOS 에서 "./gradlew helloWorld" 같은 명령어 실행이 가능
해당 명령어로 위에서 만든 hello world 출력이 가능함

이렇게 만들어진 gradlew 를 사용하여 빌드하면
내 로컬 환경이 어떻게 구성되었는지 상관없이, 즉 내 컴퓨터에 java 나 gradle 이 설치되지 않아도 빌드가 가능함
다르게 말하면, gradlew 를 다른 컴퓨터나 서버로 옮긴 후에도 빌드가 가능함



참고로 gradlew tasks 명령어를 실행하면
현재 프로젝트에서 사용할 수 있는 gradle tasks 목록을 출력해 줌



Task 에는 여러 종류가 존재함



Ad hoc Task : 단 한 번만 커스텀하게 실행되는 태스크. doFirst, doLast 등의 액션으로 실행됨


task helloWorld {   // 특별한 타입 없이 이렇게 선언 가능하다고 함. 아래 "type"을 갖는 태스크도 존재함
  doLast {  // Action 을 이렇게 선언
    println "Hello World!"
  }
}

실행 할 때는 gradle helloWorld 로 실행함



Type 을 선언하는 Task : copy 등의 타입을 선언한 태스크.
여기서 type 은, "이미 무엇을 할 지 정해져있는 작업모음 함수" 같은 것이라고 생각하자
예를 들어, copy type 은 "files 를 복사하고 dir 에 붙여넣는 작업"내용을 담고 있음
doLast 등의 actions 을 선언 할 필요가 없음. 왜냐하면 type 에서 이미 actions 정보를 갖고 있기 때문

task copyDocs(type: Copy) {
  from "src"
  into "build/docs"
  include "src/**/*md"   // .md 파일만 골라서 copy 하도록 만듦. 여기서 ** 는 특정 dir 와 그 하위 dir 의 모든 파일을 포함하는 wildcard
  includeEmptyDirs = false   // 빈 dir 는 제외하도록 설정
}
task createZip(type: Zip) {
  from "build/docs"   // build/docs 를 압축함
  archiveFileName = "docs.zip"   // 압축파일 이름
  destinationDirectory = file("build/dist")   // 생성된 압축파일이 저장되는 위치
  dependsOn copyDocs   // copyDocs 태스크가 실행된 이후에 createZip 이 실행되도록 의존 관계를 설정
}

실행 할 떄는 gradle copyDocs, gradle createZip 으로 실행함
gradle createZip 을 실행하면, 의존성이 걸려있는 copyDocs 가 같이 실행됨



--dry-run 옵션은 "gradle 실행시 어떤 tasks 를 실행할지 목록"을 보여줌
실제로 tasks 가 실행되지 않음
예를 들면 gradle createZip --dry-run

(혹은 gradle task tree 라는 community plugin 을 사용하여 실행 목록을 확인할 수 있음)



--all 은 gradle 을 실행할 때 동작을 확장하는 역할을 함
예를 들면 gradle tasks --all
gradle dependencies --all   // 모든 의존성 정보를 더 자세하게 출력함(터미널에)




gradle 이 실행(빌드가 실행 되면)되면, 내부적으로 아래 세 가지 스텝(life cycle)을 통해 실행됨.



1. Initialization phase : 빌드 대상이 되는 프로젝트의 정보를 담고 있는 settings gradle file 을 준비함



2. Configuration phase : build script 내 build 로직을 분석함
각 프로젝트마다 build script 를 갖을 수 있지만, 굳이 모든 프로젝트가 젝각각의 build script 를 갖고있을 필요는 없음.
configuration phase 에서는 task 는  실행되지 않지만
(build.gradle 내에 있는) configuration code 는 실행됨
configuration codes 는 task 바깥에 있음. 아래 코드 예제 보면 바로 이해될 것임

// 여기 configuration code 가 존재하면 configuration phase 에서 실행됨
task helloWorld {
  // 여기 configuration code 가 존재하면configuration phase 에서 실행됨
  doFirst {}
  doLast {}
}



3. Execution phase : tasks 를 기준으로 생성된 DAG 따라서 순서대로 tasks 를 실행함
DAG 는 메모리 위에 있다고 함
execution phase 에서 실행하는 code 는 task 안쪽에 있음.

task helloWorld {
  doFirst {
    // 여기 execution code 가 존재하면 execution phase 에서 실행됨
  }
  doLast{
    // 여기 execution code 가 존재하면 execution phase 에서 실행됨
  }
}



여러 프로젝트의 build.gradle 내에 동일 코드가 반복되어 쓰여지는 것을 방지하고
중복 코드를 효율적으로 관리하기 위해
plugin 이라는 기능을 제공함.



예를 들어보자

< archiving.gradle >
task helloWorld {
 doLast{
    println "Hello World!"
  }
}

위 archiving.gradle 을 아래 build.gradle 에서 가져올 수 있음

< build.gradle >
apply from: "archiving.gradle"

그러면 build.gradle 내에 helloWorld task 가 들어가게 되어, 아래처럼 gradle 명령어 실행이 가능함

gradle helloWorld



gradle 에서 제공하는 "base" 라는 plugin 을 사용할 수 있음
base plugin 은 유용한 빌드 기능을 제공함
예를 들어
- gradle clean : 정리 작업
- gradle assemble : 프로젝트 산출물을 jar 로 패키징 하는 작업
- gradle check : 프로젝트 테스트 실행하는 작업

plugin 추가는 아래처럼 하면 됨

< build.gradle >
apply plugin: "base"
apply from: "archiving.gradle"

혹은

plugins {
  id 'base'
}



Domain Object 는 프로젝트 구성 요소를 추상화한 객체임
Domain Object 는 Gradle 이 프로젝트를 구성하고 빌드를 실행하는 데 필요한 모든 주요 객체를 의미함
build.gradle 내에는 아래와 같은 Domain Object 들이 존재함



Project : Gradle 빌드의 최상위 도메인 객체임
하나의 프로젝트는 여러 서브 프로젝트를 포함할 수 있으며
각 프로젝트에는 자신만의 tasks, plugin, dependencies,configuration 이 존재함

build.gradle 내에서 아래처럼 쓰여짐

project.name = "EyeballsProject"



Task : Gradle 빌드에서 실행되는 작업들을 나타내는 객체임
tasks 는 빌드의 구체적인 단계를 정의하며, 컴파일이나 테스트, 패키징 등을 수행함

build.gradle 내에서 아래처럼 쓰여짐

task helloWorld {
  doLast {
    println "Hello World!"
  }
}



Configuration : 의존성 관리를 위한 configuration 을 나타내는 객체임
gradle 이 빌드하고 배포하는 대상 프로젝트 내에서 사용되는 라이브러리나 모듈을 정의함

build.gradle 내에서 아래처럼 쓰여짐

configurations {
  implementation ....
}



Repository : 외부 의존성(dependencies)을 가져올 때 사용하는 저장소를 나타냄
Maven, Ivy, Gradle Plugin Portal 등의 저장소 정의 가능

build.gradle 내에서 아래처럼 쓰여짐

repositories {
  mavenCentral()
}



SourceSet : 소스 코드와 그에 관련된 자원을 관련된 자원을 관리하는 객체임
각 sourceSets 은 특정 컴파일러와 설정과 함께 소스코드 파일과 리소스 파일을 정의함

build.gradle 내에서 아래처럼 쓰임

sourceSets {
  main {
    java {
      srcDirs = ['src/main/java']
    }
  }
}



gradle 에서 제공하는 "java plugin" 의 핵심 기능은 컴파일링(빌드), 테스팅, 그리고 번들링(배포)임
빌드는 java, scala 코드 class 들을 빌드해주고
배포는 class 들을 jar 파일로 아카이빙 해주는 것

만약 gradle 이 없었다면, 직접 javac 명령어와 jar 명령어를 통해 빌드하고 배포했을 것임
이러한 명령어 사용 대신, java plugin 의 기능을 이용하여 빌드 및 배포 가능




java plugin 을 사용하기 위해서, java plugin 에 필요한 코드 구조를 충족해줘야 함
아래와 같은 구조를 갖추면, java plugin 이 알아서 이를 인지하고
test, compile, bundle 작업에 사용한다고 함

-src
ㄴ main
    ㄴ java : production source code 가 포함된 dir
    ㄴ resources : 런타임에 사용되는 리소스들이 포함된 dir
ㄴ test
    ㄴ java : src/main/java 를 테스트하기 위한 code 가 포함된 dir
    ㄴ resources : 테스트에 사용되는 리소스들이 포함된 dir



java plugin 이 작업을 마친 후 생성한 결과들(classes, jar) 은 아래에 위치하게 됨

-build
ㄴ classes : java plugin 이 compile 한 class 파일들이 저장되는 dir
ㄴ libs : java plugin 이 생성한 jar 파일이 저장되는 dir
ㄴ resources 





build.gradle 은 최상위 root dir 에 위치시키면 됨
build.gradle 내에 아래와 같이 추가하면 됨

plugins {
  id 'java'
}


build.gradle 위치
https://docs.gradle.org/current/userguide/java_plugin.html

 

 

 


java plugin 의 명령어 중 compileJava 는
src/main/java 에 위치한 production source code 를 컴파일하는 작업을 진행함

java compile 가 실행 가능하도록 PATH 환경 변수 설정이 되어있어야 하며
컴파일 후 생성되는 바이너리 파일들(class 파일들)은 build/classes 에 저장됨

아래처럼 complieJava 명령어 실행

./gradlew compileJava --console=verbose



java plugin 의 명령어 중 processResources 는
src/main/resources 에 위치한 리소스 파일들을 build/resources/main dir 에 옮기는(copy) 작업을 진행함

추후 jar 파일을 만들 때 build dir 아래 있는 것들을 jar 로 만드는데
이 때 리소스 파일들까지 함께 jar 에 넣기 위해
위와 같은 작업을 하는 것임



java plugin 의 명령어 중 classes 는
위에서 실행한 두 명령어( compileJava  , processResources ) 를 한 번에 실행하는 작업임



java plugin 이 컴파일 할 때 자바 버전을 직접 지정할 수 있음
그리고 compile 하다가 이슈가 발생하면 어떻게 하고 싶은지도 지정 가능
이를테면 나는 JAVA 11 버전을 사용하고 싶다! 그러면 build.gradle 을 아래와 같이 설정하면 됨

plugins {
  id 'java'
}

java {
  sourceCompatibility = JavaVersion.VERSION_11   // compile 할 production java code 의 버전
  targetCompatibility = JavaVersion.VERSION_11   // 앞으로 생성될 class 파일이 java 11 버전에서 실행할 수 있도록 설정
}

compileJava {
  options.compilerArgs << '-Werror'
}




아래는 sourceCompatibility targetCompatibility 의 차이를 chatgpt 에게 물어본 결과임

Gradle에서 sourceCompatibility와 targetCompatibility는 Java 컴파일러가 소스 코드와 바이트코드를 처리하는 방식을 제어하는 속성입니다. 이 두 속성은 컴파일 시 호환성을 설정하는 데 사용되며, 각각의 의미는 다음과 같습니다.

1. sourceCompatibility

의미: 소스 코드가 어떤 버전의 Java 문법과 API를 사용할 수 있는지를 정의합니다.

설정하는 값: 이 값은 소스 코드를 컴파일할 때, 컴파일러가 해당 Java 버전에서 허용된 문법과 API를 사용하도록 제한합니다.

사용 예시: 만약 sourceCompatibility = '1.8'으로 설정하면, 소스 코드는 Java 8의 문법과 API를 사용할 수 있습니다. 그 이후 버전(Java 9, Java 10 등)의 기능을 사용하려고 하면 컴파일 오류가 발생합니다.

sourceCompatibility = '1.8'


2. targetCompatibility

의미: 컴파일된 바이트코드가 어떤 Java 런타임 버전에서 실행될 수 있는지를 정의합니다.

설정하는 값: 이 값은 컴파일된 바이트코드가 어떤 버전의 JVM에서 실행될 수 있는지를 결정합니다. 즉, 컴파일 결과물이 해당 버전의 JVM과 호환되는 바이트코드로 변환됩니다.

사용 예시: targetCompatibility = '1.8'으로 설정하면, 컴파일된 바이트코드는 Java 8 버전의 JVM에서 실행할 수 있게 컴파일됩니다. 만약 이 값을 Java 7으로 설정하면, 더 낮은 버전의 JVM에서도 실행할 수 있게 바이트코드를 생성하지만, 소스 코드의 문법이나 API는 여전히 sourceCompatibility에 의해 제한됩니다.

targetCompatibility = '1.8'


주요 차이점

**sourceCompatibility**는 소스 코드 레벨에서 사용할 수 있는 Java 버전을 설정합니다. 즉, 어떤 Java 문법과 API를 사용할 수 있을지를 결정합니다.

**targetCompatibility**는 바이트코드 레벨에서 어떤 Java 버전과 호환될지를 설정합니다. 즉, 컴파일된 클래스 파일이 어떤 JVM에서 실행될 수 있는지를 결정합니다.


실제 사용 시나리오

1. 같은 버전으로 설정: 대부분의 경우, sourceCompatibility와 targetCompatibility는 같은 버전으로 설정됩니다. 예를 들어, Java 8을 대상으로 하고, 그 버전에서 실행되도록 만들 때:

sourceCompatibility = '1.8'
targetCompatibility = '1.8'


2. 호환성을 위해 다른 버전으로 설정: 예를 들어, 최신 Java 문법을 사용하지만, 더 오래된 JVM에서 실행할 수 있는 코드를 생성하고 싶다면 sourceCompatibility를 최신 버전으로, targetCompatibility를 더 낮은 버전으로 설정할 수 있습니다.

Java 11 문법을 사용하고, Java 8 JVM에서 실행되도록 설정:


sourceCompatibility = '11'
targetCompatibility = '1.8'

이 설정은 Java 11의 문법을 사용하되, 최종적으로 Java 8에서 실행 가능한 바이트코드로 컴파일됩니다. 단, 이 경우 Java 8에서 존재하지 않는 API를 사용하려고 하면 런타임 오류가 발생할 수 있습니다.



요약

sourceCompatibility: 소스 코드가 사용할 수 있는 Java 문법과 API를 정의.

targetCompatibility: 컴파일된 바이트코드가 실행될 JVM 버전을 정의.


이 두 설정을 적절히 사용하면, 특정 Java 버전에서 코드를 작성하고, 더 넓은 범위의 JVM에서 실행되도록 조정할 수 있습니다.





--console=verbose 옵션이 붙으면, 빌드 실행 내용을 콘솔에 더 자세하게 출력함
--console=plain 옵션이 붙으면 단순한 텍스트 모드로 출력함
--console=rich 옵션이 붙으면 콘솔에 컬러 및 애니메이션 적용하여 빌드 진행 상태를 시각적으로 출력함. 이게 default 값임
--console=auto 옵션이 붙으면 콘솔 환경에 따라 rich 혹은 plain 으로 자동 설정됨



jar 파일 (java archive 파일) 을 생성하는 jar 명령어가 실행될 때
만약 compile 되지 않아서 class 파일이 존재하지 않는다면,
자동으로 compile 명령어를 실행하고 jar 파일을 만든다고 함
(굳이 직접 compile 명령어( compileJava  , processResources , classes )를 실행하지 않아도 된다는 말인 듯)

jar 명령어는 아래처럼 사용 가능

./gradlew jar

이렇게 생성된 jar 파일은 build/lib dir 에 생성됨


생성된 jar 파일 이름은 01_01.jar 처럼 의미없이 생성됨 
우리가 아래처럼 build.gradle 에 jar 를 추가함으로써, 직접 이름을 지정할 수 있음

jar {
  archiveBaseName = 'eyeballs'
}

그럼 build/lib/eyeballs.jar 가 생성됨



아래처럼 jar 파일에 버전도 추가 가능

version = '1.0.0'
jar {
  archiveBaseName = 'eyeballs'
}

그럼 build/lib/eyeballs-1.0.0.jar 가 생성됨



build.gradle 에 application plugin 을 추가하면
production source code 를 간단하게 실행해줌

plugins {
  id 'application'
}
application {
  mainClass = 'com.tistory.eyeballs.Main'
}

./gradlew run

만약 실행할 때 argument 도 같이 넣어 실행하고 싶다면 run 명령어 뒤에 --args 를 붙이면 됨

./gradlew run --args="2024 eyeballs"



Gradle Dependency Management Engine 은 maven 같은 repository 로부터 lib 를 다운받고 프로젝트에 적용함
빌드하는 동안  lib(dependencies) 를 다운받고, local cache 에 저장하고
project 의 classpath 에 추가하여 project 에서 해당 lib 를 사용할 수 있도록 함

또한, 서로 의존성이 있는 다양한 modules 로 구성된 프로젝트에서
modules 간 의존성을 설정할 수 있음.
이를 multi-project build 라고 부름

또한, artifact 를 생성하고 repository 에 등록하여,
다른 end user 가 다운받아 사용할 수 있도록 만들 수 있음
이를 publishing 이라고 함



dependencies 는 build.gradle 에 아래와 같이 등록 가능함
예를 들어 maven repo 로부터 commons-cli:commons-cli:1.4 라는 lib 를 다운받고 프로젝트에 추가한다고 하자

repositories {
  mavenCentral()   // maven repo 를 등록하여, 다운받을 lib 를 찾을 수 있도록 함
}

dependencies {
  implementation 'commons-cli:commons-cli:1.4'   // 다운받아 프로젝트에 적용할 lib 를 여기 추가
}



참고로 repo 에 들어있는 lib 의 형태는 아래와 같이 group, artifact, version 세 가지 정보로 이루어져 있음



Gradle 에 dependencies 를 계속 추가하여 복잡해져도,
"dependencies task" 를 이용하여 한 눈에 tree 형태로 dependencies 를 볼 수 있음

아래와 같은 명령어로 실행하면 됨

./gradlew dependencies



이 dependency 가 왜 필요한지, 어디서 왔는지 등을 알고 싶다면
"dependencyInsight task" 를 실행하여 확인할 수 있음

아래와 같은 명령어로 실행하면 됨

./gradlew dependencyInsight


" dependencyReport task" 도 사용해보자

./gradlew dependencyReport



app 과 api 라는 두 개의 프로젝트 존재하고,
app 은 api 의 class 를 사용하는, 즉 api 에 의존성을 갖는 프로젝트라고 하자

이런 상황에서 gradle 이 이슈 없이 app 을 빌드하려면 어떻게 해야 할까?
gradle 통해 app 과 api 프로젝트들의 서로 간 의존성을 관리하고 빌드하면 됨
이를 multi project build 라고 함




root dir 에 위치한 settings.gradle 에 아래와 같이 추가함

< settings.gradle >
rootProject.name = 'eyeballsProject'
include ':app', ':api'   // app, api 두 프로젝트를 나의 'eyeballsProject' 의 하위 프로젝트로 추가함


그리고 프로젝트의 하위 프로젝트들을 표시하는 명령어인 project task 를 실행하여
잘 추가되었는지 확인함

./gradlew projects

결과 : 
Root project 'eyeballsProject'
+--- Project ':api'
\--- Project ':app'



app 이 빌드되기 위해선 app 에서 api 의 class 를 import 할 수 있어야 함
이를 위해, 아래와 같이 app/build.gradle 의 dependencies 에 api 프로젝트를 추가해 줌


< app/build.gradle >

dependencies {
  implementation project(':api')
}

그리고 전체 프로젝트르 빌드하면 app 과 api 프로젝트 간 의존성이 해소되어 문제 없이 빌드됨

./gradlew classes



추가로, multi project build 를 위해
하위 프로젝트들을 include 로 추가하면 얻는 이점에 대해
Gemini 에게 물어본 결과는 아래와 같음


include 키워드를 사용하여 하위 프로젝트를 추가하는 가장 큰 이유는 다음과 같습니다.

1. 멀티 모듈 프로젝트 관리:
하나의 루트 프로젝트: 여러 개의 관련된 모듈을 하나의 루트 프로젝트 아래에서 관리할 수 있습니다. 이는 프로젝트의 복잡성을 줄이고 유지보수를 용이하게 합니다.
모듈 간 의존성 관리: 각 모듈은 독립적으로 개발하고 테스트할 수 있으며, 필요한 경우 다른 모듈에 의존하도록 설정할 수 있습니다.
2. 모듈별 빌드 설정:
모듈별 설정: 각 모듈마다 다른 빌드 설정을 적용할 수 있습니다. 예를 들어, 특정 모듈은 Java로 개발되고 다른 모듈은 Kotlin으로 개발될 수 있습니다.
유연한 빌드: 다양한 유형의 모듈을 하나의 프로젝트에서 관리할 수 있어 빌드 프로세스를 유연하게 구성할 수 있습니다.
3. 코드 재사용:
공통 모듈: 여러 프로젝트에서 공통적으로 사용되는 코드를 별도의 모듈로 분리하여 관리하고 재사용할 수 있습니다.
코드 중복 방지: 코드 중복을 줄이고 유지보수 비용을 절감할 수 있습니다.
4. 모듈별 테스트:
모듈별 테스트: 각 모듈에 대한 단위 테스트를 수행하고, 통합 테스트를 통해 모듈 간의 상호 작용을 검증할 수 있습니다.
테스트 효율성 향상: 모듈별로 테스트를 수행하여 테스트 커버리지를 높이고 테스트 시간을 단축할 수 있습니다.
5. 모듈별 배포:
모듈별 배포: 각 모듈을 독립적으로 배포할 수 있습니다.
배포 유연성: 필요에 따라 특정 모듈만 배포하거나, 여러 모듈을 함께 배포할 수 있습니다.



./gradlew installDist 명령어는, Gradle 프로젝트를 독립 실행 가능한 배포판으로 만드는 명령
해당 프로젝트를 다른 환경에서 별도의 Gradle 설치 없이 실행할 수 있도록
모든 필요한 파일과 설정을 하나의 디렉토리에 패키징하는 작업을 수행함

해당 명령어가 실행되면 프로젝트의 최상위 디렉토리에 build/install 디렉토리가 생성되고
그 안에 완성된 배포판이 포함됨

해당 명령어를 통해 생성되는 파일 구조
- bin 디렉토리: 프로젝트를 실행하는 데 필요한 실행 스크립트 (예: gradlew, gradlew.bat)가 포함됨
- lib 디렉토리: 프로젝트에서 사용하는 모든 라이브러리 파일이 포함됨
- conf 디렉토리: Gradle 설정 파일이 포함됨
- resources 디렉토리: 프로젝트의 리소스 파일이 포함됨




JUnit 은 많은 인정을 받는 표준 java test framework 임
Gradle Java plugin 은 test source code 를 찾아 실행(테스팅)함
예를 들어 아래와 같은 dir 를 갖춘 프로젝트에서 src/test/java/*.java 를 테스트 할 수 있음

src
ㄴmain
   ㄴjava
      ㄴMyClass.java
ㄴtest
   ㄴjava
      ㄴMyClassTest.java



test 를 위한 lib 인 Junit 을 dependencies 에 추가할때는 testImplementation configuration 을 사용함
testImpelentation 은 테스트 코드를 컴파일하고 실행하는 데 직접적으로 필요한 의존성을 지정.

testRuntime configuration 은 테스트 실행 시(Runtime)에만 필요한 의존성을 지정.
테스트 코드를 컴파일 할 때 필요하진 않음
하지만 테스트 실행 시에 필요한 추가적인 의존성을 설정할 수 있음

이렇게 test configuration 으로 추가된 lib 는 나중에 production 을 위한 배포파일을 만들 때, 배포 파일에 추가되지 않음
그리고 오로지 test code 만을 위해 사용됨, production source code 에는 적용되지 않음


< build.gradle >

dependencies {
  testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.0'
  testRuntime 'org.junit.jupiter:junit-jupiter-engine:5.7.0'
}

test {
  useJunitPlatform()
  testLogging {
    events 'started', 'skipped', 'failed'
  }
}

여기서 junit-jupiter-api 는, 개발자가 테스트 코드를 작성할 때 사용하는 API를 제공하는 lib
예를 들어 @BeforeEach, @AfterEach 등의 애노테이션을 제공한다거나, 
다양한 assertion 메소드를 제공함

junit-jupiter-engine 는, 실제로 테스트를 실행하고 결과를 만드는 lib
@Test 애노테이션이 달린 메소드를 찾아 테스트 대상으로 지정하고 테스트를 실행

useJunitPlatform() 은 빌드 프로세스에서 JUnit Platform을 테스트 실행 엔진으로 사용하겠다는 의미
Gradle의 'test' task 에 JUnit Platform을 설정하는 메소드이며
이 설정을 통해 Gradle은 프로젝트에서 JUnit 5 스타일의 테스트를 실행할 수 있도록 환경이 구성됨

testLogging 은 테스트 실행 시 출력되는 로그의 상세함과 내용을 조절하는 설정임
즉, 테스트 결과를 얼마나 자세하게 보여줄지를 설정
테스트를 시작할 때(started), 테스트가 실패했을 때(failed) 발생하는 이벤트를 콘솔로 출력해달라는 의미임



MyClassTest.java 는 아래와 같은 내용으로 구성 가능

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;

public class MyClassTest {
  private final MyClass myClass = new MyClass();

  @Test
  void test() {
    assertEquals("eyeballs", myClass.getName());
  }
}



test 코드를 생성한 이후에 실제로 Test 를 하기 위해
compileTestJava 명령어로 테스트 코드를 compile 함

./gradlew compileTestJava

그리고 test 명령어로 테스트 코드 실행

./gradlew test

test 를 마치고나면, JUnit 이 HTML 및 XML 로 리포팅 페이지를 만들어 줌
HTML 파일은 build/reports/tests/test/index.html 에 위치하며, 열어보면 테스트 결과를 볼 수 있음
XML 파일은 build/test-results/test/*.xml 에 위치하며, 열어보면 테스트 결과를 볼 수 있음



추가로, testImplementation 과 testRuntime 의 차이에 대해 Gemini 에게 물어본 결과

언제 어떤 configuration을 사용해야 할까요?

testImplementation:

  테스트 코드에서 직접 사용하는 의존성 (테스트 더블, 테스트 데이터 생성 라이브러리 등)
  테스트 코드 컴파일 시 필요한 의존성
testRuntime:
  테스트 실행 환경을 구축하는 데 필요한 의존성 (임베디드 DB, 웹 서버 등)
  테스트 코드 컴파일에는 필요하지 않지만, 테스트 실행 시에만 필요한 의존성

왜 구분하여 사용해야 할까요?


테스트 코드의 독립성: 테스트 코드를 생산 코드와 분리하여 관리하고, 테스트 환경에 필요한 의존성만 포함시킴으로써 테스트 코드의 독립성을 보장할 수 있습니다.

빌드 시간 단축: 테스트 실행에만 필요한 의존성은 컴파일 시에 포함되지 않으므로 빌드 시간을 단축할 수 있습니다.
의존성 관리: 테스트 환경에 필요한 의존성을 명확하게 관리하여 의존성 충돌을 방지하고 프로젝트를 안정적으로 유지할 수 있습니다.