CI/CD 학습 시리즈

목차

  1. Gradle 개요
  2. Gradle 플러그인이란?
  3. 핵심 개념
  4. 플러그인 개발 기초
  5. Extension (DSL 설정)
  6. Task 개발
  7. 플러그인 배포
  8. 실전 예제: app-builder-plugin (Part 4 참조)
  9. 실습: 간단한 플러그인 만들기
  10. 핵심 요약
  11. 참고 자료

1. Gradle 개요

1.1 Gradle이란?

Gradle은 JVM 기반의 빌드 자동화 도구입니다.

쉽게 말해서…

코드를 작성하고 나면 “컴파일 → 테스트 → 패키징 → 배포” 같은 작업을 해야 하는데, 이걸 매번 손으로 하면 너무 귀찮잖아요?

Gradle은 이런 반복 작업을 자동으로 해주는 도구예요. gradle build 한 번이면 알아서 다 해줍니다.

마치 세탁기처럼요. 빨래를 넣고 버튼만 누르면 “세탁 → 헹굼 → 탈수”를 자동으로 해주듯이, Gradle도 코드를 넣고 명령만 실행하면 빌드 과정을 자동으로 처리해줍니다.

┌─────────────────────────────────────────────────────────────┐
│                     빌드 도구 발전사                          │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  Ant (2000)  →  Maven (2004)  →  Gradle (2012)              │
│     │              │                │                        │
│     │              │                └─ Groovy/Kotlin DSL     │
│     │              │                   유연함 + 성능          │
│     │              │                                         │
│     │              └─ XML 기반, Convention over Config       │
│     │                 의존성 관리 도입                        │
│     │                                                        │
│     └─ XML 기반, 절차적 빌드                                  │
│                                                              │
└─────────────────────────────────────────────────────────────┘

1.2 Gradle vs Maven

Maven과 Gradle, 뭐가 다를까요?

둘 다 빌드 도구인데, Maven은 XML로 설정하고 Gradle은 코드(Groovy/Kotlin)로 설정해요.

비유하자면:

  • Maven = 정해진 양식에 맞춰 작성하는 공문서 (딱딱하지만 명확함)
  • Gradle = 자유롭게 쓸 수 있는 메모장 (유연하지만 배울 게 좀 있음)

요즘은 Gradle이 더 인기 있어요. 특히 빌드 속도가 빠르고, “이전에 빌드한 거랑 뭐가 바뀌었지?” 확인해서 바뀐 것만 다시 빌드해주거든요.

비교 Maven Gradle
설정 파일 pom.xml (XML) build.gradle (Groovy/Kotlin)
빌드 속도 느림 빠름 (증분 빌드, 캐싱)
유연성 제한적 매우 유연
학습 곡선 낮음 중간
플러그인 개발 복잡 상대적으로 쉬움
멀티 모듈 지원 더 강력한 지원

1.3 빌드 도구가 없다면?

순수 Java로 빌드하면 어떻게 될까요?

Gradle이나 Maven 같은 빌드 도구 없이 javac, java 명령어만으로 빌드하면… 지옥이에요 🔥

순수 Java로 빌드하기

# ═══════════════════════════════════════════════════════════
# 1. 의존성 직접 다운로드 (Maven Central에서 하나하나...)
# ═══════════════════════════════════════════════════════════
mkdir -p libs
curl -o libs/guava-32.0.0-jre.jar https://repo1.maven.org/.../guava-32.0.0-jre.jar

# 😱 전이 의존성도 직접 다운로드해야 함!
curl -o libs/failureaccess-1.0.1.jar https://repo1.maven.org/.../failureaccess-1.0.1.jar
curl -o libs/checker-qual-3.33.0.jar https://repo1.maven.org/.../checker-qual-3.33.0.jar
# ... guava 하나 쓰려면 의존성이 10개 넘음...

# ═══════════════════════════════════════════════════════════
# 2. 컴파일 (classpath 직접 나열)
# ═══════════════════════════════════════════════════════════
mkdir -p build/classes

