CI/CD 학습 시리즈

실제 Gradle 플러그인 프로젝트(app-builder-plugin)를 분석하며 Kotlin 문법을 학습한 내용을 정리합니다.

프로젝트 구조

app-builder-gradle-plugin/
└── src/main/kotlin/com/knet/plugins/gradle/
    ├── AppBuilderPlugin.kt           # 플러그인 진입점
    ├── entity/                       # 데이터 모델
    ├── extension/                    # 플러그인 설정
    ├── logger/                       # 로깅 유틸리티
    ├── tasks/
    │   ├── builder/                  # 빌드 태스크
    │   │   └── executor/             # AppBuilder, Version, Build, Helm Executor
    │   └── targets/                  # 모듈 감지 태스크
    │       └── executor/             # DetectorExecutor
    └── utils/                        # 공통 유틸리티

Level 1: 기본 문법

1.1 enum class - 열거형 클래스

Kotlin:

enum class LogLevel(
    val pattern: String,      // 각 enum 값이 가질 속성
) {
    FATAL(pattern = ".* (FATAL|CRITICAL) .*"),
    ERROR(pattern = ".* (ERROR|SEVERE) .*"),
    WARN(pattern = ".* WARN(ING)? .*"),
    DEBUG(pattern = ".* (DEBUG|FINE|TRACE) .*"),
    INFO(pattern = ".* INFO .*");

    // enum 안에 메서드 정의
    fun matches(line: String): Boolean {
        return pattern.toRegex().containsMatchIn(line)
    }

    companion object {
        fun findMatchingLevel(line: String): LogLevel? {
            return entries.find { it.matches(line) }
        }
    }
}

Java 동등 코드:

public enum LogLevel {
	FATAL(".* (FATAL|CRITICAL) .*"),
	ERROR(".* (ERROR|SEVERE) .*"),
	WARN(".* WARN(ING)? .*"),
	DEBUG(".* (DEBUG|FINE|TRACE) .*"),
	INFO(".* INFO .*");

	private final String pattern;  // 필드 직접 선언 필요

	LogLevel(String pattern) {     // 생성자 직접 작성 필요
		this.pattern = pattern;
	}

	public String getPattern() {   // getter 직접 작성 필요
		return pattern;
	}

	public boolean matches(String line) {
		return Pattern.compile(pattern).matcher(line).find();
	}

	public static LogLevel findMatchingLevel(String line) {
		return Arrays.stream(values())
			.filter(level -> level.matches(line))
			.findFirst()
			.orElse(null);
	}
}
Kotlin Java 차이점
enum class LogLevel(val pattern: String) 필드 + 생성자 + getter 각각 작성 Kotlin은 한 줄로 끝
companion object { } static 메서드 Kotlin은 블록으로 그룹화
entries values() Kotlin 1.9+에서 추가
entries.find { } Arrays.stream().filter().findFirst() Kotlin이 더 간결

1.2 object - 싱글톤 객체

Kotlin:

object AnsiColor {
    const val RESET = "\u001B[0m"
    const val TIME_GRAY = "\u001B[90m"
    const val BG_BRIGHT_RED = "\u001B[101m"
}

// 사용
println("${AnsiColor.TIME_GRAY}시간${AnsiColor.RESET}")

Java 동등 코드:

public final class AnsiColor {
	public static final String RESET = "\u001B[0m";
	public static final String TIME_GRAY = "\u001B[90m";
	public static final String BG_BRIGHT_RED = "\u001B[101m";

	private AnsiColor() {
	}  // 인스턴스화 방지
}

// 사용
System.out.

println(AnsiColor.TIME_GRAY +"시간"+AnsiColor.RESET);
Kotlin Java 차이점
object AnsiColor final class + private 생성자 Kotlin은 키워드 하나로 싱글톤
const val public static final Kotlin이 더 간결
"${변수}" + 변수 + (문자열 연결) Kotlin 문자열 템플릿이 편리

1.3 data class - 데이터 클래스

Kotlin:

data class ModuleInfo(
    var index: Int,        // var = 변경 가능
    val name: String,      // val = 변경 불가 (읽기전용)
    val language: String,
    val type: String,
    var changed: Boolean,
    var version: String?,  // ? = nullable (null 허용)
    var status: String,
)

// 사용
val module = ModuleInfo(
    index = 0, name = "edoc-api", language = "JAVA",
    type = "app", changed = true, version = "1.0.0", status = "PENDING"
)

println(module)                    // toString() 자동
module.copy(version = "1.0.1")     // 일부만 바꾼 복사본
module == otherModule              // equals() 자동

Java 동등 코드 (전통적인 방식):

public class ModuleInfo {
	private int index;
	private final String name;
	private final String language;
	private final String type;
	private boolean changed;
	private String version;  // nullable
	private String status;

