메인 콘텐츠로 이동하기

Kotlin @PublishedApi 어노테이션

·3 분
Cover
Cover art generated by ChatGPT

@PublishedApi 어노테이션은 Kotlin에서 제공해주는 기본 어노테이션 중 하나다. 자주 사용되지는 않지만 라이브러리를 개발하거나 잘 정리된 내부 모듈을 개발할 때 사용하면 유용하게 쓸 수 있는 어노테이션이다. 이 글에서는 인라인 함수와 @PublichedApi를 언제 사용하는지 알아볼 것이다.

inline 함수와 문제점 #

Kotlin에서 inline 키워드를 사용하면 인라인 함수를 선언할 수 있다. 인라인 함수는 컴파일 시점에 함수 호출 부분을 함수의 내용으로 치환시켜 최적화 된다. 간단하게 예시를 들어 보자면 아래와 같다.

fun main() {
    println("Hello, World!")
    action { i ->
        println("Loop: $i")
    }
    printlin("Action Finished")
}

inline fun action(callback: (Int) -> Unit) {
    for (i in 0 until l0) {
        callback(i)
    }
}

action은 인라인 함수다. 소스 코드상에서는 별도의 함수로 보이지만 실제로 컴파일 시에 action의 동작은 호출 부분으로 이동한다.

fun main() {
    println("Hello, World!")
    for (i in 0 until l0) {
        println("Loop: $i")
    }
    printlin("Action Finished")
}

실제 컴파일 이후는 마치 위의 코드를 짠 것처럼 동작할 것이다. 이런 것을 인라이닝(inlining) 이라고 한다.

인라이닝을 하면 함수 호출이라는 작업을 줄여 성능을 최적화 시키고 추가적으로 컴파일러의 최적화를 받게 만들어 성능을 끌어 올릴 수 있다. 하지만 너무 남용하게 되면 같은 코드가 여기 저기에 붙여넣어져 바이너리의 크기를 증가시키는 단점이 있다. Intellij류의 IDE에서는 Kotlin에서 inline이 불필요한 경우 경고를 띄워 인라이닝의 남용을 최소화 해준다.

하지만 인라인 함수는 문제점이 한 가지 존재한다. public inline 함수는 그 내부에서 public이 아닌 API를 사용할 수 없다.

public인 인라인 함수는 컴파일 시 호출 영역으로 옮겨질 것이다. 따라서 privateinternal같은 접근제한자로 선언된 다른 함수는 인라인 함수를 호출한 부분에서 접근할 수 없다. 이 때문에 public 인라인 함수 내부에서는 public이 아닌 다른 함수를 호출 할 수 없다.

@PublishedApi #

이런 경우를 위해 @PublishedApi 어노테이션이 존재한다.

@PublishedApiinternal 접근제한자와 함께 사용할 수 있다. internal API에 어노테이션을 붙이면 컴파일러는 컴파일 시 해당 API를 public으로 변환해 public inline 함수에서도 접근이 가능하게 만들어준다.

이 변환은 컴파일 된 바이너리 코드에서 일어나는 일이며, 소스 코드 상에서는 internal이 유지되어 외부에서 접근할 수 없다. 따라서 외부에서 접근하지 않기를 바라는 API를 숨기면서 동시에 public inline 함수 내부 동작을 정리할 수 있다.

실제 사용 예시 #

@Immutable
@JvmInline
value class DpSize internal constructor(@PublishedApi internal val packedValue: Long) {
    // ...
}

Jetpack Compose의 Dp 클래스는 내부적으로 packedValue@PublishedApi가 붙은 internal로 정의하고 있다. 따라서 다른 외부 함수에서는 packedValue에 직접 접근할 수 없지만 관련된 public API들을 사용할 수는 있다.

여기서는 inline 함수가 아니라 value class 때문인데, value class는 일종의 인라인 클래스라고 이해할 수 있다. Kotlin에서 value class로 선언된 클래스는 컴파일 시 감싸는 객체를 제거하고 value class가 가진 프로퍼티로 대체한다. 즉, 위의 Dp 클래스는 컴파일 시 Long으로 치환된다.
@Composable
inline fun HorizontalGrid(
    rows: SimpleGridCells,
    modifier: Modifier = Modifier,
    horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
    verticalArrangement: Arrangement.Vertical = Arrangement.Top,
    alignment: Alignment = Alignment.TopStart,
    content: @Composable GridScope.() -> Unit,
) {
    val calculateRowCellHeightsFunction = rememberRowCellHeightConstraints(
        rows = rows,
        verticalArrangement = verticalArrangement,
    )
    val measurePolicy = rememberHorizontalGridMeasurePolicy(
        calculateRowCellHeightConstraints = calculateRowCellHeightsFunction,
        fillCellHeight = remember(rows) { rows.fillCellSize() },
        horizontalArrangement = horizontalArrangement,
        verticalArrangement = verticalArrangement,
        alignment = alignment
    )
    Layout(
        content = { GridScopeInstance.content() },
        measurePolicy = measurePolicy,
        modifier = modifier,
    )
}

@PublishedApi
@Composable
internal fun rememberRowCellHeightConstraints(
    rows: SimpleGridCells,
    verticalArrangement: Arrangement.Vertical,
): Density.(Constraints) -> List<Int> {
    // ...
}

@PublishedApi
@Composable
internal fun rememberHorizontalGridMeasurePolicy(
    calculateRowCellHeightConstraints: Density.(Constraints) -> List<Int>,
    fillCellHeight: Boolean,
    horizontalArrangement: Arrangement.Horizontal,
    verticalArrangement: Arrangement.Vertical,
    alignment: Alignment,
): MeasurePolicy {
    // ...
}

GridLayout for Compose 라이브러리에서도 HorizontalGridVerticalGrid 컴포저블은 인라인 함수이지만 내부에서 필요한 함수들은 @PublishedApi가 붙은 internal로 정의되어 있다.

레퍼런스 #