Kotlin @PublishedApi 어노테이션

목차
@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
인 인라인 함수는 컴파일 시 호출 영역으로 옮겨질 것이다.
따라서 private
나 internal
같은 접근제한자로 선언된 다른 함수는 인라인 함수를 호출한 부분에서 접근할 수 없다.
이 때문에 public
인라인 함수 내부에서는 public
이 아닌 다른 함수를 호출 할 수 없다.
@PublishedApi #
이런 경우를 위해 @PublishedApi
어노테이션이 존재한다.
@PublishedApi
는 internal
접근제한자와 함께 사용할 수 있다.
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 라이브러리에서도 HorizontalGrid
와 VerticalGrid
컴포저블은 인라인 함수이지만 내부에서 필요한 함수들은 @PublishedApi
가 붙은 internal
로 정의되어 있다.