	// 생성자
	public ModuleInfo(int index, String name, String language,
					  String type, boolean changed, String version, String status) {
		this.index = index;
		this.name = name;
		this.language = language;
		this.type = type;
		this.changed = changed;
		this.version = version;
		this.status = status;
	}

	// Getter (7개)
	public int getIndex() {
		return index;
	}

	public String getName() {
		return name;
	}

	public String getLanguage() {
		return language;
	}

	public String getType() {
		return type;
	}

	public boolean isChanged() {
		return changed;
	}

	public String getVersion() {
		return version;
	}

	public String getStatus() {
		return status;
	}

	// Setter (var 필드만)
	public void setIndex(int index) {
		this.index = index;
	}

	public void setChanged(boolean changed) {
		this.changed = changed;
	}

	public void setVersion(String version) {
		this.version = version;
	}

	public void setStatus(String status) {
		this.status = status;
	}

	// equals() - 직접 구현 필요
	@Override
	public boolean equals(Object o) {
		if (this == o) return true;
		if (o == null || getClass() != o.getClass()) return false;
		ModuleInfo that = (ModuleInfo) o;
		return index == that.index && changed == that.changed &&
			Objects.equals(name, that.name) &&
			Objects.equals(language, that.language) &&
			Objects.equals(type, that.type) &&
			Objects.equals(version, that.version) &&
			Objects.equals(status, that.status);
	}

	// hashCode() - 직접 구현 필요
	@Override
	public int hashCode() {
		return Objects.hash(index, name, language, type, changed, version, status);
	}

	// toString() - 직접 구현 필요
	@Override
	public String toString() {
		return "ModuleInfo{index=" + index + ", name='" + name + "', ...}";
	}

	// copy() - Java에는 없음, 직접 구현해야 함
	public ModuleInfo copy(Integer index, String name, /* ... */) {
		return new ModuleInfo(
			index != null ? index : this.index,
			name != null ? name : this.name,
			// ... 모든 필드
		);
	}
}

Java 16+ record (간단한 경우만):

// record는 모든 필드가 final (val)이어야 함
// var 필드나 nullable 표현이 제한적
public record ModuleInfo(
		int index,
		String name,
		String language,
		String type,
		boolean changed,
		String version,
		String status
	) {
}
Kotlin Java 차이점
data class 7줄 전통: 70줄+ / record: 10줄 Kotlin이 훨씬 간결
val/var 혼용 record는 모두 final Kotlin이 유연함
String? @Nullable String 또는 Optional<String> Kotlin null 안전성 내장
copy(version = "1.0.1") 직접 구현 필요 Kotlin 자동 생성
Named argument Java에 없음 가독성 향상

1.4 val vs var

Kotlin:

val name = "edoc-api"    // 재할당 불가 (타입 추론: String)
name = "other"           // 컴파일 에러!

var version = "1.0.0"    // 재할당 가능
version = "1.0.1"        // OK

Java 동등 코드:

final String name = "edoc-api";  // final = 재할당 불가
name ="other";                   // 컴파일 에러!

String version = "1.0.0";         // final 없음 = 재할당 가능
version ="1.0.1";                // OK
Kotlin Java 차이점
val final Kotlin은 기본이 불변 권장
var (일반 변수) Java는 기본이 가변
타입 추론 val name = "text" var name = "text" (Java 10+) 둘 다 타입 추론 가능

1.5 Nullable (?) - Kotlin의 핵심 기능

Kotlin:

var version: String? = null    // null 허용
var name: String = "edoc"      // null 불가 (컴파일 에러!)
name = null                    // 컴파일 에러!

// null 안전 접근
version?.length                // null이면 null 반환
version ?: "default"           // null이면 기본값
version!!                      // null 아님을 단언 (위험)

Java 동등 코드:

String version = null;         // Java는 모든 참조 타입이 nullable
String name = "edoc";
name =null;                   // 가능! (NPE 위험)

// null 체크 - 직접 해야 함
	if(version !=null){
int len = version.length();
}

// Optional 사용 (Java 8+)
Optional<String> optVersion = Optional.ofNullable(version);
int len = optVersion.map(String::length).orElse(0);

// 기본값
String result = version != null ? version : "default";
// 또는
String result = Objects.requireNonNullElse(version, "default");
Kotlin Java 차이점
String? @Nullable String (어노테이션) Kotlin은 타입 시스템에 내장
String (non-null) 없음 (모든 참조가 nullable) 컴파일 시점에 NPE 방지
?. (safe call) if (x != null) x.method() Kotlin이 간결
?: (Elvis) 삼항 연산자 x != null ? x : default Kotlin이 간결
!! Objects.requireNonNull(x) 명시적 단언

Kotlin Null Safety의 장점:

Java:   런타임에 NPE 발생 → 서비스 장애
Kotlin: 컴파일 시점에 null 체크 강제 → 안전한 코드

Level 2: 함수와 주요 문법

2.1 함수 선언

Kotlin:

// 기본 형태
fun getCurrentBranch(): String {
    return "main"
}

// 반환 타입 생략 (Unit = void)
fun configureGitUser() {
    // ...
}

// 표현식 함수 (한 줄)
private fun normalizeKey(name: String): String = name.substringAfterLast(":")

// 기본값 파라미터
fun connect(host: String, port: Int = 8080): Connection {
    ...
}

// Named argument
connect(host = "localhost", port = 3000)
connect(host = "localhost")  // port는 기본값 8080

Java 동등 코드:

// 기본 형태
public String getCurrentBranch() {
	return "main";
}

// void 반환
public void configureGitUser() {
	// ...
}

// 한 줄 함수도 블록 필요
private String normalizeKey(String name) {
	return name.substring(name.lastIndexOf(":") + 1);
}

// 기본값 파라미터 - Java에 없음! 오버로딩으로 구현
public Connection connect(String host, int port) { ...}

public Connection connect(String host) {
	return connect(host, 8080);  // 오버로딩
}

// Named argument - Java에 없음!
connect("localhost",3000);
Kotlin Java 차이점
fun 반환타입 앞에 키워드가 다름
Unit void 반환값 없음
= expression { return expression; } 표현식 함수로 간결
기본값 파라미터 오버로딩으로 구현 Kotlin이 편리
Named argument 없음 가독성 향상

2.2 when 표현식 (switch 대체)

Kotlin:

// 값 매칭 (표현식으로 값 반환)
return when (logLevel) {
    FATAL, ERROR -> "${BG_BRIGHT_RED}$line${RESET}"
    WARN -> "${BG_BRIGHT_YELLOW}$line${RESET}"
    else -> "${BG_BRIGHT_WHITE}$line${RESET}"
}

// 조건 매칭 (if-else 대체)
when {
    appDockerfile.exists() -> return MODULE_TYPE_APP
    libDockerfile.exists() -> return MODULE_TYPE_LIB
}

// 타입 매칭
when (obj) {
    is String -> println("문자열: ${obj.length}")  // 자동 캐스팅!
    is Int -> println("정수: ${obj + 1}")
    else -> println("알 수 없음")
}

Java 동등 코드:

// switch 표현식 (Java 14+)
return switch(logLevel){
	case FATAL,ERROR ->BG_BRIGHT_RED +line +RESET;
    case WARN ->BG_BRIGHT_YELLOW +line +RESET;
default ->BG_BRIGHT_WHITE +line +RESET;
};

// 조건 매칭 - Java에 없음! if-else 체인 필요
	if(appDockerfile.

exists()){
	return MODULE_TYPE_APP;
}else if(libDockerfile.

exists()){
	return MODULE_TYPE_LIB;
}

// 타입 매칭 - Java 17+ pattern matching
	if(obj instanceof
String s){
	System.out.

println("문자열: "+s.length());
	}else if(obj instanceof
Integer i){
	System.out.

println("정수: "+(i +1));
	}else{
	System.out.

println("알 수 없음");
}
Kotlin Java 차이점
when (value) switch (value) (Java 14+) 비슷
when { 조건 } if-else 체인 Kotlin만의 기능
is Type -> instanceof (Java 17+) Kotlin이 더 간결
자동 스마트 캐스팅 패턴 매칭 변수 필요 Kotlin이 편리

2.3 문자열 템플릿

Kotlin:

val name = "edoc-api"
println("모듈: $name")                    // "모듈: edoc-api"
println("길이: ${name.length}")           // "길이: 8"
println("대문자: ${name.uppercase()}")    // "대문자: EDOC-API"

// 여러 줄 문자열
val json = """
    {
        "name": "$name",
        "version": "1.0.0"
    }
""".trimIndent()

Java 동등 코드:

String name = "edoc-api";
System.out.

println("모듈: "+name);                         // 문자열 연결
System.out.

println("길이: "+name.length());
	System.out.

println("대문자: "+name.toUpperCase());

// String.format 사용
	System.out.

println(String.format("모듈: %s, 길이: %d", name, name.length()));

// 여러 줄 문자열 (Java 15+)
String json = """
	{
	    "name": "%s",
	    "version": "1.0.0"
	}
	""".formatted(name);
Kotlin Java 차이점
"$변수" + 변수 + 또는 %s Kotlin이 직관적
"${표현식}" + 표현식 + 중괄호로 표현식 삽입
"""...""" """...""" (Java 15+) 비슷
.trimIndent() 수동 처리 필요 들여쓰기 자동 제거

2.4 클래스 정의와 생성자

Kotlin:

