0. Intro
Whether your source code is written in Java, Kotlin, or both, there are several places you must choose a JDK or Java language version for your build.
- Android Developers Document
우리는 앱 빌드를 위해 JDK 또는 Java 언어 버전을 여러 곳에서 선택해야한다. `compileOptions` 와 `kotlinOptions` 을 이용하여 Java 버전과 `JVM` 버전을 설정해주는 과정을 먼저 살펴보고, Kotlin과 Java 소스코드가 Gradle 빌드 도구로 JDK 실행환경에서 작동하는 과정을 정리한다. 또 `Gradle JDK`는 어떻게 정의될 수 있는지 그 관계까지 알아본다.
1. Java sourceCompatibility, targetCompatibility
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
package org.gradle.kotlin.dsl
public fun org.gradle.api.Project.java(configure: org.gradle.api.Action<org.gradle.api.plugins.JavaPluginExtension>): kotlin.Unit { /* compiled code */ }
java 블럭은 Kotlin DSL에서 제공하는 확장함수이며 Java 관련 설정을 쉽게 적용할 수 있도록 도와주는 API이다.
Convention Plugin을 작성할 때 build.gradle.kts 에서 작성할 수 있는데, Convention Plugin 은 Kotlin과 Java 기반으로 작성된 코드이므로 사용 가능한 Java 버전을 명시적으로 설정해야 한다.
- `sourceCompatibility`: 코드를 작성할 때 사용할 Java 버전을 지정한다. 즉, 위의 예시에서는 Java 17 문법을 사용할 수 있도록 한다.
- `targetCompatibility`: 컴파일된 바이트코드(.class)가 실행될 JVM의 최소 버전을 지정한다. Java 17 이상의 JVM에서 실행 가능하도록 바이트코드를 생성한다. 즉, 컴파일된 클래스 파일(.class)이 Java 17 이상에서 실행 가능하게 된다.
Java 17 문법을 사용할 수 있고, Java 17 에서 실행 가능한 바이트코드를 만들겠다는 의미
왜 둘을 동일하게 설정할까?
sourceCompatibility보다 targetCompatibility를 더 낮게 설정하면, 새로운 문법을 사용했는데 구버전 JVM에서 실행하려고 할 때 문제가 발생할 가능성이 있다. 예를 들어 Java 11 문법으로 작성된 코드가 Java 8 용 바이트코드로 컴파일되는데, Java 8에서 실행할 수 없는 Java 11 기능을 사용하면 런타임에서 ClassNotFoundException 등의 오류가 발생할 수 있다.
targetCompatibility를 높게 설정하면, 굳이 최신 JVM에서만 실행되도록 제한하는 것이라 의미가 없다.
targetCompatibility은 sourceCompatibility 보다 크거나 같아야 합니다. 실제로는 sourceCompatibility, targetCompatibility, jvmTarget가 일반적으로 동일한 값을 사용해야 합니다.
- Android Developers Document
실제 sourceCompatibility 를 17로, targetCompatibility을 16로 지정했을 시 아래와 같은 컴파일 에러가 뜬다.
2. compileOptions : Android 프로젝트에서 사용
Android 프로젝트에서는 `java { }` 대신 `compileOptions` 를 사용하는 것이 일반적이다.
java { }과 compileOptions 는 Java 코드를 위한 설정
android {
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
}
3. kotlinOptions
kotlinOptions.jvmTarget 은 Kotlin 코드를 위한 설정
`java { }` 설정은 Java 컴파일러 (`javac`)가 Java 코드(`.java 파일`)을 컴파일할 때 사용할 버전을 지정하는 것이다. 즉, Java 코드에만 영향을 미친다. Kotlin 코드(`.kt 파일`)에는 적용되지 않는다. 코틀린 파일은 kotlinOptions에서 JVM 타겟을 지정한다.
tasks.withType<KotlinCompile>().configureEach {
kotlinOptions {
jvmTarget = "17"
}
}
즉, kotlinOptions.jvmTarget = "17"을 설정해야 .kt 파일이 Java 17용 바이트코드로 컴파일된다. java {} 블록이 없어도 Kotlin 파일을 컴파일하는 데에는 전혀 영향이 없다.
현재는 compileOptions, kotlinOptions 모두 Java 8을 기본으로 두고있다.
The targetCompatibility and jvmTarget properties determine the Java class-format version used when generating bytecode for compiled Java and Kotlin source, respectively.
- Android Developers Document
Kotlin DSL 에서는 아래와 같이 설정할 수 있다. 이렇게 하면 Kotlin과 Java 코드 모두 Java 17 을 타겟으로 설정되며, kotlinOptions.jvmTarget 을 수동으로 설정할 필요가 없다.
kotlin {
jvmToolchain(17)
}
코틀린 프로젝트에서 kotlinOptions 설정을 안해준다면?
sealed interface MyInterface //sealed interface는 JDK 17부터 도입
이 코드를 jvmTarget 없이 컴파일할 때,
- Kotlin 컴파일러(kotlinc)는 기본적으로 JVM 1.8 바이트코드로 컴파일
Kotlin은 JVM 기반 언어이기 때문에, Kotlin에서 사용한 sealed interface와 같은 기능은 실제로 JVM에서 지원하는 기능을 기준으로 컴파일된다. 즉 Android Studio에서 사용하는 JDK가 17 이상이라면, Kotlin 컴파일러가 JDK 17의 기능을 활용할 수 있고 컴파일 자체는 된다. compileOptions 과 마찬가지로, Java 8에서 실행할 수 없는 Java 17 기능을 사용하면 런타임에서 오류가 발생할 수 있기 때문에 명시적으로 kotlinOptions을 추가해야 동일한 JVM 버전을 타겟으로 하도록 보장할 수 있다.
Kotlin 프로젝트에서 java {} 블록이 필요할까?
Kotlin 프로젝트에서 java {} 블록이 꼭 필요한 경우는 두 가지이다.
1. Kotlin + Java 혼합 프로젝트인 경우
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
만약 프로젝트에 Java 코드(.java)도 함께 포함되어 있다면, Java 코드도 Kotlin 코드와 같은 JVM 버전으로 컴파일해야 한다. 이걸 설정하지 않으면, Java 의 기본 타겟 버전이 낮아서 충돌이 발생할 수도 있다. 특히, Kotlin 코드에서 Java 17 기능을 사용하고 있는데, Java 코드가 Java 8로 컴파일되면 문제 생긴다.
2. Kotlin 프로젝트지만 일부 Gradle 플러그인이 Java 설정을 필요로 하는 경우
어떤 Gradle 플러그인이나 라이브러리는 java {} 설정을 참고하여 동작하는 경우가 있다. Convention Plugin 이나, Kotlin-Spring 과 같은 Plugin은 Java 버전 설정을 요구할 수 있다. 이런 경우에만 Kotlin 프로젝트에서도 java { } 블록을 추가하면 된다.
4. JDK와 compileOptions, kotlinOptions 관계
JDK(Java Development Kit) 는 Java 프로그램을 만들고 실행하는 데 필요한 모든 도구가 포함된 개발 키트
JDK가 영향을 주는 요소를 간단히 살펴보면, 아래와 같다.
- Java 및 Kotlin 코드의 컴파일과 실행 환경
- Gradle 및 AGP의 동작
- Java 및 Kotlin의 기본 동작 지원
자바 프로그램을 만들고 실행하는 도구들이라고 하면 compileOptions, kotlinOptions 없이 JDK만으로도 돌아가지 않을까? 라고 생각할 수도 있지만 JDK만으로는 부족한 부분이 있어 별도로 설정해야한다.
즉, JDK는 실행 환경을 제공할 뿐 프로젝트에서 어떤 버전의 바이트코드를 생성할지는 직접 결정하지 않는다. 각각의 언어 Java, Kotlin이 JVM 바이트코드를 어떤 버전으로 생성할지 명확하게 지정해줘야 하는 것이다.
JDK, CompileOptions, KotlinOptions 버전을 모두 맞춰야하는 이유
- 만약 JDK 17을 사용하면서 compileOptions와 kotlinOptions를 JDK 8에 맞게 설정하면, JDK 17의 최신 기능(e.g sealed class)을 사용할 수 없게 되며, 빌드 오류가 발생할 수 있다.
- 반대로, JDK 8을 사용하면서 compileOptions와 kotlinOptions를 JDK 17에 맞게 설정하면, JDK 8이 지원하지 않는 기능을 사용할 수 없게 되어, 빌드 오류나 실행 오류가 발생할 수 있다.
5. Gradle과 JDK의 관계
source 코드들이 JVM 바이트코드를 어떤 버전으로 생성할지, 그리고 그 실행환경은 JDK에서 이루어지는 과정까지 보았다. 이 JDK를 사용하여 Gradle이란 빌드 도구로 실제 컴파일 및 바이트 코드 등을 생성하는데 Gradle과 JDK 간의 관계에 대해서 알아보자.
Gradle은 빌드 도구로, JDK를 사용하여 소스 코드 컴파일, 바이트코드 생성, 애플리케이션 실행 등의 작업을 수행한다.
Gradle과 JDK(Java Development Kit)는 빌드 및 실행 환경에서 밀접하게 연관되어 있다.
- Gradle은 Java 기반의 프로그램으로 JVM에서 실행된다.
- 따라서, Gradle을 실행하려면 최소한 JRE(Java Runtime Enviroment)가 필요하고, 일부 기능(컴파일)을 사용하려면 JDK가 필요하다.
- Java 또는 Kotlin 프로젝트에서 Gradle은 JDK를 사용하여 .java 또는 .kt 파일을 바이트코드(.class)로 변환한다.
- 컴파일 과정에서 javac 또는 kotlinc를 호출하는데, 이 과정에서도 JDK가 필요하다.
- Gradle 빌드 스크립트 실행인 build.gradle(Groovy DSL) 또는 build.gradle.kts(Kotlin DSL) 스크립트도 JVM에서 실행되므로, JDK가 필요하다. 빌드 스크립트에서 Java 또는 Kotlin 관련 설정을 적용할 때 JDK가 중요한 역할을 한다.
Android Studio 에서 실행되는 Gradle JDK의 결정방식
- defined by (for Studio builds):
- Android Studio 에서 실행할 경우, Android Studio가 사용하는 JDK를 따른다.
- Android Studio는 기본적으로 내장된 `JBR`(JetBrains Runtime JDK)를 사용한다.
- Android Studio에서 Gradle이 사용하는 JDK 버전을 확인하고 싶다면, Android Studio의 Settings 에서 확인할 수 있다. 기본적으로 Android Studio의 JDK 경로(JetBrains Runtime JDK)가 셋팅된 것을 볼 수 있다.
- `JBR`
- JetBrains가 Android Studio, IntelliJ IDEA 등의 개발 도구를 실행하기 위해 커스텀한 JDK
- 일반적인 OpenJDK 기반이지만, UI 및 성능 최적화가 되어 있다.
터미널에서 실행되는 Gradle JDK의 결정방식
- defined by (for shell builds):
- 가장 우선순위가 높은 것은 `gradle.properties` 파일에 설정된 `org.gradle.java.home` 값이다.
- 만약 `org.gradle.java.home` 값이 설정되지 않았다면, 기본적으로 `JAVA_HOME` 환경 변수를 사용한다.
- gradle -version 의 JVM 부분에서 Gradle이 사용하는 JDK 버전을 확인할 수 있다.
왜 두 개의 실행 환경이 나누어져 있을까?
예를 들어, Android Studio는 JetBrains Runtime JDK를 사용하지만, 터미널에서는 OpenJDK를 사용할 수도 있다.
Gradle을 실행하는 환경을 나눠 더 유연하고 안정적으로 관리할 수 있다.
- 터미널이나 CI/CD 환경에서 독립적으로 Gradle을 실행할 때 사용
- 개발자는 Android Studio에서 편하게 개발하고, 빌드는 CI/CD 환경에서 실행될 수도 있다.
- 터미널에서 실행되는 Gradle은 CI/CD 자동화에 적합하도록 따로 설정할 수 있도록 설계될 수 있다.
- 플랫폼 독립성을 보장하기 위해
- Gradle은 Android 프로젝트뿐만 아니라, 일반적인 Java/Kotlin 프로젝트에서도 사용된다.
- Android Studio 없이 Gradle을 실행하는 경우도 많기 때문에, 터미널에서 실행되는 Gradle 환경이 별도로 필요하다.
6. Java 버전 변경 시 고려할 점
- Android Gradle Plugin 에 맞는 Java 버전 확인
- 프로젝트가 Java 8, 11, 17 등 어떤 버전에서 동작하는지 확인해야 한다. 예를 들어, Android 프로젝트에서는 최신 버전의 AGP(Android Gradle Plugin)가 지원하는 Java 버전도 고려해야 한다.
- `AGP`, `Gradle`, `JDK` 는 서로 버전 호환성을 신경써야한다. 현재 나는 AGP 를 8.4 를 사용하고 있는데 Gradle Version은 8.6, JDK Version은 17 (JBR) 로 기본적으로 셋팅되어있다.
- 팀 내 개발 환경 확인
- 팀원들의 JDK 버전이 일치하는지 확인해야한다. 예를 들어, 팀원이 Java 11을 사용하고 있는데 프로젝트가 Java 17로 설정되어 있으면, 컴파일할 때 문제가 발생할 수 있다.
- 각 팀원이 사용하는 JDK 버전에 따라 Java/Kotlin 컴파일러가 서로 다른 바이트코드를 생성할 수 있기 때문
- 같은 코드라도, 누군가는 정상 빌드되고 누군가는 빌드 오류가 날 수 있어 프로젝트가 일관되게 동작하지 않을 위험이 있다
- 때문에 JDK 버전과 compileOptions, kotlinOptions 까지 모두 일치시키는 것이 좋다.
- 라이브러리 호환성 체크
- 사용 중인 라이브러리가 지원하는 Java 버전도 확인해야 한다. 예를 들어, 일부 구버전 라이브러리는 Java 17에서 동작하지 않을 수 있다.
- 사용 중인 라이브러리가 지원하는 Java 버전도 확인해야 한다. 예를 들어, 일부 구버전 라이브러리는 Java 17에서 동작하지 않을 수 있다.
Reference
- [Android 빌드의 Java 버전](https://developer.android.com/build/jdks?hl=ko)
'🐸 Android' 카테고리의 다른 글
[Gradle] Android Gradle Plugin과 Gradle 기본 생성 파일 (0) | 2025.02.02 |
---|---|
[Android] Convention Plugin (0) | 2025.01.30 |
FCM Notification 2 - background 에서 푸쉬 알람받기 (0) | 2023.12.12 |
FCM Notification 1 - 안드로이드 13에서 Notification 권한 허가 변경 (0) | 2023.12.12 |
[Android] 렌더링 관점에서 효율적인 ListAdapter (1) | 2023.09.07 |