Kotlin Gradle 플러그인의 바이너리 호환성 검증

Kotlin Gradle 플러그인의 바이너리 호환성 검증 (Binary compatibility validation in the Kotlin Gradle plugin)

바이너리 호환성 검증은 라이브러리 작성자가 사용자가 더 새 버전으로 업그레이드할 때 코드를 망가뜨리지 않도록 도와주는 기능이에요. 매끄러운 업그레이드 경험을 제공하는 것뿐 아니라 사용자와 장기적인 신뢰를 쌓고 라이브러리 채택을 지속시키는 데도 중요하죠.

출처: Binary compatibility validation in the Kotlin Gradle plugin

본문

바이너리 호환성(binary compatibility)이란 한 라이브러리의 두 버전이 컴파일된 바이트코드를 다시 컴파일 없이 서로 바꿔서 실행할 수 있다는 뜻이에요.

Kotlin Gradle 플러그인은 바이너리 호환성 검증을 지원해요. 플러그인은 현재 코드에서 ABI(Application Binary Interface) 덤프를 생성하고, 이를 이전 덤프와 비교해서 차이점을 강조해 줘요. 이 변경 사항을 검토하면 잠재적으로 바이너리와 호환되지 않는 수정을 찾아 조치를 취할 수 있어요.

활성화하는 방법 (How to enable)

바이너리 호환성 검증을 활성화하려면 build.gradle.kts 파일에 abiValidation {} 블록을 추가하세요. 사용자 지정 구성이 없다면 abiValidation() 함수를 사용해도 돼요.

kotlin {
    @OptIn(org.jetbrains.kotlin.gradle.dsl.abi.ExperimentalAbiValidation::class)
    abiValidation()
}
kotlin {
    abiValidation()
}

KGP가 필요한 Gradle 작업을 만들어 줘요. 여러 모듈에서 바이너리 호환성을 검사하려면 각 모듈을 따로 구성해 주세요.

바이너리 호환성 문제 확인하기 (Check for binary compatibility issues)

코드를 변경한 뒤 잠재적으로 바이너리와 호환되지 않는 문제가 있는지 확인하려면 IntelliJ IDEA에서 checkKotlinAbi Gradle 작업을 실행하거나 프로젝트 디렉터리에서 다음 명령을 사용하세요.

./gradlew checkKotlinAbi

이 작업은 ABI 덤프를 비교하고 감지된 차이점을 오류로 출력해요. 바이너리 호환성을 유지하기 위해 코드를 어떻게 바꿔야 할지 신중하게 출력을 확인하세요.

기본적으로 바이너리 호환성 검증이 활성화된 프로젝트에서 check 작업을 실행하면 Gradle은 checkKotlinAbi 작업도 함께 실행해요.

참조 ABI 덤프 갱신하기 (Update reference ABI dump)

Gradle이 최신 변경 사항을 검사할 때 사용하는 참조 ABI 덤프를 갱신하려면 IntelliJ IDEA에서 updateKotlinAbi 작업을 실행하거나 프로젝트 디렉터리에서 다음 명령을 사용하세요.

./gradlew updateKotlinAbi

변경 사항이 이전 버전과 바이너리 호환성을 유지한다고 확신할 때만 참조 덤프를 갱신하세요.

필터 구성하기 (Configure filters)

ABI 덤프에 포함할 클래스, 프로퍼티, 함수를 제어하는 필터를 정의할 수 있어요. filters {} 블록을 사용해서 각각 excluded {}included {} 블록으로 제외 규칙과 포함 규칙을 추가하세요.

Gradle은 어떤 제외 규칙에도 매치되지 않는 선언만 ABI 덤프에 포함해요. 포함 규칙이 정의되어 있으면 선언은 그중 하나와 매치하거나, 구성원 중 하나 이상이 매치해야 해요.

규칙은 다음을 기준으로 삼을 수 있어요.

  • 클래스, 프로퍼티, 함수의 정규화된 이름(byNames).
  • BINARY 또는 RUNTIME retention을 가진 애노테이션의 이름(annotatedWith).