class GitUtils(
    private val project: Project,    // 주 생성자 파라미터 = 클래스 속성
    private val logger: Logger,
    private val workspace: File
) {
    init {
        // 객체 생성 시 자동 실행
        initializeGitEnvironment()
    }

    // 보조 생성자
    constructor(project: Project) : this(project, defaultLogger, defaultWorkspace)
}

// 상속
open class Animal(val name: String)                    // open = 상속 가능
class Dog(name: String) : Animal(name)                 // : = extends

// 인터페이스 구현
class MyPlugin : Plugin<Project> {
    override fun apply(project: Project) {}
}

Java 동등 코드:

public class GitUtils {
	private final Project project;
	private final Logger logger;
	private final File workspace;

	// 주 생성자
	public GitUtils(Project project, Logger logger, File workspace) {
		this.project = project;
		this.logger = logger;
		this.workspace = workspace;
		initializeGitEnvironment();  // init 블록 역할
	}

	// 보조 생성자 (오버로딩)
	public GitUtils(Project project) {
		this(project, defaultLogger, defaultWorkspace);
	}
}

// 상속
public class Animal {                                   // 기본이 상속 가능
	private final String name;

	public Animal(String name) {
		this.name = name;
	}
}

public class Dog extends Animal {                       // extends
	public Dog(String name) {
		super(name);
	}
}

// 인터페이스 구현
public class MyPlugin implements Plugin<Project> {      // implements
	@Override
	public void apply(Project project) {
	}
}
Kotlin Java 차이점
class Foo(val x: Int) 필드 + 생성자 + getter 별도 Kotlin이 간결
init { } 생성자 본문 명시적 초기화 블록
: (상속/구현) extends/implements 콜론으로 통일
open class (기본이 상속 가능) Kotlin은 기본이 final
override fun @Override 키워드로 명시

2.5 Null Safety 연산자 정리

Kotlin:

val fromEnv = System.getenv("BRANCH_NAME") ?: System.getenv("GIT_BRANCH")
if (!fromEnv.isNullOrBlank()) return sanitizeBranch(fromEnv)

// 다양한 null 처리 패턴
val length = name?.length ?: 0                    // null이면 0
val upper = name?.uppercase()                     // null이면 null
val nonNull = name ?: throw IllegalArgumentException()  // null이면 예외
val forced = name!!                               // null이면 NPE (위험!)

Java 동등 코드:

String fromEnv = System.getenv("BRANCH_NAME");
if(fromEnv ==null)fromEnv =System.

getenv("GIT_BRANCH");
if(fromEnv !=null&&!fromEnv.

isBlank())return

sanitizeBranch(fromEnv);

// Java의 null 처리
int length = name != null ? name.length() : 0;
String upper = name != null ? name.toUpperCase() : null;
if(name ==null)throw new

IllegalArgumentException();

String forced = Objects.requireNonNull(name);
연산자 이름 Java 동등 코드
?: Elvis x != null ? x : default
?. Safe call if (x != null) x.method()
!! Not-null 단언 Objects.requireNonNull(x)
?.takeIf { } 조건부 반환 Optional.filter()

2.6 컬렉션 함수 (람다)

Kotlin:

val dockerfileModules = modules.filter { module ->
    hasDockerfile(module, workspace)
}

output.trim()
    .split(" ")
    .filter { it.isNotBlank() }   // it = 각 요소 (람다 기본 파라미터)
    .map { it.uppercase() }
    .forEach { println(it) }

// 체이닝 예제
val result = modules
    .filter { it.changed }              // 변경된 것만
    .map { it.name }                    // 이름만 추출
    .sorted()                           // 정렬
    .joinToString(", ")                 // 문자열로 합침

Java 동등 코드 (Stream API):

List<String> dockerfileModules = modules.stream()
	.filter(module -> hasDockerfile(module, workspace))
	.collect(Collectors.toList());

Arrays.

stream(output.trim().

split(" "))
	.

filter(s ->!s.

isBlank())
	.

map(String::toUpperCase)
    .

forEach(System.out::println);

// 체이닝 예제
String result = modules.stream()
	.filter(m -> m.isChanged())         // getter 호출
	.map(Module::getName)               // 메서드 레퍼런스
	.sorted()
	.collect(Collectors.joining(", "));
Kotlin Java Stream 차이점
filter { } .filter(x -> ...) Kotlin이 간결
map { } .map(x -> ...) 동일
forEach { } .forEach(x -> ...) 동일
find { } .filter().findFirst() Kotlin이 직관적
any { } .anyMatch(x -> ...) 이름만 다름
firstOrNull() .findFirst().orElse(null) Kotlin이 간결
바로 사용 가능 .stream() 필요 Kotlin 컬렉션이 편리
결과가 List .collect() 필요 Kotlin이 간결

2.7 vararg (가변 인자)

Kotlin:

fun execGitCommand(vararg args: String): String {
    val processBuilder = ProcessBuilder("git", *args)
    //                                         ^ spread 연산자
}

// 호출
execGitCommand("fetch", "origin", "--tags")
execGitCommand("status")

// 배열을 vararg로 전달
val commands = arrayOf("fetch", "origin")
execGitCommand(*commands)  // spread 연산자로 펼침

Java 동등 코드:

public String execGitCommand(String... args) {
	// Java의 varargs는 내부적으로 배열
	List<String> command = new ArrayList<>();
	command.add("git");
	command.addAll(Arrays.asList(args));
	ProcessBuilder processBuilder = new ProcessBuilder(command);
}

// 호출
execGitCommand("fetch","origin","--tags");

execGitCommand("status");

// 배열을 vararg로 전달 - 그냥 전달하면 됨
String[] commands = {"fetch", "origin"};

execGitCommand(commands);
Kotlin Java 차이점
vararg args: String String... args 문법만 다름
*array (spread) 그냥 전달 Kotlin은 명시적 펼침 필요

2.8 스코프 함수 - Kotlin만의 강력한 기능

Kotlin:

// also - 부가 작업 후 원래 객체 반환
private val mapper = jacksonObjectMapper()
    .findAndRegisterModules()
    .also { println("Mapper 초기화: $it") }  // 로깅 등 부가 작업

// let - null 체크 후 변환
val length = name?.let { it.length } ?: 0

// apply - 객체 설정
val person = Person().apply {
    this.name = "Kim"
    this.age = 30
}

// run - 객체 내에서 계산
val result = person.run {
    "$name is $age years old"
}

// with - 객체를 인자로 받아 블록 실행
with(person) {
    println(name)
    println(age)
}

Java 동등 코드 (스코프 함수 없음!):

// also - Java에 없음, 별도 변수 필요
ObjectMapper mapper = new ObjectMapper().findAndRegisterModules();
System.out.

println("Mapper 초기화: "+mapper);
// mapper를 그대로 사용

// let - Optional 또는 if문
int length = Optional.ofNullable(name)
	.map(String::length)
	.orElse(0);
// 또는
int length = name != null ? name.length() : 0;

// apply - Java에 없음, setter 따로 호출
Person person = new Person();
person.

setName("Kim");
person.

setAge(30);

// run - Java에 없음
String result = person.getName() + " is " + person.getAge() + " years old";

// with - Java에 없음
System.out.

println(person.getName());
	System.out.

println(person.getAge());
스코프 함수 반환값 참조 Java 대체
also { } 원래 객체 it 없음 (변수 저장 후 사용)
let { } 람다 결과 it Optional.map()
apply { } 원래 객체 this 없음 (setter 체이닝)
run { } 람다 결과 this 없음
with(obj) { } 람다 결과 this 없음

스코프 함수 선택 가이드:

원래 객체 반환?
├── Yes → 객체 참조가 it? → also (로깅, 검증)
│         객체 참조가 this? → apply (초기화, 설정)
└── No  → 객체 참조가 it? → let (null 체크, 변환)
          객체 참조가 this? → run (계산)

2.9 Pair, Triple과 구조 분해

Kotlin:

// Pair 반환 - 두 값을 묶어서 반환
private fun normalizeBuildInfo(buildInfo: BuildInfo): Pair<BuildInfo, Boolean> {
    return Pair(buildInfo, false)
    // 또는: return buildInfo to false
}

// Triple 반환 - 세 값을 묶어서 반환
fun getEnvironment(): Triple<String, String, String> {
    return Triple("dev", "test", "prod")
}

// 구조 분해 (destructuring) - 한 번에 여러 변수에 할당
val (normalized, changed) = normalizeBuildInfo(raw)
val (dev, test, prod) = getEnvironment()

// data class도 구조 분해 가능
data class Person(val name: String, val age: Int)
val (name, age) = Person("Kim", 30)

Java 동등 코드:

// Pair - Java에 없음! 직접 만들거나 라이브러리 사용
// Apache Commons: Pair<BuildInfo, Boolean>
// 또는 Map.Entry, 또는 커스텀 클래스

// 보통 커스텀 record 사용 (Java 16+)
record NormalizeResult(BuildInfo buildInfo, boolean changed) {
}

private NormalizeResult normalizeBuildInfo(BuildInfo buildInfo) {
	return new NormalizeResult(buildInfo, false);
}

// 구조 분해 - Java에 없음! 각각 꺼내야 함
NormalizeResult result = normalizeBuildInfo(raw);
BuildInfo normalized = result.buildInfo();
boolean changed = result.changed();
Kotlin Java 차이점
Pair<A, B> 없음 (record 필요) Kotlin 내장
Triple<A, B, C> 없음 Kotlin 내장
val (a, b) = pair pair.getFirst() 구조 분해 지원
a to b new Pair<>(a, b) 간결한 생성 문법

