Opt-in 요구사항

Opt-in 요구사항

Kotlin 표준 라이브러리는 특정 API 요소를 사용하려면 명시적인 동의를 요구하고 부여하는 메커니즘을 제공해요. 이 메커니즘으로 라이브러리 작성자는 opt-in이 필요한 특정 조건을 사용자에게 알릴 수 있어요. 예를 들어 API가 실험 상태라서 나중에 바뀔 가능성이 크다거나 하는 경우죠.

출처: Kotlin 공식 문서

본문

사용자를 보호하기 위해 컴파일러는 이런 조건에 대해 경고하고, 해당 API를 사용하려면 먼저 opt-in을 하도록 요구해요.

API에 opt-in하기

라이브러리 작성자가 라이브러리 API의 선언을 opt-in이 필요하도록 표시하면, 여러분은 코드에서 그 API를 사용하기 전에 명시적으로 동의를 해야 해요. opt-in하는 방법은 여러 가지인데, 상황에 가장 잘 맞는 방법을 고르는 걸 권장해요.

지역적으로 opt-in하기

코드에서 특정 API 요소를 사용할 때 opt-in하려면 실험 API 마커를 참조하는 @OptIn 애너테이션을 쓰세요. 예를 들어 opt-in이 필요한 DateProvider 클래스를 사용하려 한다고 해볼게요.

// Library code
@RequiresOptIn(message = "This API is experimental. It could change in the future without notice.")
@Retention(AnnotationRetention.BINARY)
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
annotation class MyDateTime

@MyDateTime
// A class requiring opt-in
class DateProvider

여러분의 코드에서는 DateProvider 클래스를 사용하는 함수를 선언하기 전에, MyDateTime 애너테이션 클래스를 참조하는 @OptIn 애너테이션을 추가하면 돼요.

// Client code
@OptIn(MyDateTime::class)

// Uses DateProvider
fun getDate(): Date {
    val dateProvider: DateProvider
    // ...
}

중요한 점은 이 방식에서는 getDate() 함수가 코드의 다른 곳에서 호출되거나 다른 개발자가 사용하더라도 opt-in이 필요하지 않다는 거예요.

// Client code
@OptIn(MyDateTime::class)

// Uses DateProvider
fun getDate(): Date {
    val dateProvider: DateProvider
    // ...
}

fun displayDate() {
    // OK: No opt-in is required
    println(getDate()) 
}

opt-in 요구사항은 전파되지 않아요. 즉 다른 사람들이 모르는 사이에 실험 API를 사용할 수 있다는 뜻이죠. 그러니 opt-in 요구사항을 전파하는 편이 더 안전해요.

opt-in 요구사항 전파하기

라이브러리처럼 제3자용으로 쓰일 의도의 API를 여러분의 코드에서 사용할 때, 그 opt-in 요구사항을 여러분의 API로도 전파할 수 있어요. 이렇게 하려면 여러분의 선언을 라이브러리가 쓰는 것과 같은 opt-in 요구사항 애너테이션으로 표시하면 됩니다.

예를 들어 DateProvider 클래스를 사용하는 함수를 선언하기 전에 @MyDateTime 애너테이션을 추가해 보세요.

// Client code
@MyDateTime
fun getDate(): Date {
    // OK: the function requires opt-in as well
    val dateProvider: DateProvider
    // ...
}

fun displayDate() {
    println(getDate())
    // Error: getDate() requires opt-in
}

이 예시에서 볼 수 있듯이, 애너테이션이 붙은 함수는 @MyDateTime API의 일부처럼 보여요. opt-in은 getDate() 함수의 사용자에게 opt-in 요구사항을 전파하죠.

API 요소의 시그니처가 opt-in이 필요한 타입을 포함한다면, 그 시그니처 자체도 opt-in이 필요해야 해요. 그렇지 않고 API 요소는 opt-in이 필요 없는데 시그니처가 opt-in이 필요한 타입을 포함한다면, 그것을 사용하면 오류가 발생해요.

// Client code
@MyDateTime
fun getDate(dateProvider: DateProvider = DateProvider()): Date

@MyDateTime
fun displayDate() {
    // OK: the function requires opt-in as well
    println(getDate())
}

마찬가지로, 시그니처에 opt-in이 필요한 타입이 포함된 선언에 @OptIn을 적용해도 opt-in 요구사항은 여전히 전파돼요.