# 모든 JAR를 classpath에 직접 나열해야 함
javac -d build/classes \
  -cp "libs/guava-32.0.0-jre.jar:libs/failureaccess-1.0.1.jar:libs/checker-qual-3.33.0.jar" \
  src/main/java/com/example/*.java

# ═══════════════════════════════════════════════════════════
# 3. 리소스 복사 (직접!)
# ═══════════════════════════════════════════════════════════
cp -r src/main/resources/* build/classes/

# ═══════════════════════════════════════════════════════════
# 4. JAR 생성 (MANIFEST.MF 직접 작성)
# ═══════════════════════════════════════════════════════════
echo "Manifest-Version: 1.0" > build/MANIFEST.MF
echo "Main-Class: com.example.Main" >> build/MANIFEST.MF
echo "Class-Path: libs/guava-32.0.0-jre.jar libs/failureaccess-1.0.1.jar" >> build/MANIFEST.MF

jar cfm build/myapp.jar build/MANIFEST.MF -C build/classes .

# ═══════════════════════════════════════════════════════════
# 5. 테스트 컴파일 & 실행 (JUnit 러너도 직접 실행)
# ═══════════════════════════════════════════════════════════
javac -d build/test-classes -cp "build/classes:libs/*" src/test/java/com/example/*.java
java -cp "build/classes:build/test-classes:libs/*" org.junit.runner.JUnitCore com.example.MyTest

가장 큰 문제: 전이 의존성

Spring Boot Starter Web 하나만 써도...
    │
    ├─ spring-boot-starter-web
    │   ├─ spring-boot-starter
    │   │   ├─ spring-boot
    │   │   ├─ spring-boot-autoconfigure
    │   │   ├─ spring-core
    │   │   └─ ... (10개+)
    │   ├─ spring-webmvc
    │   │   ├─ spring-aop
    │   │   ├─ spring-beans
    │   │   └─ ... (5개+)
    │   └─ spring-boot-starter-tomcat
    │       ├─ tomcat-embed-core
    │       └─ ... (3개+)
    │
    └─ 총 50개+ JAR를 직접 다운로드? 🤯

비교 요약

항목 순수 Java Gradle (플러그인 없이) Gradle + Java 플러그인
의존성 관리 직접 다운로드 😱 자동 자동
전이 의존성 직접 찾아서 다운 😱😱 자동 해결 자동 해결
컴파일 javac 직접 실행 설정 필요 (~50줄) 자동
classpath 직접 나열 설정 필요 자동
JAR 생성 jar + MANIFEST 직접 설정 필요 (~20줄) 자동
테스트 직접 실행 설정 필요 (~15줄) 자동
버전 충돌 직접 해결 😱😱😱 자동 해결 자동 해결
증분 빌드 불가능 지원 지원

빌드 도구의 가장 큰 가치

사실 빌드 도구의 핵심 가치는 의존성 관리예요! 특히 전이 의존성 자동 해결버전 충돌 해결이 없으면 현대적인 Java 개발은 거의 불가능해요.

  • 순수 Java = 지옥 🔥
  • Gradle (플러그인 없이) = 힘듦 😓
  • Gradle + Java 플러그인 = 천국 😇

1.4 Gradle 빌드 파일

build.gradle 파일이 뭐예요?

프로젝트의 “빌드 설명서“예요. “이 프로젝트는 Java 프로젝트야”, “Spring 라이브러리가 필요해”, “버전은 1.0.0이야” 이런 정보를 적어두는 파일이에요.

요리로 치면 레시피와 같아요. “재료(dependencies)는 뭐가 필요하고, 어떤 순서로 만들지(tasks)” 적어둔 거죠.

// build.gradle (Groovy DSL)
plugins {
    id 'java'
}

group = 'com.example'
version = '1.0.0'

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework:spring-core:5.3.0'
    testImplementation 'junit:junit:4.13'
}

tasks.register('hello') {
    doLast {
        println 'Hello, Gradle!'
    }
}
// build.gradle.kts (Kotlin DSL)
plugins {
    java
}

group = "com.example"
version = "1.0.0"

repositories {
    mavenCentral()
}

dependencies {
    implementation("org.springframework:spring-core:5.3.0")
    testImplementation("junit:junit:4.13")
}

tasks.register("hello") {
    doLast {
        println("Hello, Gradle!")
    }
}

1.5 Gradle 빌드 라이프사이클

Gradle은 어떤 순서로 동작할까요?

gradle build를 실행하면 Gradle은 3단계로 동작해요:

  1. 초기화: “어떤 프로젝트들이 있지?” 확인 (settings.gradle 읽기)
  2. 구성: “뭘 해야 하지?” 파악 (build.gradle 읽고 할 일 목록 만들기)
  3. 실행: “자, 이제 하자!” 실제 빌드 작업 수행

마치 요리사가:

  1. 냉장고 열어서 재료 확인하고 (초기화)
  2. 레시피 보면서 순서 정리하고 (구성)
  3. 실제로 요리하는 것 (실행)

    과 같아요!

┌─────────────────────────────────────────────────────────────┐
│                  Gradle 빌드 라이프사이클                      │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  1. 초기화 (Initialization)                                  │
│     └─ settings.gradle 읽기                                  │
│     └─ 어떤 프로젝트가 빌드에 포함되는지 결정                   │
│                                                              │
│  2. 구성 (Configuration)                                     │
│     └─ build.gradle 실행                                     │
│     └─ Task 그래프 생성                                       │
│     └─ 플러그인 적용, 의존성 해석                              │
│                                                              │
│  3. 실행 (Execution)                                         │
│     └─ 선택된 Task 실행                                       │
│     └─ 의존성 순서대로 실행                                    │
│                                                              │
└─────────────────────────────────────────────────────────────┘

$ gradle build
    │
    ▼
[초기화] settings.gradle 읽기
    │
    ▼
[구성] build.gradle 실행, Task 그래프 생성
    │
    ▼
[실행] compileJava → processResources → classes → jar → build

2. Gradle 플러그인이란?

한 줄 요약: 플러그인 = 기능 확장팩

게임에서 DLC(다운로드 콘텐츠) 깔면 새로운 기능이 생기잖아요? Gradle 플러그인도 똑같아요. 설치하면 새로운 기능이 추가돼요!

2.1 플러그인의 역할

플러그인 = 재사용 가능한 빌드 로직을 패키징한 것

왜 플러그인이 필요할까요?

예를 들어, Java 프로젝트를 빌드하려면:

  • 소스 코드 컴파일하고
  • 테스트 실행하고
  • JAR 파일 만들고…

이걸 매번 직접 설정하면 엄청 귀찮겠죠?

plugins { id 'java' } 한 줄만 쓰면, 누군가 미리 만들어둔 “Java 빌드 기능”이 뿅! 하고 추가돼요.

마치 스마트폰에 앱 설치하듯이, Gradle에 플러그인을 설치하면 새로운 기능을 쓸 수 있는 거예요.

┌─────────────────────────────────────────────────────────────┐
│                    플러그인이 하는 일                          │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  1. Task 추가                                                │
│     └─ compileJava, test, jar 등                            │
│                                                              │
│  2. 설정(Convention) 추가                                    │
│     └─ sourceCompatibility, targetCompatibility 등          │
│                                                              │
│  3. Extension 추가 (DSL)                                     │
│     └─ java { }, application { } 등                         │
│                                                              │
│  4. 다른 플러그인 적용                                        │
│     └─ java 플러그인은 base 플러그인을 자동 적용               │
│                                                              │
└─────────────────────────────────────────────────────────────┘

IntelliJ Gradle 창에서 확인하기

IntelliJ의 Gradle 창에 보이는 Task들은 전부 플러그인이 등록한 것!

┌─────────────────────────────────────────────────────────────┐
│            IntelliJ Gradle 창에 보이는 Task들                 │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  Tasks                                                       │
│  ├── application          ← application 플러그인            │
│  │   └── bootRun          ← spring-boot 플러그인            │
│  │                                                           │
│  ├── build                ← java (base) 플러그인            │
│  │   ├── assemble                                           │
│  │   ├── bootBuildImage   ← spring-boot 플러그인            │
│  │   ├── bootJar          ← spring-boot 플러그인            │
│  │   ├── build                                              │
│  │   ├── classes          ← java 플러그인                   │
│  │   ├── clean            ← base 플러그인                   │
│  │   ├── jar              ← java 플러그인                   │
│  │   └── testClasses      ← java 플러그인                   │
│  │                                                           │
│  ├── publishing           ← maven-publish 플러그인          │
│  ├── verification         ← java 플러그인 (test 등)         │
│  └── other                                                  │
│      ├── GetTargets       ← app-builder 플러그인            │
│      └── AppBuilder       ← app-builder 플러그인            │
│                                                              │
└─────────────────────────────────────────────────────────────┘

플러그인별 등록 Task

플러그인 등록하는 Task
base clean, assemble, build, check
java compileJava, classes, jar, test, testClasses
application run, startScripts, installDist
spring-boot bootRun, bootJar, bootBuildImage
kotlin compileKotlin, buildKotlinToolingMetadata
maven-publish publish, publishToMavenLocal
app-builder GetTargets, AppBuilder
# 프로젝트에서 사용 가능한 모든 Task 확인
$ gradle tasks --all

# 특정 Task가 어디서 왔는지 확인
$ gradle help --task bootJar

결론: 플러그인 추가하면 Task가 늘어나고, 제거하면 Task가 사라져요! plugins { } 블록이 비어있으면 Task도 거의 없어요.

2.2 Java 플러그인 상세

plugins { id 'java' } 한 줄이면 뭐가 생길까요?

Java 플러그인은 가장 기본적인 Core 플러그인이에요. 이 한 줄만 추가하면 Java 프로젝트 빌드에 필요한 모든 것이 자동으로 설정돼요!

소스셋(Source Sets) 정의

Java 플러그인은 표준 디렉토리 구조를 자동으로 인식해요:

src/
├── main/
│   ├── java/        # 프로덕션 소스 코드
│   └── resources/   # 프로덕션 리소스
└── test/
    ├── java/        # 테스트 소스 코드
    └── resources/   # 테스트 리소스

태스크(Tasks) 추가

태스크 설명
compileJava Java 소스 컴파일
processResources 리소스 파일 복사
classes 컴파일 + 리소스 처리
compileTestJava 테스트 소스 컴파일
test 단위 테스트 실행
jar JAR 파일 생성
clean 빌드 디렉토리 삭제
build 전체 빌드 수행

의존성 구성(Configurations)

구성 설명
implementation 컴파일 및 런타임 의존성
compileOnly 컴파일 시에만 필요 (예: Lombok)
runtimeOnly 런타임에만 필요 (예: JDBC 드라이버)
testImplementation 테스트용 의존성
testRuntimeOnly 테스트 런타임에만 필요

빌드 출력 구조

build/
├── classes/java/main/    # 컴파일된 클래스
├── resources/main/       # 처리된 리소스
├── libs/                 # 생성된 JAR
└── reports/tests/        # 테스트 리포트

플러그인 있을 때 vs 없을 때 비교

직접 비교해보면 플러그인의 가치를 확실히 알 수 있어요!

With plugins { id 'java' } (약 10줄)

plugins {
    id 'java'
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'com.google.guava:guava:32.0.0-jre'
    testImplementation 'junit:junit:4.13.2'
}

끝! gradle build 하면 컴파일, 테스트, JAR 생성 다 됨.

Without Java 플러그인 (약 100줄+)

// 플러그인 없이 시작
plugins {
    id 'base'  // 최소한의 기반 플러그인만
}

// ═══════════════════════════════════════════════════════════
// 1. 의존성 구성(Configurations) 직접 생성
// ═══════════════════════════════════════════════════════════
configurations {
    implementation
    compileOnly
    runtimeOnly
    testImplementation
    testCompileOnly
    testRuntimeOnly

    // 상속 관계 설정
    testImplementation.extendsFrom(implementation)
    testRuntimeOnly.extendsFrom(runtimeOnly)
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'com.google.guava:guava:32.0.0-jre'
    testImplementation 'junit:junit:4.13.2'
}

// ═══════════════════════════════════════════════════════════
// 2. 소스 디렉토리 직접 정의
// ═══════════════════════════════════════════════════════════
def mainJava = file('src/main/java')
def mainResources = file('src/main/resources')
def testJava = file('src/test/java')
def testResources = file('src/test/resources')

def classesDir = file("${buildDir}/classes/java/main")
def testClassesDir = file("${buildDir}/classes/java/test")
def resourcesDir = file("${buildDir}/resources/main")
def testResourcesDir = file("${buildDir}/resources/test")

// ═══════════════════════════════════════════════════════════
// 3. 컴파일 태스크 직접 정의
// ═══════════════════════════════════════════════════════════
tasks.register('compileJava', JavaCompile) {
    source = fileTree(mainJava)
    destinationDirectory = classesDir
    classpath = configurations.implementation
    sourceCompatibility = '17'
    targetCompatibility = '17'
}

tasks.register('compileTestJava', JavaCompile) {
    source = fileTree(testJava)
    destinationDirectory = testClassesDir
    classpath = configurations.testImplementation + files(classesDir)
    sourceCompatibility = '17'
    targetCompatibility = '17'

    dependsOn 'compileJava'
}

// ═══════════════════════════════════════════════════════════
// 4. 리소스 복사 태스크 직접 정의
// ═══════════════════════════════════════════════════════════
tasks.register('processResources', Copy) {
    from mainResources
    into resourcesDir
}

tasks.register('processTestResources', Copy) {
    from testResources
    into testResourcesDir
}

// ═══════════════════════════════════════════════════════════
// 5. classes 태스크 (컴파일 + 리소스)
// ═══════════════════════════════════════════════════════════
tasks.register('classes') {
    dependsOn 'compileJava', 'processResources'
}

tasks.register('testClasses') {
    dependsOn 'compileTestJava', 'processTestResources'
}

// ═══════════════════════════════════════════════════════════
// 6. JAR 태스크 직접 정의
// ═══════════════════════════════════════════════════════════
tasks.register('jar', Jar) {
    from classesDir
    from resourcesDir

    archiveBaseName = project.name
    archiveVersion = project.version
    destinationDirectory = file("${buildDir}/libs")

    manifest {
        attributes(
                'Implementation-Title': project.name,
                'Implementation-Version': project.version
        )
    }

    dependsOn 'classes'
}

// ═══════════════════════════════════════════════════════════
// 7. 테스트 태스크 직접 정의
// ═══════════════════════════════════════════════════════════
tasks.register('test', Test) {
    testClassesDirs = files(testClassesDir)
    classpath = configurations.testRuntimeOnly +
            files(classesDir, testClassesDir, resourcesDir, testResourcesDir)

    useJUnit()  // 또는 useJUnitPlatform()

    reports {
        html.required = true
        html.outputLocation = file("${buildDir}/reports/tests")
    }

    dependsOn 'testClasses'
}

// ═══════════════════════════════════════════════════════════
// 8. build 태스크 연결
// ═══════════════════════════════════════════════════════════
tasks.register('build') {
    dependsOn 'jar', 'test'
}

비교 요약

항목 With 플러그인 Without 플러그인
코드량 ~10줄 ~100줄+
소스셋 자동 인식 직접 정의
Configurations 자동 생성 직접 생성
태스크 자동 등록 직접 등록
태스크 의존성 자동 연결 직접 연결
증분 빌드 최적화됨 직접 구현 필요
에러 처리 잘 되어있음 직접 구현 필요

한 줄 요약

plugins { id 'java' } 한 줄이 100줄 이상의 보일러플레이트를 대신해줘요! 실제 Java 플러그인은 증분 빌드 최적화, 캐싱, 에러 처리 등 훨씬 더 많은 기능이 있어서 직접 구현하려면 수백 줄이 될 수도 있어요.

2.3 Spring Boot 플러그인 상세

plugins { id 'org.springframework.boot' } 하면 뭐가 달라질까요?

Java 플러그인이 “Java 프로젝트 빌드”를 도와준다면, Spring Boot 플러그인은 “실행 가능한 애플리케이션 패키징“을 도와줘요!

실행 가능한 Fat JAR 생성

일반 JAR (java 플러그인):
├── com/example/MyApp.class     # 내 코드만 있음
└── META-INF/

Fat JAR (Spring Boot 플러그인):
├── BOOT-INF/
│   ├── classes/                # 내 코드
│   └── lib/                    # 모든 의존성 JAR 포함!
│       ├── spring-core-6.x.jar
│       ├── spring-web-6.x.jar
│       └── ...
├── org/springframework/boot/loader/  # Spring Boot 로더
└── META-INF/MANIFEST.MF

Fat JAR 덕분에 java -jar myapp.jar 한 번에 실행 가능!

태스크(Tasks) 추가

태스크 설명
bootJar 실행 가능한 Fat JAR 생성
bootWar 실행 가능한 WAR 생성
bootRun 애플리케이션 바로 실행
bootBuildImage Docker 이미지 생성 (Buildpacks)

의존성 버전 자동 관리

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.2.0'
    id 'io.spring.dependency-management' version '1.1.4'  // 보통 같이 사용
}

dependencies {
    // 버전 안 써도 됨! Spring Boot가 호환되는 버전 자동 설정
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

기존 jar 태스크와의 관계

// Spring Boot 플러그인 적용하면:
tasks.named('jar') {
    enabled = false  // 일반 jar는 비활성화
}

tasks.named('bootJar') {
    enabled = true   // bootJar가 대신 실행됨
}

빌드 정보 생성

springBoot {
    buildInfo()  // build-info.properties 생성
}

// 애플리케이션에서 사용 가능
@Autowired
BuildProperties buildProperties;  // 버전, 빌드 시간 등

Java 플러그인과의 비교

항목 java 플러그인 spring-boot 플러그인
JAR 생성 일반 JAR (클래스만) Fat JAR (의존성 포함)
실행 방식 classpath 설정 필요 java -jar 바로 실행
의존성 버전 직접 명시 BOM으로 자동 관리
개발 실행 직접 설정 bootRun 제공
Docker 직접 설정 bootBuildImage 제공

2.4 플러그인 종류

플러그인도 종류가 있어요

  1. Core 플러그인: Gradle에 기본으로 들어있는 것 (무료 기본 앱 같은 거)
  2. Community 플러그인: 다른 사람들이 만들어서 공유한 것 (앱스토어 앱)
  3. Custom 플러그인: 우리가 직접 만드는 것 (직접 개발한 앱)
┌─────────────────────────────────────────────────────────────┐
│                      플러그인 종류                            │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  1. Core 플러그인 (Gradle 내장)                              │
│     └─ java, application, war, groovy 등                    │
│     └─ plugins { id 'java' }                                │
│                                                              │
│  2. Community 플러그인 (Gradle Plugin Portal)               │
│     └─ org.springframework.boot, com.github.node-gradle 등  │
│     └─ plugins { id 'org.springframework.boot' version '3.0'}│
│                                                              │
│  3. Custom 플러그인 (직접 개발)                               │
│     └─ 회사/프로젝트 맞춤형 빌드 로직                          │
│     └─ plugins { id 'com.example.app-builder' version '1.0'} │
│                                                              │
└─────────────────────────────────────────────────────────────┘

3가지 플러그인 비교

구분 Core Community Custom
예시 java org.springframework.boot app-builder
만든 곳 Gradle 팀 Spring 팀 (VMware) 우리 회사
배포 위치 Gradle 내장 Gradle Plugin Portal 사설 Nexus
버전 명시 불필요 필요 필요
소스 코드 Gradle GitHub Spring GitHub 사내 저장소

결국 같은 구조!

핵심 포인트

Core든, Community든, Custom이든 전부 같은 Gradle 플러그인 시스템으로 만들어져요!

즉, plugins { id 'java' }plugins { id 'org.springframework.boot' }나 우리가 만든 plugins { id 'app-builder' }내부 구조는 동일해요.

// 1. Core: Java 플러그인 (Gradle 팀이 만듦)
class JavaPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        // Extension 등록 → java { sourceCompatibility = 17 }
        // Task 등록 → compileJava, jar, test 등
    }
}

// 2. Community: Spring Boot 플러그인 (Spring 팀이 만듦)
class SpringBootPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        // Extension 등록 → springBoot { buildInfo() }
        // Task 등록 → bootJar, bootRun 등
    }
}

// 3. Custom: App Builder 플러그인 (우리가 만듦)
class AppBuilderPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        // Extension 등록 → appBuilder { ecrRegistry = '...' }
        // Task 등록 → GetTargets, AppBuilder 등
    }
}
┌─────────────────────────────────────────────────────────────┐
│              전부 같은 Plugin<Project> 구현!                  │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  plugins { id 'java' }                                      │
│  plugins { id 'org.springframework.boot' version '3.2.0' }  │
│  plugins { id 'app-builder' version '1.0.0' }               │
│       │           │               │                          │
│       ▼           ▼               ▼                          │
│  ┌─────────────────────────────────────────────────────┐    │
│  │           Plugin<Project>.apply(project)            │    │
│  │                                                     │    │
│  │  1. Extension 등록 (DSL 설정 블록)                   │    │
│  │  2. Task 등록 (실행할 작업)                          │    │
│  │  3. Convention 설정 (기본값)                         │    │
│  └─────────────────────────────────────────────────────┘    │
│                                                              │
└─────────────────────────────────────────────────────────────┘

결론: 플러그인 개발을 배우면 Gradle 생태계의 모든 플러그인이 어떻게 동작하는지 이해할 수 있어요!

2.5 플러그인 적용 방법

// 1. Core 플러그인
plugins {
    id 'java'
}

// 2. Community 플러그인 (버전 필요)
plugins {
    id 'org.springframework.boot' version '3.0.0'
}

// 3. Custom 플러그인 (로컬 또는 사설 저장소)
plugins {
    id 'com.example.plugins.app-builder-gradle-plugin' version '1.0.0'
}

// 저장소 설정 (settings.gradle.kts)
pluginManagement {
    repositories {
        gradlePluginPortal()
        maven { url = uri("https://nexus.company.com/repository/maven-public/") }
    }
}

3. 핵심 개념

플러그인 개발 전에 알아야 할 3가지

Gradle 플러그인을 만들려면 3가지 개념만 알면 돼요:

개념 쉬운 설명 비유
Project 빌드 대상 요리할 재료
Task 실행할 작업 요리 레시피의 각 단계
Extension 설정값 요리 양념 조절 (소금 많이/적게)

이 3가지만 이해하면 플러그인 개발의 80%는 끝이에요!

3.1 Project

Project = 빌드의 기본 단위

Project가 뭐예요?

간단해요. 빌드할 프로젝트 그 자체예요.

build.gradle 파일이 있는 폴더 하나가 Project 하나예요. 멀티 모듈 프로젝트면 여러 개의 Project가 있는 거고요.

플러그인 입장에서 Project는 “내가 작업할 대상”이에요. “이 프로젝트에 어떤 Task를 추가할까?”, “어떤 설정을 적용할까?” 하는 거죠.

// build.gradle에서 암시적으로 사용 가능
project.name        // 프로젝트 이름
project.version     // 버전
project.group       // 그룹
project.buildDir    // 빌드 디렉토리
project.projectDir  // 프로젝트 디렉토리
project.rootDir     // 루트 디렉토리

// 멀티 모듈에서
project.parent      // 부모 프로젝트
project.subprojects // 자식 프로젝트들
project.allprojects // 모든 프로젝트
// 플러그인 코드에서 (Kotlin)
class MyPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        println("프로젝트 이름: ${project.name}")
        println("빌드 디렉토리: ${project.buildDir}")
    }
}

3.2 Task

Task = 실행 가능한 작업 단위

Task가 뭐예요?

gradle build하면 실행되는 하나하나의 작업이에요.

  • compileJava → Java 코드 컴파일하는 Task
  • test → 테스트 실행하는 Task
  • jar → JAR 파일 만드는 Task

레시피에서 “1. 양파 썰기”, “2. 볶기”, “3. 간 맞추기” 같은 각 단계라고 생각하면 돼요.

Task끼리 순서가 있어요. “컴파일 끝나야 테스트할 수 있다” 이런 식으로요. 이걸 Task 의존성이라고 해요.

┌─────────────────────────────────────────────────────────────┐
│                       Task 개념                              │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  $ gradle build                                              │
│       │                                                      │
│       ▼                                                      │
│  ┌─────────┐    ┌─────────┐    ┌─────────┐    ┌─────────┐  │
│  │ compile │ → │  test   │ → │   jar   │ → │  build  │     │
│  │  Java   │    │         │    │         │    │         │   │
│  └─────────┘    └─────────┘    └─────────┘    └─────────┘   │
│       │              │              │              │         │
│       └──────────────┴──────────────┴──────────────┘         │
│                    Task 의존성 체인                           │
│                                                              │
└─────────────────────────────────────────────────────────────┘
// Task 정의 (build.gradle)
tasks.register('hello') {
    group = 'custom'
    description = '인사 출력'

    doFirst {
        println 'Task 시작!'
    }

    doLast {
        println 'Hello, World!'
    }
}

// Task 의존성
tasks.register('goodbye') {
    dependsOn 'hello'

    doLast {
        println 'Goodbye!'
    }
}

// $ gradle goodbye
// 출력:
// Task 시작!
// Hello, World!
// Goodbye!

3.3 Extension

Extension = 플러그인의 DSL 설정

Extension이 뭐예요?

플러그인 사용자가 설정을 바꿀 수 있게 해주는 거예요.

예를 들어 app-builder 플러그인을 쓸 때:

appBuilder {
    ecrRegistry = 'xxx.dkr.ecr.ap-northeast-2.amazonaws.com'
}

이렇게 설정하잖아요? 이 appBuilder { } 블록이 Extension이에요!

비유하자면:

  • 플러그인 = 전자레인지
  • Extension = 전자레인지의 버튼들 (시간 설정, 온도 설정)

Extension 덕분에 같은 플러그인이라도 프로젝트마다 다르게 설정할 수 있어요.

// java 플러그인이 제공하는 Extension
java {
    sourceCompatibility = JavaVersion.VERSION_17
    targetCompatibility = JavaVersion.VERSION_17
}

// application 플러그인이 제공하는 Extension
application {
    mainClass = 'com.example.Main'
}

// Custom Extension (app-builder-plugin)
appBuilder {
    ecrRegistry = 'xxx.dkr.ecr.region.amazonaws.com'
    helmValuesRepo = 'https://github.com/org/helm-values.git'
}

3.4 전체 구조

┌─────────────────────────────────────────────────────────────┐
│                   Gradle 플러그인 구조                        │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  Plugin (플러그인)                                           │
│    │                                                         │
│    ├─ Extension 등록 (DSL 설정)                              │
│    │    └─ appBuilder { ecrRegistry = '...' }               │
│    │                                                         │
│    ├─ Task 등록                                              │
│    │    └─ GetTargets, AppBuilder 등                        │
│    │                                                         │
│    └─ Convention 설정                                        │
│         └─ 기본값, 소스 디렉토리 등                           │
│                                                              │
│  build.gradle에서 사용:                                      │
│  ┌─────────────────────────────────────────────────────┐    │
│  │ plugins {                                           │    │
│  │     id 'com.example.plugins.app-builder' version '1.0' │    │
│  │ }                                                   │    │
│  │                                                     │    │
│  │ appBuilder {        ← Extension 사용               │    │
│  │     ecrRegistry = 'xxx'                            │    │
│  │ }                                                   │    │
│  │                                                     │    │
│  │ $ gradle GetTargets  ← Task 실행                   │    │
│  └─────────────────────────────────────────────────────┘    │
│                                                              │
└─────────────────────────────────────────────────────────────┘

4. 플러그인 개발 기초

드디어 플러그인을 만들어볼 차례!

지금까지 배운 걸 정리하면:

  1. 플러그인 = 빌드 기능을 추가하는 확장팩
  2. Project = 작업 대상
  3. Task = 실행할 작업
  4. Extension = 설정 옵션

이제 이것들을 조합해서 플러그인을 만들어볼 거예요.

플러그인을 만드는 건 크게 3단계예요:

  1. Extension 클래스 만들기 (설정 옵션 정의)
  2. Task 클래스 만들기 (실제 할 일 정의)
  3. Plugin 클래스에서 1, 2를 등록하기

4.1 프로젝트 구조

플러그인 프로젝트는 이렇게 생겼어요

일반 Kotlin/Java 프로젝트랑 거의 같아요. 다만 gradlePlugin { } 설정으로 “이건 플러그인이야”라고 알려줘야 해요.

my-gradle-plugin/
├── build.gradle.kts          # 플러그인 빌드 설정
├── settings.gradle.kts       # 프로젝트 설정
├── src/
│   ├── main/
│   │   └── kotlin/           # 또는 java/, groovy/
│   │       └── com/example/
│   │           ├── MyPlugin.kt           # 플러그인 메인 클래스
│   │           ├── MyExtension.kt        # Extension (DSL)
│   │           └── tasks/
│   │               └── MyTask.kt         # Task 클래스
│   └── test/
│       └── kotlin/
│           └── com/example/
│               └── MyPluginTest.kt
└── gradle/
    └── wrapper/

4.2 build.gradle.kts (플러그인 프로젝트)

플러그인 프로젝트의 build.gradle.kts 해석

아래 코드가 좀 길어 보이지만, 핵심은 간단해요:

  • kotlin-dsl: “Kotlin으로 플러그인 만들 거야”
  • gradlePlugin { }: “플러그인 ID는 이거고, 메인 클래스는 이거야”
plugins {
    `kotlin-dsl`        // Kotlin으로 플러그인 개발
    `maven-publish`     // 배포용
}

group = "com.example"
version = "1.0.0"

repositories {
    mavenCentral()
    gradlePluginPortal()
}

dependencies {
    // Gradle API (자동 포함)
    // implementation(gradleApi())

    // 테스트
    testImplementation(gradleTestKit())
    testImplementation("org.junit.jupiter:junit-jupiter:5.9.0")
}

// 플러그인 정의
gradlePlugin {
    plugins {
        create("myPlugin") {
            id = "com.example.my-plugin"                    // 플러그인 ID
            implementationClass = "com.example.MyPlugin"    // 메인 클래스
            displayName = "My Custom Plugin"
            description = "내 커스텀 플러그인 설명"
        }
    }
}

// 테스트 설정
tasks.test {
    useJUnitPlatform()
}

4.3 기본 플러그인 클래스

플러그인의 핵심! Plugin 클래스

모든 플러그인은 Plugin<Project> 인터페이스를 구현해요. apply() 메서드 하나만 구현하면 끝!

apply()사용자가 플러그인을 적용할 때 실행돼요. 여기서 Extension 등록하고, Task 등록하고… 초기 설정을 다 해요.

사용자가 plugins { id 'com.example.my-plugin' } 쓰면
     ↓
MyPlugin.apply(project) 가 자동으로 호출됨!
// src/main/kotlin/com/example/MyPlugin.kt
package com.example

import org.gradle.api.Plugin
import org.gradle.api.Project

class MyPlugin : Plugin<Project> {

    override fun apply(project: Project) {
        // 1. 플러그인이 적용될 때 실행되는 코드
        println("MyPlugin이 ${project.name}에 적용되었습니다!")

        // 2. Extension 등록 → 사용자가 myConfig { } 블록을 쓸 수 있게 됨
        project.extensions.create("myConfig", MyExtension::class.java)

        // 3. Task 등록 → 사용자가 gradle myTask 를 실행할 수 있게 됨
        project.tasks.register("myTask", MyTask::class.java) {
            it.group = "custom"
            it.description = "내 커스텀 태스크"
        }
    }
}

4.4 사용자 프로젝트에서 적용

// 사용자의 build.gradle
plugins {
    id 'com.example.my-plugin' version '1.0.0'
}

// Extension 사용
myConfig {
    message = 'Hello from config!'
}

// Task 실행
// $ gradle myTask

5. Extension (DSL 설정)

Extension = 플러그인의 “설정 화면”

앱에 설정 화면이 있듯이, 플러그인에도 설정이 필요해요. Extension이 바로 그 설정을 담당해요.

사용자가 build.gradle에서:

myConfig {
    message = 'Hello'
    count = 10
}

이렇게 쓸 수 있게 해주는 게 Extension이에요!

5.1 Extension이란?

Extension = 플러그인 사용자가 설정할 수 있는 DSL 블록

// 이런 DSL을 제공하고 싶다면:
appBuilder {
    ecrRegistry = 'xxx.dkr.ecr.region.amazonaws.com'
    workspace = '/path/to/project'

    github {
        username = 'user'
        password = 'token'
    }
}

5.2 단순 Extension

가장 기본적인 Extension 만들기

Extension 클래스는 설정값을 담는 그릇이에요.

Property<String>이 뭐냐고요? 그냥 String 쓰면 안 되나요? 안 돼요!

Gradle은 지연 평가(Lazy Evaluation)를 써요. “지금 당장 값이 뭔지 몰라도 돼, 나중에 필요할 때 가져올게”

왜냐하면 build.gradle 파일이 위에서 아래로 순서대로 실행되는데, Extension 등록할 때는 아직 사용자가 값을 안 넣었을 수도 있거든요.

Property는 “나중에 값이 들어올 박스”라고 생각하면 돼요.

// Extension 클래스
package com.example

import org.gradle.api.provider.Property

abstract class MyExtension {
    // Property: Gradle의 지연 평가 타입 (나중에 값이 채워질 박스)
    abstract val message: Property<String>
    abstract val count: Property<Int>

    init {
        // 기본값 설정
        message.convention("Default message")
        count.convention(10)
    }
}
// 플러그인에서 등록
class MyPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        // Extension 등록
        val extension = project.extensions.create(
            "myConfig",
            MyExtension::class.java
        )

        // Task에서 Extension 값 사용
        project.tasks.register("printConfig") {
            it.doLast {
                println("Message: ${extension.message.get()}")
                println("Count: ${extension.count.get()}")
            }
        }
    }
}
// 사용자의 build.gradle
myConfig {
    message = 'Hello!'
    count = 42
}

// $ gradle printConfig
// Message: Hello!
// Count: 42

5.3 중첩 Extension

설정 안에 설정이 있다면?

설정이 복잡해지면 그룹으로 묶고 싶을 때가 있어요.

appBuilder {
    ecrRegistry = '...'

    github {           // ← 이렇게 그룹으로 묶고 싶다!
        username = '...'
        password = '...'
    }
}

이런 걸 중첩 Extension이라고 해요. Extension 안에 또 다른 Extension을 넣는 거죠.

// 중첩 설정을 위한 클래스들
abstract class AppBuilderExtension(objects: ObjectFactory) {
    abstract val ecrRegistry: Property<String>
    abstract val workspace: Property<String>

    // 중첩 Extension
    val github: GithubConfig = objects.newInstance(GithubConfig::class.java)
    val nexus: NexusConfig = objects.newInstance(NexusConfig::class.java)

    companion object {
        const val NAME = "appBuilder"
    }
}

abstract class GithubConfig {
    abstract val username: Property<String>
    abstract val password: Property<String>
}

abstract class NexusConfig {
    abstract val url: Property<String>
    abstract val username: Property<String>
    abstract val password: Property<String>
}
// 플러그인에서 등록
class MyPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        project.extensions.create(
            AppBuilderExtension.NAME,
            AppBuilderExtension::class.java,
            project.objects  // ObjectFactory 전달
        )
    }
}
// 사용자의 build.gradle
appBuilder {
    ecrRegistry = 'xxx.dkr.ecr.region.amazonaws.com'
    workspace = '/path/to/project'

    github {
        username = 'myuser'
        password = 'mytoken'
    }

    nexus {
        url = 'https://nexus.company.com'
        username = 'nexus-user'
        password = 'nexus-pass'
    }
}

5.4 환경변수와 Extension 통합

abstract class AppBuilderExtension(objects: ObjectFactory) {
    abstract val ecrRegistry: Property<String>

    init {
        // 환경변수를 기본값으로 사용
        ecrRegistry.convention(
            System.getenv("ECR_REGISTRY") ?: ""
        )
    }
}
// 사용자는 환경변수 또는 직접 설정 가능
appBuilder {
    // 설정하면 환경변수보다 우선
    ecrRegistry = 'custom-registry.com'

    // 설정 안 하면 환경변수 ECR_REGISTRY 사용
}

6. Task 개발

Task = 플러그인이 “실제로 하는 일”

Extension이 “설정”이라면, Task는 “행동”이에요.

예를 들어:

  • GetTargets Task → 변경된 모듈 찾기
  • AppBuilder Task → Docker 이미지 빌드하기

Task 클래스를 만들 때 핵심은:

  1. @TaskAction 붙은 메서드 = 실제 실행될 코드
  2. @Input 붙은 필드 = 입력값 (이게 바뀌면 Task 다시 실행)
  3. @Output 붙은 필드 = 출력물 (캐싱에 사용)

6.1 Task 기본 구조

Task 클래스의 뼈대

Task는 DefaultTask를 상속받아서 만들어요. @TaskAction이 붙은 메서드가 gradle myTask 할 때 실행되는 코드예요.

package com.example.tasks

import org.gradle.api.DefaultTask
import org.gradle.api.tasks.TaskAction
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.OutputFile
import org.gradle.api.provider.Property
import org.gradle.api.file.RegularFileProperty
import java.io.File

abstract class MyTask : DefaultTask() {

    // 입력 값 (변경되면 Task 재실행)
    @get:Input
    abstract val message: Property<String>

    // 출력 파일
    @get:OutputFile
    abstract val outputFile: RegularFileProperty

    init {
        // 기본값
        group = "custom"
        description = "내 커스텀 태스크"
    }

    @TaskAction
    fun execute() {
        // Task 실행 로직
        val msg = message.get()
        val file = outputFile.get().asFile

        println("메시지: $msg")
        file.writeText(msg)
        println("파일 생성: ${file.absolutePath}")
    }

    companion object {
        const val TASK_NAME = "myTask"
    }
}

6.2 Task를 플러그인에 등록

class MyPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        // Extension 등록
        val extension = project.extensions.create(
            "myConfig",
            MyExtension::class.java
        )

        // Task 등록 및 Extension과 연결
        project.tasks.register(MyTask.TASK_NAME, MyTask::class.java) { task ->
            // Extension 값을 Task에 전달
            task.message.set(extension.message)

            // 출력 파일 경로 설정
            task.outputFile.set(
                project.layout.buildDirectory.file("output/result.txt")
            )
        }
    }
}

6.3 Task 입출력 어노테이션

왜 @Input, @Output을 붙일까요?

Gradle의 강력한 기능 중 하나가 증분 빌드예요. “이전에 빌드한 거랑 뭐가 달라졌지?” 확인해서, 달라진 것만 다시 빌드해요.

이걸 위해 Gradle이 알아야 하는 게:

  • @Input: “이 값이 바뀌면 Task 다시 실행해야 해”
  • @Output: “이 파일이 결과물이야, 캐싱해둬”

예를 들어 @Input version = "1.0.0"인데 값이 안 바뀌었으면? → Task 스킵! (시간 절약)

$ gradle build
> Task :compileJava UP-TO-DATE  ← 입력이 안 바뀌어서 스킵됨!
abstract class BuildTask : DefaultTask() {

    // ─────────────────────────────────────────────
    // 입력 (Input) - 값이 바뀌면 Task 재실행
    // ─────────────────────────────────────────────

    @get:Input                          // 단순 값
    abstract val version: Property<String>

    @get:Input
    @get:Optional                       // 선택적 입력
    abstract val optionalConfig: Property<String>

    @get:InputFile                      // 입력 파일
    abstract val configFile: RegularFileProperty

    @get:InputDirectory                 // 입력 디렉토리
    abstract val sourceDir: DirectoryProperty

    @get:InputFiles                     // 여러 파일
    abstract val resources: ConfigurableFileCollection

    // ─────────────────────────────────────────────
    // 출력 (Output) - 증분 빌드에 사용
    // ─────────────────────────────────────────────

    @get:OutputFile                     // 출력 파일
    abstract val outputFile: RegularFileProperty

    @get:OutputDirectory                // 출력 디렉토리
    abstract val outputDir: DirectoryProperty

    // ─────────────────────────────────────────────
    // 내부 (Internal) - 캐싱에 영향 없음
    // ─────────────────────────────────────────────

    @get:Internal
    abstract val tempData: Property<String>

    @TaskAction
    fun execute() {
        // 구현
    }
}

6.4 Task 의존성

Task끼리 순서가 있어요

“테스트하려면 먼저 컴파일해야 해” 이런 순서 관계를 의존성이라고 해요.

  • dependsOn(taskA): “taskA가 먼저 실행되어야 해”
  • mustRunAfter(taskB): “taskB 다음에 실행해줘 (근데 taskB는 필수는 아냐)”

마치 요리할 때:

  • 볶음밥 만들려면 → 먼저 밥이 있어야 함 (dependsOn)
  • 밥 있으면 → 볶음밥 만든 다음에 설거지 (mustRunAfter)
class MyPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        // Task A 등록
        val taskA = project.tasks.register("taskA") {
            it.doLast { println("Task A 실행") }
        }

        // Task B 등록 (A에 의존)
        val taskB = project.tasks.register("taskB") {
            it.dependsOn(taskA)
            it.doLast { println("Task B 실행") }
        }

        // Task C 등록 (B 다음에 실행)
        project.tasks.register("taskC") {
            it.mustRunAfter(taskB)
            it.doLast { println("Task C 실행") }
        }

        // 기존 Task에 의존성 추가
        project.tasks.named("build").configure {
            it.dependsOn(taskB)
        }
    }
}

// $ gradle taskB
// 출력:
// Task A 실행
// Task B 실행

// $ gradle taskC taskB
// 출력:
// Task A 실행
// Task B 실행
// Task C 실행 (B 다음에)

6.5 외부 명령 실행

abstract class ShellTask : DefaultTask() {

    @get:Input
    abstract val command: Property<String>

    @TaskAction
    fun execute() {
        val cmd = command.get()

        // 방법 1: project.exec
        project.exec {
            it.commandLine("sh", "-c", cmd)
        }

        // 방법 2: ProcessBuilder (더 세밀한 제어)
        val processBuilder = ProcessBuilder("sh", "-c", cmd)
            .directory(project.projectDir)
            .redirectErrorStream(true)

        val process = processBuilder.start()
        val output = process.inputStream.bufferedReader().readText()
        val exitCode = process.waitFor()

        if (exitCode != 0) {
            throw RuntimeException("명령 실패: $cmd\n$output")
        }

        println(output)
    }
}

7. 플러그인 배포

만든 플러그인을 어떻게 공유할까요?

플러그인을 만들었으면 다른 프로젝트에서 쓸 수 있게 배포해야 해요.

배포 방법 3가지:

  1. 로컬 배포 (내 컴퓨터에서만) - 테스트용
  2. 사설 저장소 (회사 Nexus) - 회사 내부용
  3. Gradle Plugin Portal (전 세계 공개) - 오픈소스용

7.1 로컬 테스트 (publishToMavenLocal)

일단 내 컴퓨터에서 테스트해보기

gradle publishToMavenLocal 하면 내 컴퓨터의 ~/.m2/repository에 플러그인이 설치돼요. 다른 프로젝트에서 mavenLocal()을 추가하면 이 플러그인을 가져다 쓸 수 있어요.

// build.gradle.kts
plugins {
    `kotlin-dsl`
    `maven-publish`
}

// $ gradle publishToMavenLocal
// ~/.m2/repository에 배포됨
// 테스트 프로젝트의 settings.gradle
pluginManagement {
    repositories {
        mavenLocal()  // 로컬 저장소 우선
        gradlePluginPortal()
    }
}

7.2 사설 저장소 배포 (Nexus)

// build.gradle.kts
publishing {
    repositories {
        maven {
            name = "nexus"
            url = uri("https://nexus.company.com/repository/maven-hosted/")
            credentials {
                username = System.getenv("NEXUS_USERNAME")
                    ?: providers.gradleProperty("NEXUS_USERNAME").orNull
                password = System.getenv("NEXUS_PASSWORD")
                    ?: providers.gradleProperty("NEXUS_PASSWORD").orNull
            }
        }
    }
}

// $ gradle publish

7.3 Gradle Plugin Portal 배포

// build.gradle.kts
plugins {
    `kotlin-dsl`
    `com.gradle.plugin-publish` version "1.2.0"
}

gradlePlugin {
    website = "https://github.com/yourorg/your-plugin"
    vcsUrl = "https://github.com/yourorg/your-plugin.git"

    plugins {
        create("myPlugin") {
            id = "com.example.my-plugin"
            implementationClass = "com.example.MyPlugin"
            displayName = "My Plugin"
            description = "플러그인 설명"
            tags = listOf("ci", "cd", "build")
        }
    }
}

// $ gradle publishPlugins

8. 실전 예제: app-builder-plugin

이 섹션은 간략한 소개입니다.

app-builder-plugin의 상세한 분석Part 4: App-Builder Plugin 실전 분석에서 다룹니다.

8.1 프로젝트 구조: Gradle 플러그인 + Jenkins Shared Library

app-builder-plugin은 두 가지가 함께 있어요!

단순히 Gradle 플러그인만 있는 게 아니라, Jenkins Shared Library도 함께 있어요. 이 둘이 협력해서 CI/CD 파이프라인을 완성합니다.

app-builder-plugin/
│
├── app-builder-gradle-plugin/           # 🔧 Gradle 플러그인
│   ├── build.gradle.kts
│   └── src/main/kotlin/
│       └── com/knet/plugins/gradle/
│           ├── AppBuilderPlugin.kt      # Plugin<Project> 구현
│           ├── extension/
│           │   ├── AppBuilderExtension.kt
│           │   └── config/
│           │       ├── GithubConfig.kt
│           │       ├── NexusConfig.kt
│           │       └── ...
│           └── tasks/
│               ├── targets/
│               │   └── GetTargetsTask.kt    # 변경 감지
│               └── builder/
│                   ├── AppBuilderTask.kt    # 빌드 실행
│                   └── executor/
│                       ├── BuildExecutor.kt
│                       ├── HelmExecutor.kt
│                       └── VersionExecutor.kt
│
└── app-builder-gradle-plugin-pipeline/  # 🚀 Jenkins Shared Library
    └── vars/
        └── build.groovy                 # build() 함수 정의

8.2 두 가지 역할

구분 Gradle 플러그인 Jenkins Shared Library
위치 app-builder-gradle-plugin/ app-builder-gradle-plugin-pipeline/
역할 빌드 로직 (변경 감지, 이미지 빌드) CI/CD 파이프라인 오케스트레이션
사용 위치 프로젝트의 build.gradle Jenkinsfile
호출 방식 gradle GetTargets @Library(...) _ + build()
개발 언어 Kotlin Groovy

핵심 포인트

  • Gradle 플러그인: “어떻게 빌드할 것인가” (실제 빌드 로직)
  • Jenkins Shared Library: “언제, 어떤 순서로 빌드할 것인가” (파이프라인 흐름)

8.3 호출 방식의 차이

Gradle 플러그인과 Jenkins Shared Library는 완전히 다른 시스템이에요!

┌─────────────────────────────────────────────────────────────┐
│              두 가지 완전히 다른 호출 방식!                    │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  Gradle 플러그인                 Jenkins Shared Library      │
│  ────────────────                ──────────────────────      │
│                                                              │
│  build.gradle:                   Jenkinsfile:                │
│  ┌─────────────────────┐        ┌─────────────────────┐     │
│  │ plugins {           │        │ @Library('...') _   │     │
│  │     id 'java'       │        │ build()             │     │
│  │     id 'app-builder'│        │                     │     │
│  │ }                   │        │                     │     │
│  └─────────────────────┘        └─────────────────────┘     │
│           │                              │                   │
│           ▼                              ▼                   │
│  Plugin.apply(project)           vars/build.groovy          │
│  → Task 등록                      → call() 실행              │
│           │                              │                   │
│           ▼                              ▼                   │
│  $ gradle build                  Jenkins가 파이프라인 실행   │
│  $ gradle compileJava                                        │
│  $ gradle GetTargets                                         │
│  $ gradle AppBuilder                                         │
│                                                              │
└─────────────────────────────────────────────────────────────┘

호출 방식 비교

구분 Gradle 플러그인 Jenkins Shared Library
선언 plugins { id 'java' } @Library('...') _
호출 gradle <task명> 함수명() (예: build())
실행 주체 Gradle CLI Jenkins
핵심 메서드 apply(project) call()
결과물 Task 등록 Pipeline 실행

예시: java 플러그인 vs app-builder 플러그인

// build.gradle
plugins {
    id 'java'                    // JavaPlugin.apply() → Task 등록
    id 'app-builder'             // AppBuilderPlugin.apply() → Task 등록
}
# java 플러그인이 등록한 Task 실행
$ gradle compileJava
$ gradle test
$ gradle jar
$ gradle build

# app-builder 플러그인이 등록한 Task 실행
$ gradle GetTargets      # 변경된 모듈 감지
$ gradle AppBuilder      # Docker 빌드, Helm 배포

예시: Jenkins Shared Library

// Jenkinsfile
@Library('app-builder-gradle-plugin-pipeline') _

build()      // vars/build.groovy의 call() 직접 호출
// 내부에서 sh 'gradle GetTargets' 실행
// 내부에서 sh 'gradle AppBuilder' 실행

전체 흐름 정리

┌─────────────────────────────────────────────────────────────┐
│                       실행 순서                              │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  1. plugins { id 'app-builder' }                            │
│         │                                                    │
│         ▼                                                    │
│     AppBuilderPlugin.apply(project)                         │
│         │                                                    │
│         ├─ GetTargets Task 등록                              │
│         └─ AppBuilder Task 등록                              │
│                                                              │
│  2. Jenkinsfile에서 build() 호출                             │
│         │                                                    │
│         ▼                                                    │
│     vars/build.groovy의 call() 실행                         │
│         │                                                    │
│         ├─ sh './gradlew GetTargets'  → GetTargetsTask 실행 │
│         └─ sh './gradlew AppBuilder'  → AppBuilderTask 실행 │
│                                                              │
└─────────────────────────────────────────────────────────────┘

핵심 차이

  • Gradle 플러그인: Task를 등록하고, gradle 명령어로 실행
  • Jenkins Library: 함수를 정의하고, 함수명()으로 직접 호출
  • 둘의 협력: Jenkins에서 build() → 내부에서 gradle GetTargets 명령어 실행!

8.4 전체 CI/CD 흐름

┌─────────────────────────────────────────────────────────────┐
│                     전체 CI/CD 흐름                          │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  1. Jenkinsfile (각 프로젝트에 있음)                          │
│     ┌─────────────────────────────────────────────────┐     │
│     │ @Library('app-builder-gradle-plugin-pipeline') _ │     │
│     │ build()                                          │     │
│     └─────────────────────────────────────────────────┘     │
│              │                                               │
│              ▼                                               │
│  2. Jenkins Shared Library (vars/build.groovy)              │
│     ┌─────────────────────────────────────────────────┐     │
│     │ def call() {                                     │     │
│     │     stage('Detect Changes') {                    │     │
│     │         sh './gradlew GetTargets'  ─────────┐    │     │
│     │     }                                        │    │     │
│     │     stage('Build & Deploy') {                │    │     │
│     │         sh './gradlew AppBuilder'  ─────────┤    │     │
│     │     }                                        │    │     │
│     │ }                                            │    │     │
│     └──────────────────────────────────────────────│────┘     │
│              │                                      │         │
│              ▼                                      ▼         │
│  3. Gradle 플러그인 (app-builder-gradle-plugin)              │
│     ┌─────────────────────────────────────────────────┐     │
│     │ AppBuilderPlugin.kt                             │     │
│     │   ├─ Extension 등록 (appBuilder { })            │     │
│     │   ├─ GetTargetsTask: 변경된 모듈 감지            │     │
│     │   └─ AppBuilderTask: Docker 빌드, Helm 배포     │     │
│     └─────────────────────────────────────────────────┘     │
│                                                              │
└─────────────────────────────────────────────────────────────┘

8.5 사용 예시

1. 프로젝트의 build.gradle (Gradle 플러그인 사용)

plugins {
    id 'java'
    id 'com.knet.plugins.app-builder-gradle-plugin' version '1.0.0'
}

appBuilder {
    ecrRegistry = 'xxx.dkr.ecr.ap-northeast-2.amazonaws.com'
    workspace = '/path/to/project'

    github {
        username = 'user'
        password = 'token'
    }

    nexus {
        url = 'https://nexus.company.com'
    }
}

2. 프로젝트의 Jenkinsfile (Jenkins Shared Library 사용)

@Library('app-builder-gradle-plugin-pipeline') _

build()  // 이 한 줄로 전체 CI/CD 파이프라인 실행!

3. vars/build.groovy (Shared Library 내부)

def call() {
    pipeline {
        agent any

        stages {
            stage('Checkout') {
                steps {
                    checkout scm
                }
            }

            stage('Detect Changes') {
                steps {
                    sh './gradlew GetTargets'
                }
            }

            stage('Build & Deploy') {
                steps {
                    sh './gradlew AppBuilder'
                }
            }
        }
    }
}

8.6 Jenkins Shared Library 호출 규칙

build()로 호출하는 건 어떻게 동작하는 걸까요?

Jenkins Shared Library의 표준 규칙이에요. vars/ 디렉토리의 파일 이름이 함수 이름이 되고, call() 메서드가 실행돼요!

파일 이름 = 함수 이름

vars/
├── build.groovy      →  build() 로 호출
├── deploy.groovy     →  deploy() 로 호출
├── notify.groovy     →  notify() 로 호출
└── myCustom.groovy   →  myCustom() 로 호출

call() 메서드가 기본 실행 메서드

// vars/build.groovy
def call() {           // ← build() 호출 시 이 메서드 실행
    pipeline {
        agent any
        stages {
            // ...
        }
    }
}

파라미터 받기

// vars/build.groovy
def call(Map config = [:]) {    // ← build(branch: 'main') 이렇게 호출
    pipeline {
        agent any
        stages {
            stage('Checkout') {
                steps {
                    git branch: config.branch ?: 'develop'
                }
            }
        }
    }
}
// Jenkinsfile
@Library('my-shared-library') _

build()                              // 기본값 사용
build(branch: 'main')                // branch 파라미터 전달
build(branch: 'main', skipTests: true)

다른 메서드도 정의 가능

// vars/build.groovy
def call() {
    // 기본 빌드
}

def withDocker(String image) {
    // Docker로 빌드
}

def skipTests() {
    // 테스트 스킵 빌드
}
// Jenkinsfile 에서 사용
build()                    // call() 실행
build.withDocker('node')   // withDocker() 실행
build.skipTests()          // skipTests() 실행

요약: vars/파일명.groovy파일명() 으로 호출, call() 메서드가 실행됨!

8.7 배운 개념이 어떻게 적용되는가?

개념 app-builder-plugin 적용
Plugin AppBuilderPlugin.kt - 진입점
Extension AppBuilderExtension.kt - appBuilder { } DSL 제공
Task GetTargetsTask, AppBuilderTask
중첩 Extension github { }, nexus { }, spring { }
Task 의존성 GetTargets → AppBuilder 순서 실행
@Input/@Output 변경 감지, 증분 빌드에 활용

다음 단계: Part 4에서 각 컴포넌트의 상세 구현, Executor 패턴, 변경 감지 알고리즘 등을 학습하세요.


9. 실습: 간단한 플러그인 만들기

직접 만들어보는 게 최고의 공부!

지금까지 배운 내용을 총정리해서, 아주 간단한 “인사 플러그인”을 만들어볼게요.

만들 플러그인:

// 사용자가 이렇게 설정하면
greeting {
    name = 'Gradle'
    greeting = '안녕하세요'
}

// gradle greet 실행하면
// 출력: 안녕하세요, Gradle!

9.1 프로젝트 생성

mkdir my-gradle-plugin
cd my-gradle-plugin
gradle init --type kotlin-gradle-plugin

9.2 Extension 작성

// src/main/kotlin/com/example/GreetingExtension.kt
package com.example

import org.gradle.api.provider.Property

abstract class GreetingExtension {
    abstract val name: Property<String>
    abstract val greeting: Property<String>

    init {
        name.convention("World")
        greeting.convention("Hello")
    }

    companion object {
        const val NAME = "greeting"
    }
}

9.3 Task 작성

// src/main/kotlin/com/example/GreetingTask.kt
package com.example

import org.gradle.api.DefaultTask
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.TaskAction

abstract class GreetingTask : DefaultTask() {

    @get:Input
    abstract val name: Property<String>

    @get:Input
    abstract val greeting: Property<String>

    init {
        group = "greeting"
        description = "인사말을 출력합니다"
    }

    @TaskAction
    fun greet() {
        val message = "${greeting.get()}, ${name.get()}!"
        println(message)
    }

    companion object {
        const val TASK_NAME = "greet"
    }
}

9.4 플러그인 작성

// src/main/kotlin/com/example/GreetingPlugin.kt
package com.example

import org.gradle.api.Plugin
import org.gradle.api.Project

class GreetingPlugin : Plugin<Project> {

    override fun apply(project: Project) {
        // Extension 등록
        val extension = project.extensions.create(
            GreetingExtension.NAME,
            GreetingExtension::class.java
        )

        // Task 등록
        project.tasks.register(GreetingTask.TASK_NAME, GreetingTask::class.java) {
            it.name.set(extension.name)
            it.greeting.set(extension.greeting)
        }
    }
}

9.5 테스트

// 테스트 프로젝트의 build.gradle
plugins {
    id 'com.example.greeting' version '1.0.0'
}

greeting {
    name = 'Gradle'
    greeting = '안녕하세요'
}

// $ gradle greet
// 출력: 안녕하세요, Gradle!

10. 핵심 요약

이것만 기억하세요!

플러그인 = Extension + Task

┌─────────────────────────────────────────────────────────────┐
│                    플러그인 만들기 3줄 요약                    │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  1. Extension 클래스 만들기                                  │
│     → 사용자가 설정할 수 있는 옵션 정의                       │
│     → build.gradle에서 myConfig { } 블록으로 사용            │
│                                                              │
│  2. Task 클래스 만들기                                       │
│     → 실제로 수행할 작업 정의                                 │
│     → @TaskAction 메서드가 실행됨                            │
│                                                              │
│  3. Plugin 클래스에서 등록하기                                │
│     → apply() 메서드에서 Extension, Task 등록                │
│     → plugins { id '...' } 하면 apply()가 호출됨             │
│                                                              │
└─────────────────────────────────────────────────────────────┘

비유로 정리

개념 비유 설명
Gradle 세탁기 빌드 자동화 도구
Plugin 세탁기 기능 새로운 빌드 기능 추가
Extension 버튼/다이얼 설정 옵션
Task 세탁/헹굼/탈수 실행할 작업
Property 값이 들어올 박스 지연 평가를 위한 타입

코드 흐름

사용자가 plugins { id 'my-plugin' } 쓰면
    ↓
Plugin.apply(project) 호출됨
    ↓
Extension 등록 (myConfig { } 사용 가능)
Task 등록 (gradle myTask 실행 가능)
    ↓
사용자가 myConfig { message = 'Hello' } 로 설정
    ↓
gradle myTask 실행하면 Task의 @TaskAction 메서드 실행!

11. 참고 자료

공식 문서

API 문서

예제