2.10 문자열 확장 함수들

Kotlin:

raw.removePrefix("refs/heads/")
    .removePrefix("origin/")
    .trim()

remoteUrl.substringAfterLast("/")
    .removeSuffix(".git")

// 더 많은 확장 함수
"  hello  ".trim()                    // "hello"
"hello".uppercase()                   // "HELLO"
"HELLO".lowercase()                   // "hello"
"hello".capitalize()                  // "Hello" (deprecated, use replaceFirstChar)
"hello world".split(" ")              // ["hello", "world"]
"hello".repeat(3)                     // "hellohellohello"
"hello".reversed()                    // "olleh"
"hello".take(3)                       // "hel"
"hello".drop(2)                       // "llo"
"abc".padStart(5, '0')                // "00abc"
"abc".padEnd(5, '0')                  // "abc00"

Java 동등 코드:

// removePrefix - Java에 없음! 직접 구현
String result = raw;
if(result.

startsWith("refs/heads/")){
result =result.

substring("refs/heads/".length());
	}
	if(result.

startsWith("origin/")){
result =result.

substring("origin/".length());
	}
result =result.

trim();

// substringAfterLast - Java에 없음!
int lastSlash = remoteUrl.lastIndexOf("/");
String afterSlash = lastSlash >= 0 ? remoteUrl.substring(lastSlash + 1) : remoteUrl;
if(afterSlash.

endsWith(".git")){
afterSlash =afterSlash.

substring(0,afterSlash.length() -4);
	}

// Java 문자열 메서드
	"  hello  ".

trim();                   // "hello"
"hello".

toUpperCase();                // "HELLO"
"HELLO".

toLowerCase();                // "hello"
"hello world".

split(" ");             // String[] 반환
"hello".

repeat(3);                    // "hellohellohello" (Java 11+)
new

StringBuilder("hello").

reverse().

toString();  // "olleh"
"hello".

substring(0,3);              // "hel"
"hello".

substring(2);                 // "llo"
String.

format("%5s","abc").

replace(' ','0');  // "00abc" (복잡!)
Kotlin Java 차이점
removePrefix() 직접 구현 Kotlin 내장
removeSuffix() 직접 구현 Kotlin 내장
substringAfterLast() lastIndexOf() + substring() Kotlin 내장
substringBeforeLast() 직접 구현 Kotlin 내장
take(n) substring(0, n) Kotlin이 직관적
drop(n) substring(n) Kotlin이 직관적

Level 3: Gradle 플러그인 구조

3.1 Plugin 클래스 - 진입점

class AppBuilderPlugin : Plugin<Project> {

    override fun apply(project: Project) {
        // 1. Extension 등록 (DSL 설정 블록)
        project.extensions.create(
            AppBuilderExtension.NAME,           // "appBuilder"
            AppBuilderExtension::class.java,
            project.objects
        )

        // 2. Task 등록
        registerTasks(project)
    }

    private fun registerTasks(project: Project) {
        project.tasks.register(GetTargetsTask.TASK_NAME, GetTargetsTask::class.java)
        project.tasks.register(AppBuilderTask.TASK_NAME, AppBuilderTask::class.java)
    }
}
Gradle API 설명
Plugin<Project> Gradle 플러그인 인터페이스
apply(project) 플러그인이 적용될 때 실행
project.extensions.create() DSL 설정 블록 등록
project.tasks.register() 태스크 등록 (lazy)

3.2 Extension 클래스 - DSL 설정

abstract class AppBuilderExtension @Inject constructor(objects: ObjectFactory) {
    companion object {
        val NAME = "appBuilder"
    }

    var ecrRegistry: Property<String> = objects.property(String::class.java)
    var dryRun: Property<Boolean> = objects.property(Boolean::class.java).convention(false)

    val nexus: NexusConfig = objects.newInstance(NexusConfig::class.java)

    fun nexus(action: Action<NexusConfig>) = action.execute(nexus)
}

사용자가 build.gradle.kts에서 사용하는 방식:

appBuilder {
    ecrRegistry = "111111111.dkr.ecr.ap-northeast-2.amazonaws.com"
    dryRun = true

    nexus {
        username = "admin"
        password = "secret"
    }
}

3.3 Property - Gradle의 지연 평가

var ecrRegistry: Property<String> = objects.property(String::class.java)

// 값 읽기
val registry = appBuilderExtension.ecrRegistry.get()
val registry = appBuilderExtension.ecrRegistry.getOrElse("default")
val registry = appBuilderExtension.ecrRegistry.orNull

3.4 Task 클래스

open class GetTargetsTask : DefaultTask() {

    companion object {
        val TASK_NAME = "GetTargets"
    }