// Client code
@OptIn(MyDateTime::class)
// Propagates opt-in due to DateProvider in the signature
fun getDate(dateProvider: DateProvider = DateProvider()): Date

fun displayDate() {
    println(getDate())
    // Error: getDate() requires opt-in
}

opt-in 요구사항을 전파할 때 기억할 점은, 어떤 API 요소가 안정화되어 더 이상 opt-in 요구사항이 없어졌다 해도, 여전히 opt-in 요구사항을 가진 다른 API 요소들은 실험 상태로 남아 있다는 거예요. 예를 들어 라이브러리 작성자가 getDate() 함수가 이제 안정적이라서 opt-in 요구사항을 제거했다고 해볼게요.

// Library code
// No opt-in requirement
fun getDate(): Date {
    val dateProvider: DateProvider
    // ...
}

opt-in 애너테이션을 제거하지 않고 displayDate() 함수를 사용한다면, opt-in이 더 이상 필요 없는데도 이 함수는 여전히 실험 상태로 남아요.

// Client code

// Still experimental!
@MyDateTime 
fun displayDate() {
    // Uses a stable library function
    println(getDate())
}

여러 API에 opt-in하기

여러 API에 opt-in하려면 선언을 그들의 opt-in 요구사항 애너테이션 모두로 표시하면 돼요. 예를 들어:

@ExperimentalCoroutinesApi
@FlowPreview

또는 @OptIn으로 이렇게 할 수도 있어요.

@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)

파일에 opt-in하기

파일의 모든 함수와 클래스에서 opt-in이 필요한 API를 사용하려면, 파일 최상단에 패키지 지정과 import보다 앞서 파일 레벨 애너테이션 @file:OptIn을 추가하면 됩니다.

// Client code
@file:OptIn(MyDateTime::class)

모듈에 opt-in하기

-opt-in 컴파일러 옵션은 Kotlin 1.6.0부터 사용할 수 있어요. 이전 Kotlin 버전에서는 -Xopt-in을 사용하세요.

opt-in이 필요한 API를 매번 애너테이션으로 표시하고 싶지 않다면, 모듈 전체에 대해 opt-in을 할 수 있어요. 모듈에서 API 사용에 opt-in하려면 -opt-in 인자로 컴파일하면서, 사용하는 API의 opt-in 요구사항 애너테이션의 정규화된 이름을 지정하면 됩니다: -opt-in=org.mylibrary.OptInAnnotation. 이 인자로 컴파일하는 것은 모듈의 모든 선언에 @OptIn(OptInAnnotation::class) 애너테이션이 있는 것과 같은 효과를 가져요.

Gradle로 모듈을 빌드한다면 다음과 같이 인자를 추가할 수 있어요.

import org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask
// ...

tasks.named<KotlinCompilationTask<*>>("compileKotlin").configure {
    compilerOptions.optIn.add("org.mylibrary.OptInAnnotation")
}
import org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask
// ...

tasks.named('compileKotlin', KotlinCompilationTask) {
    compilerOptions {
        optIn.add('org.mylibrary.OptInAnnotation')
    }
}

Gradle 모듈이 멀티플랫폼 모듈이라면 optIn 메서드를 사용하세요.

kotlin {
    compilerOptions {
        optIn.add("org.mylibrary.OptInAnnotation")
    }
}
kotlin {
    compilerOptions {
        optIn.add('org.mylibrary.OptInAnnotation')
    }
}

Maven에서는 다음과 같이 사용해요.

<build>
    <plugins>
        <plugin>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-maven-plugin</artifactId>
            <version>${kotlin.version}</version>
            <executions>...</executions>
            <configuration>
                <args>
                    <arg>-opt-in=org.mylibrary.OptInAnnotation</arg>                    
                </args>
            </configuration>
        </plugin>
    </plugins>
</build>

모듈 레벨에서 여러 API에 opt-in하려면, 모듈에서 사용하는 각 opt-in 요구사항 마커마다 설명한 인자 중 하나를 추가하면 돼요.

클래스나 인터페이스 상속에 opt-in하기

때로 라이브러리 작성자는 API를 제공하면서, 사용자가 그것을 확장하기 전에 명시적으로 opt-in하도록 요구하고 싶을 수 있어요. 예를 들어 라이브러리 API가 사용에는 안정적이지만 상속에는 안정적이지 않을 수 있는데, 나중에 새로운 추상 함수로 확장될 가능성이 있기 때문이죠. 라이브러리 작성자는 open 또는 추상 클래스비함수형 인터페이스@SubclassOptInRequired 애너테이션으로 표시해서 이 요구사항을 강제할 수 있어요.