이름 규칙에는 와일드카드 **, *, ?를 사용할 수 있어요.

  • **은 마침표를 포함해 문자가 0개 이상 매치돼요.
  • *은 마침표를 제외한 문자가 0개 이상 매치돼요. 단일 클래스 이름을 지정할 때 사용하세요.
  • ?은 정확히 한 문자와 매치돼요.

예를 들어:

kotlin {
    @OptIn(org.jetbrains.kotlin.gradle.dsl.abi.ExperimentalAbiValidation::class)
    abiValidation {
        filters {
            excluded {
                byNames.add("**.InternalUtils")
                annotatedWith.add("com.example.annotations.InternalApi")
            }

            included {
                byNames.add("com.example.api.**")
                annotatedWith.add("com.example.annotations.PublicApi")
            }
        }
    }
}
kotlin {
    abiValidation {
        filters {
            excluded {
                byNames.add("**.InternalUtils")
                annotatedWith.add("com.example.annotations.InternalApi")
            }

            included {
                byNames.add("com.example.api.**")
                annotatedWith.add("com.example.annotations.PublicApi")
            }
        }
    }
}

이 예시에서는:

  • 제외:
    • InternalUtils 클래스.
    • @InternalApi로 애노테이션된 선언.
  • 포함:
    • com.example.api 패키지의 모든 것.
    • @PublicApi로 애노테이션된 선언.

필터링에 대해 더 알아보려면 Kotlin Gradle 플러그인 API 참조를 확인하세요.

지원되지 않는 타깃의 추론된 변경 방지하기 (Prevent inferred changes for unsupported targets)

멀티플랫폼 프로젝트에서 호스트 시스템이 모든 타깃을 컴파일할 수 없다면 Kotlin Gradle 플러그인은 사용 가능한 타깃에서 ABI 변경을 추론하려고 해요. 이렇게 하면 나중에 더 많은 타깃을 지원하는 호스트로 전환했을 때 잘못된 실패를 피할 수 있어요.

이 동작을 비활성화하려면 build.gradle.kts 파일에 다음을 추가하세요.

kotlin {
    @OptIn(org.jetbrains.kotlin.gradle.dsl.abi.ExperimentalAbiValidation::class)
    abiValidation {
        keepLocallyUnsupportedTargets.set(false)
    }
}
kotlin {
    abiValidation {
        keepLocallyUnsupportedTargets = false
    }
}

타깃이 지원되지 않고 추론이 비활성화되어 있으면 checkKotlinAbi 작업은 완전한 ABI 덤프를 생성할 수 없기 때문에 실패해요. 바이너리와 호환되지 않는 변경을 놓칠 위험보다 작업이 실패하는 편이 낫다고 생각한다면 이 동작이 유용하게 쓰일 수 있어요.

maven-publish 플러그인의 퍼블리케이션 포함하기 (Include publications from the maven-publish plugin)

기본적으로 바이너리 호환성 검증은 Kotlin 컴파일 출력물을 사용해 ABI 덤프를 생성해요. 그래서 생성된 ABI 덤프가 최종적으로 퍼블리시된 아티팩트를 반영하지 못할 수 있어요. 예를 들어 maven-publish 플러그인을 사용할 때 relocation 같은 후처리 단계가 컴파일 이후에 아티팩트를 수정할 수 있죠.

ABI 덤프가 maven-publish 플러그인으로 퍼블리시된 아티팩트를 정확히 반영하게 하려면 build.gradle.kts 파일에 다음을 추가하세요.

kotlin {
    @OptIn(org.jetbrains.kotlin.gradle.dsl.abi.ExperimentalAbiValidation::class)
    abiValidation {
        binariesSource.set(MAVEN_PUBLICATIONS)
    }
}
kotlin {
    abiValidation {
        binariesSource = MAVEN_PUBLICATIONS
    }
}

Kotlin/Android 프로젝트와 Android 타깃이 있는 멀티플랫폼 프로젝트는 JAR 파일을 퍼블리시하지 않기 때문에 이 기능이 적용되지 않아요.