    @Internal
    val extension = this.project.extensions.getByName(...) as GetTargetsTaskExtension

    @TaskAction
    fun execute() {
        val (targetModule, workspace) = processParameters()
        val executor = DetectorExecutor(project, logger)
        executor.execute(workspace, ...)
    }
}
어노테이션 설명
@TaskAction 태스크 실행 메서드 지정
@Internal Gradle 캐시에서 제외
@Input 입력 값 (변경 시 재실행)
@OutputFile 출력 파일 (캐시 키)

3.5 클래스 참조 문법

AppBuilderExtension::class                    // KClass<AppBuilderExtension>
AppBuilderExtension::class.java               // Class<AppBuilderExtension>
AppBuilderExtension::class.simpleName         // "AppBuilderExtension"

3.6 as 연산자 (타입 캐스팅)

Kotlin:

// 안전하지 않은 캐스팅 (실패 시 ClassCastException)
val extension = project.extensions.getByName("appBuilder") as AppBuilderExtension

// 안전한 캐스팅 (실패 시 null)
val extension = project.extensions.getByName("appBuilder") as? AppBuilderExtension

// 스마트 캐스팅 (is 체크 후 자동 캐스팅)
if (obj is String) {
    println(obj.length)  // 자동으로 String으로 캐스팅됨!
}

Java 동등 코드:

// 캐스팅 (실패 시 ClassCastException)
AppBuilderExtension extension = (AppBuilderExtension) project.getExtensions().getByName("appBuilder");

// 안전한 캐스팅 - instanceof 체크 필요
Object obj = project.getExtensions().getByName("appBuilder");
AppBuilderExtension extension = obj instanceof AppBuilderExtension
	? (AppBuilderExtension) obj
	: null;

// 타입 체크 후 캐스팅 (Java 17+ 패턴 매칭)
if(obj instanceof
String s){
	System.out.

println(s.length());
	}
// Java 16 이하
	if(obj instanceof String){
String s = (String) obj;  // 수동 캐스팅 필요
    System.out.

println(s.length());
	}
Kotlin Java 차이점
as (Type) 같음
as? instanceof + 캐스팅 Kotlin이 간결
스마트 캐스팅 패턴 매칭 (Java 17+) Kotlin이 더 오래됨

3.7 의존성 주입 (@Inject)

abstract class AppBuilderExtension @Inject constructor(objects: ObjectFactory) {
    // Gradle이 자동으로 ObjectFactory 주입
}

Level 4: 실전 비즈니스 로직 (Executor)

4.1 Executor 아키텍처

GetTargetsTask                    AppBuilderTask
     │                                  │
     ▼                                  ▼
DetectorExecutor              AppBuilderExecutor (오케스트레이터)
                                        │
                              ┌─────────┼─────────┐
                              ▼         ▼         ▼
                         Version    Build     Helm
                         Executor   Executor  Executor

4.2 ?.let { } 패턴 - null 안전 처리

private fun getModuleInfo(buildInfo: BuildInfo, targetModule: String): ModuleInfo {
    val normalized = normalizeKey(targetModule)

    // null이 아니면 블록 실행
    buildInfo.modules[normalized]?.let {
        return it
    }

    buildInfo.modules[targetModule]?.let {
        return it
    }

    throw IllegalArgumentException("Module '$targetModule' not found")
}

4.3 copy() - data class 복사

val updatedInfo = moduleInfo.copy(
    version = versionInfo.version,   // 이 값만 변경
    status = "SUCCESS"               // 이 값만 변경
)
// 나머지 속성은 원본 유지

4.4 Triple - 세 값 묶기

private fun determineEnvironmentByBranch(branchName: String): Triple<String, String, String> {
    val nodeEnv = if (branchName == "main") "prod" else "dev"
    val goProEnv = if (branchName == "main") "prod" else "dev"
    val springEnv = "cloudconfig,test"

    return Triple(nodeEnv, goProEnv, springEnv)
}

// 구조 분해로 받기
val (nodeEnv, goEnv, springEnv) = determineEnvironmentByBranch(currentBranch)

4.5 apply { } - 객체 설정 블록

val dumperOptions = DumperOptions().apply {
    defaultFlowStyle = DumperOptions.FlowStyle.BLOCK
    isPrettyFlow = true
    indent = 2
}

4.6 use { } - 자동 리소스 해제

Kotlin:

process.inputStream.bufferedReader().use { reader ->
    reader.lineSequence().forEach { line ->
        println(formatLogLine(line))
    }
}
// use 블록 끝나면 자동으로 reader.close()

// 파일 읽기
File("data.txt").bufferedReader().use { reader ->
    val content = reader.readText()
}

// 여러 리소스
FileInputStream("input.txt").use { input ->
    FileOutputStream("output.txt").use { output ->
        input.copyTo(output)
    }
}

Java 동등 코드 (try-with-resources):