그런 API 요소를 사용하고 코드에서 확장하려면, 애너테이션 클래스를 참조하는 @SubclassOptInRequired 애너테이션을 쓰세요. 예를 들어 opt-in이 필요한 CoreLibraryApi 인터페이스를 사용하려 한다고 해볼게요.

// Library code
@RequiresOptIn(
 level = RequiresOptIn.Level.WARNING,
 message = "Interfaces in this library are experimental"
)
annotation class UnstableApi()

@SubclassOptInRequired(UnstableApi::class)
// An interface requiring opt-in to extend
interface CoreLibraryApi 

여러분의 코드에서 CoreLibraryApi 인터페이스로부터 상속하는 새 인터페이스를 만들기 전에, UnstableApi 애너테이션 클래스를 참조하는 @SubclassOptInRequired 애너테이션을 추가하면 돼요.

// Client code
@SubclassOptInRequired(UnstableApi::class)
interface SomeImplementation : CoreLibraryApi

클래스에 @SubclassOptInRequired 애너테이션을 사용할 때, opt-in 요구사항은 내부 또는 중첩 클래스에는 전파되지 않는다는 점을 기억하세요.

// Library code
@RequiresOptIn
annotation class ExperimentalFeature

@SubclassOptInRequired(ExperimentalFeature::class)
open class FileSystem {
    open class File
}

// Client code

// Opt-in is required
class NetworkFileSystem : FileSystem()

// Nested class
// No opt-in required
class TextFile : FileSystem.File()

@OptIn 애너테이션을 사용해서 opt-in할 수도 있어요. 실험 마커 애너테이션을 써서 코드에서 클래스를 사용하는 모든 지점으로 요구사항을 더 전파할 수도 있죠.

// Client code
// With @OptIn annotation
@OptInRequired(UnstableApi::class)
interface SomeImplementation : CoreLibraryApi

// With annotation referencing annotation class
// Propagates the opt-in requirement further
@UnstableApi
interface SomeImplementation : CoreLibraryApi

opt-in이 필요하도록 API 요구하기

여러분은 라이브러리 사용자가 API를 사용하기 전에 opt-in하도록 요구할 수 있어요. 또한 opt-in 요구사항을 제거하기로 결정하기 전까지, API 사용에 대한 특별한 조건을 사용자에게 알릴 수도 있어요.

opt-in 요구사항 애너테이션 만들기

모듈의 API를 사용하려면 opt-in이 필요하도록 하려면, opt-in 요구사항 애너테이션으로 쓸 애너테이션 클래스를 만들어야 해요. 이 클래스는 @RequiresOptIn으로 애너테이션해야 해요.

@RequiresOptIn
@Retention(AnnotationRetention.BINARY)
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
annotation class MyDateTime

opt-in 요구사항 애너테이션은 몇 가지 요구사항을 충족해야 해요. 다음을 가져야 합니다.

  • BINARY 또는 RUNTIME 유지(reteention).
  • targetEXPRESSION, FILE, TYPE, TYPE_PARAMETER가 없어야 함.
  • 매개변수가 없어야 함.

opt-in 요구사항은 두 가지 심각도 레벨 중 하나를 가질 수 있어요.

  • RequiresOptIn.Level.ERROR. opt-in이 필수예요. 그렇지 않으면 표시된 API를 사용하는 코드는 컴파일되지 않아요. 이게 기본 레벨이에요.
  • RequiresOptIn.Level.WARNING. opt-in이 필수는 아니지만 권장돼요. 없으면 컴파일러가 경고를 냅니다.

원하는 레벨을 설정하려면 @RequiresOptIn 애너테이션의 level 매개변수를 지정하면 돼요.

추가로 API 사용자에게 message를 제공할 수도 있어요. 컴파일러는 opt-in 없이 API를 사용하려는 사용자에게 이 메시지를 보여줘요.

@RequiresOptIn(level = RequiresOptIn.Level.WARNING, message = "This API is experimental. It can be incompatibly changed in the future.")
@Retention(AnnotationRetention.BINARY)
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
annotation class ExperimentalDateTime

opt-in이 필요한 독립적인 기능을 여러 개 배포한다면, 각각에 대해 애너테이션을 선언하세요. 그러면 클라이언트가 명시적으로 수락한 기능만 사용할 수 있으므로 API 사용이 더 안전해져요. 또 기능별로 opt-in 요구사항을 독립적으로 제거할 수 있어서 API 유지보수도 더 쉬워지죠.

API 요소 표시하기

API 요소를 사용하려면 opt-in이 필요하도록, 선언을 opt-in 요구사항 애너테이션으로 표시하면 돼요.

@MyDateTime
class DateProvider

@MyDateTime
fun getTime(): Time {}

몇몇 언어 요소에는 opt-in 요구사항 애너테이션을 적용할 수 없다는 점을 기억하세요.

  • backing field나 프로퍼티의 게터에는 애너테이션을 붙일 수 없고, 프로퍼티 자체에만 붙일 수 있어요.
  • 지역 변수나 값 매개변수에는 애너테이션을 붙일 수 없어요.

API 확장에 opt-in 요구하기

때로 API의 특정 부분을 사용하고 확장할 수 있는 범위를 더 세밀하게 제어하고 싶을 때가 있어요. 예를 들어 API가 사용에는 안정적이지만 이런 경우가 있을 수 있죠.

  • 지속적인 진화 때문에 구현이 불안정한 경우. 기본 구현 없는 새로운 추상 함수를 추가할 계획인 인터페이스 묶음이 있을 때처럼요.
  • 개별 함수들이 조율된 방식으로 동작해야 하는 등, 구현이 섬세하거나 깨지기 쉬운 경우.
  • 외부 구현에 대해 앞으로 하위 호환되지 않는 방식으로 계약이 약화될 수 있는 경우. 예컨대 입력 매개변수 T를 이전에 null 값을 고려하지 않던 코드에서 nullable 버전 T?로 바꾸는 경우처럼요.

이런 경우 사용자가 API를 더 확장하기 전에 opt-in하도록 요구할 수 있어요. 사용자는 API로부터 상속하거나 추상 함수를 구현해서 API를 확장하죠. @SubclassOptInRequired 애너테이션으로 open 또는 추상 클래스비함수형 인터페이스에 이 opt-in 요구사항을 강제할 수 있어요.

API 요소에 opt-in 요구사항을 추가하려면 애너테이션 클래스를 참조하는 @SubclassOptInRequired 애너테이션을 쓰세요.

@RequiresOptIn(
 level = RequiresOptIn.Level.WARNING,
 message = "Interfaces in this library are experimental"
)
annotation class UnstableApi()

@SubclassOptInRequired(UnstableApi::class)
// An interface requiring opt-in to extend
interface CoreLibraryApi 

@SubclassOptInRequired 애너테이션으로 opt-in을 요구할 때, 그 요구사항은 내부 또는 중첩 클래스에는 전파되지 않는다는 점을 기억하세요.

API에서 @SubclassOptInRequired 애너테이션을 실제로 어떻게 쓰는지 보려면 kotlinx.coroutines 라이브러리의 SharedFlow 인터페이스를 확인해 보세요.

사전 안정(Pre-stable) API의 opt-in 요구사항

아직 안정적이지 않은 기능에 opt-in 요구사항을 쓴다면, 클라이언트 코드를 깨뜨리지 않도록 API 졸업(graduation)을 신중히 다뤄야 해요.

사전 안정 API가 졸업해서 안정 상태로 출시되면, 선언에서 opt-in 요구사항 애너테이션을 제거하세요. 그러면 클라이언트가 제한 없이 사용할 수 있어요. 다만 기존 클라이언트 코드가 호환되도록 애너테이션 클래스는 모듈에 남겨 두어야 해요.

API 사용자가 코드에서 애너테이션을 제거하고 다시 컴파일하도록 모듈을 업데이트하게 하려면, 애너테이션을 @Deprecated로 표시하고 폐기 메시지에 설명을 넣으세요.

@Deprecated("This opt-in requirement is not used anymore. Remove its usages from your code.")
@RequiresOptIn
annotation class ExperimentalDateTime

더 알아보기 (Learn more)

  • 표준 입력 읽기 문서에서 입력 처리 기본기를 확인하세요.
  • 범위 함수 문서에서 스코프 함수를 살펴보세요.
  • opt-in 마커를 실제 API에 적용한 예로 kotlinx.coroutinesFlowPreview를 참고하세요.