// Java 7+ try-with-resources
try(BufferedReader reader = new BufferedReader(
	new InputStreamReader(process.getInputStream()))){
	reader.

lines().

forEach(line ->{
	System.out.

println(formatLogLine(line));
	});
	}
// try 블록 끝나면 자동으로 reader.close()

// 파일 읽기
	try(
BufferedReader reader = new BufferedReader(new FileReader("data.txt"))){
String content = reader.lines().collect(Collectors.joining("\n"));
}

// 여러 리소스
	try(
FileInputStream input = new FileInputStream("input.txt");
FileOutputStream output = new FileOutputStream("output.txt")){
	input.

transferTo(output);
}
Kotlin Java 차이점
.use { } try () { } Kotlin은 확장 함수, Java는 문법
람다 내부에서 사용 try 블록 내부에서 사용 Kotlin이 함수형
중첩 use 세미콜론으로 구분 Java가 더 간결

4.7 toMutableMap() - 불변 → 가변 변환

val currentBuildInfo = jsonUtil.readBuildInfo()
val updatedModules = currentBuildInfo.modules.toMutableMap()

updatedModules[key] = updatedInfo  // 이제 수정 가능

4.8 forEachIndexed - 인덱스와 함께 순회

dockerfileModules.forEachIndexed { index, module ->
    val moduleInfo = ModuleInfo(
        index = index,
        name = module,
        // ...
    )
}

4.9 ?: throw - null이면 예외

val version = moduleInfo.version
    ?: throw IllegalArgumentException("Module version is missing")

4.10 try-finally 패턴

try {
    cloneHelmRepo(workDir)
    updateModuleValues(...)
    commitAndPushChanges(...)
} finally {
    // 성공/실패 관계없이 항상 실행
    if (workDir.exists()) {
        workDir.deleteRecursively()
    }
}

4.11 중첩 data class

class BuildExecutor(...) {

    // 클래스 내부에 정의된 data class
    private data class ModuleBuildContext(
        val module: String,
        val moduleType: String,
        val tagVersion: String,
        // ...
    )
}

전체 요약

Level 1: 기본 문법
├── enum class    열거형 + 속성 + 메서드
├── object        싱글톤
├── data class    자동 equals/hashCode/toString/copy
└── val/var, ?    불변/가변, nullable

Level 2: 함수와 컬렉션
├── fun           함수 선언, 표현식 함수
├── when          switch 대체 (값/조건 매칭)
├── Null Safety   ?. ?: !! takeIf
├── 컬렉션 함수    filter, map, forEach, find, any
└── 스코프 함수    also, let, apply, run

Level 3: Gradle 플러그인
├── Plugin<Project>  플러그인 인터페이스
├── Extension        DSL 설정 블록 (abstract + @Inject)
├── Task             @TaskAction으로 실행 메서드 지정
└── Property<T>      Gradle 지연 평가 속성

Level 4: 비즈니스 로직
├── ?.let { }      null 안전 처리 패턴
├── .copy()        data class 부분 복사
├── .use { }       자동 리소스 해제
├── Pair/Triple    여러 값 반환
└── toMutableMap() 불변→가변 변환

Kotlin vs Java 핵심 비교표

코드량 비교

기능 Kotlin Java 차이
data class (7필드) 7줄 70줄+ 10배 ↓
싱글톤 3줄 10줄+ 3배 ↓
null 체크 1줄 (?.) 3줄+ (if문) 3배 ↓
컬렉션 필터링 1줄 3줄 (.stream().collect()) 3배 ↓

Kotlin만의 기능 (Java에 없음)

기능 설명 Java 대체
스코프 함수 also, let, apply, run 없음
Elvis 연산자 ?: 삼항 연산자
Safe call ?. if문
스마트 캐스팅 is 체크 후 자동 캐스팅 패턴 매칭 (Java 17+)
구조 분해 val (a, b) = pair 없음
확장 함수 String.removePrefix() 유틸 클래스
기본값 파라미터 fun foo(x: Int = 0) 오버로딩
Named argument foo(name = "a") 없음
when 조건 매칭 when { 조건 -> } if-else 체인

Java가 더 나은 경우

상황 이유
기존 Java 프로젝트 호환성, 팀 익숙함
Android 아닌 서버 Java 생태계가 더 큼
레거시 코드 유지보수 Java 개발자 많음

결론: 언제 Kotlin을 선택할까?

✅ Kotlin 추천
├── Gradle 플러그인 개발 (Kotlin DSL 지원)
├── Android 앱 개발 (공식 언어)
├── 새 프로젝트 시작
├── 간결한 코드 선호
└── Null Safety 중요

✅ Java 유지
├── 기존 Java 프로젝트
├── 팀이 Java에 익숙
└── Spring 생태계 (둘 다 OK)