본문 바로가기
Android/Compose 숨참고 Deep dive

Compose 컴파일러 Part 2 : Compose annotations

by DONXUX 2023. 4. 13.

@Composable

@Composable에 관한 내용을 살펴보시려면 아래 링크를 참고하시기 바랍니다.

https://ducorner.tistory.com/11

 

Compose 내부 구조

이 글은 2020.08.29에 작성된 Compose 세부구현에 관한 문서를 바탕으로 정리한 글이므로, 포스팅 작성 시기의 세부구현과는 다를 수 있음을 알려드립니다. 원문 : https://medium.com/androiddevelopers/under-the-

ducorner.tistory.com

 

@ExperimentalComposeApi

아직 안정화되지 않은 Composable이나 클래스에 적용되는 어노테이션입니다. 향후 변경될 수 있거나, 제거될 수 있음을 암시합니다.

 

@ComposeCompilerApi

이 어노테이션은 Compose 자체에서 컴파일러 외부에서 사용되지 않도록합니다. 따라서 컴파일러 외부에서 해당 부분을 사용하려고하면 인라인 오류가 발생합니다.

 

@InternalComposeApi

Compose 내부에서만 사용할 수 있는 API임을 나타냅니다. 향후 변경될 수 있음을 나타냅니다. Kotlin에서 지원하지 않는 모듈 간 사용을 허용하기 때문에 Kotlin의 internal 키워드보다 더 넓은 범위를 가집니다.

 

@DisallowComposableCalls

함수 내부에서 Composable 호출을 금지하도록 합니다.

그렇다면 함수 내부에서 Composable 호출을 금지해야할 상황이 무엇일까요? 이해를 돕기위해 ComposeNode 시그니처를 살펴봅시다.

 

@Composable inline fun <T : Any, reified E : Applier<*>> ComposeNode(
	noinline factory: () -> T, 
	update: @DisallowComposableCalls Updater<T>.() -> Unit
) {  
	// ... 
}

ComposeNode는 inline Compsoable 함수이며, update 람다 또한 inline으로 정의됩니다. 그리고 @DisallowComposableCalls 어노테이션이 지정되어있습니다. 만약, update 람다가 @DisallowComposableCalls 어노테이션을 지정하지 않으면 어떻게 될까요?

런타임에서 Recomposition이 될 때마다, 인라인을 호출하게되는데, 만약 람다 내부에 Composable 함수가 있다면 해당 Composable 함수가 예상치 못한 결과를 가질 수 있습니다(Recomposition 시, 같은 값을 가져도 inline이기 때문에 항상 호출됩니다). 따라서 이를 방지하기 위해 @DisallowComposableCalls를 사용해 Composable 호출을 차단합니다.

클라이언트 프로젝트에서는 거의 사용하지 않을 가능성이 높습니다. 

 

@ReadOnlyComposable

값을 읽기만 하는 Composeable에 사용하여 읽기 전용 Composable로 만듭니다(지정된 Composable의 하위 Composable들도 마찬가지로 값을 읽기만 해야합니다). 주로 텍스트, 이미지, 테마 또는 기타 업데이트 할 필요가 없는 UI 요소에서 사용됩니다. 예를 들면 로고, 라이선스 정보 등이 있습니다. 이 어노테이션을 지정하면 값이 불변(immutable)하다는 것을 보장하기 때문에, 런타임 중 읽기를 제외한 행위에 필요한 코드 생성을 방지할 수 있습니다. 이를 통해 최적화를 기대할 수 있습니다. 

 

우선 컴파일러는 Composable에 대한 Group을 생성하지만, ReadOnlyComposable은 Group을 생성하지 않습니다. 왜 그럴까요? 읽기 행위만 하는 ReadOnlyComposable은 Group 생성하는 것이 아무런 가치가 없기 때문입니다. 일반적으로 Composable은 컴파일러에서 Group을 생성하고 Slot table에 적재됩니다. 이 Group을 생성함으로써 런타임에서 Recomposition 시, 다른 Composable 함수의 데이터로 덮어씌워질 때 쓰여진 데이터를 정리하거나, id를 유지하면서 이동이 가능해지는 것입니다. 하지만 ReadOnlyComposable은 애초에 값이 업데이트가 되지 않기 때문에 Group을 생성조차 하지 않습니다.

 

@NonRestartableComposable

함수 또는 프로퍼티 getter에 적용하면, 해당 함수나 getter가 재시작이 불가능한 Composable이 됩니다. 이 어노테이션을 지정하면 컴파일러는 함수가 재구성되거나 재시작되지 않도록 필요한 부분을 생성하지 않습니다. 이 어노테이션은 다른 Composable 함수가 호출하여 다시 구성될 가능성이 매우 높은 작은 함수에 대해서 사용하는 것이 적절합니다. 그리고 이러한 함수들은 self-invalidate 되는 것이 그다지 유용하지 않기 때문에, 부모 Composable 함수에 의해 무효화/재구성되어야 합니다.

 

@StableMarker

이 어노테이션은 어노테이트된 타입의 데이터 안정성을 나타냅니다. @Immutable 및 @Stable과 같은 어노테이션을 어노테이트하는 메타-어노테이션입니다. 재사용성을 위해 만들어졌으며, 메타-어노테이션으로 어노테이트된 모든 어노테이션에도 적용됩니다.

 

이 어노테이션을 지정하기 위해 다음과 같은 요구사항을 충족해야합니다. 

  • 호출된 두 인스턴스에 대한 equals 호출 결과는 항상 동일해야 합니다.
  • public 속성이 변경될 때마다 Composition에 알림이 전달됩니다.
  • 어노테이션이 달린 유형의 모든 public 속성도 안정적이어야 합니다.

Compose에서는 위의 요구사항을 만족하면 '안정적(Stable)'이라고 표현합니다.

 

@Immutable 또는 @Stable로 어노테이션이 달린 유형은 이러한 요구 사항을 암시해야하며, 이는 둘 다 @StableMarker로 플래그가 지정되어 있기 때문입니다.

 

이 어노테이션은 컴파일러에게 약속을 하지만 컴파일러가 유효성을 검사하지는 않습니다. 이 어노테이션과 아래 어노테이션은 사용하는 개발자의 책임이며, 요구 사항이 모두 충족되어야만 사용하도록 주의해야합니다.

@Immutable

이 어노테이션은 클래스에 적용되며, 모든 공개 클래스 속성과 필드가 생성 후에 변경되지 않을 것이라는 엄격한 약속을 컴파일러에게 알리는 역할을 합니다.

이는 val 키워드보다 강력한 약속입니다. val은 속성이 setter를 통해 재할당 될 수 없도록 보장하지만, 여전히 가변 데이터 구조를 가리킬 수 있으므로, 데이터가 가변적인 경우가 있을 수 있습니다. 이는 Compose 런타임의 기대를 어길 수 있습니다. 다른 말로 하면, 코틀린 언어에는 불변을 보장하는 키워드나 기타 메커니즘이 없기 때문에 Compose에서 따로 어노테이션이 필요합니다.

값이 초기화 된 후에는 변경되지 않을 것이라는 가정을 기반으로, 런타임은 Smart recomposition 및 Recomposition 건너뛰기 기능에 최적화를 적용할 수 있습니다.

 

@Immutable로 표시해도 안전한 클래스의 좋은 예는 모든 속성이 val이고, 커스텀 getter가 없는 데이터 클래스입니다. 이 경우 모든 속성이 기본 타입이거나 @Immutable로 플래그 지정된 타입일 때만 해당됩니다. 커스텀 getter가 있다면 이는 호출될 때마다 계산되어 매번 다른 결과를 반환할 수 있기 때문에 비안정적인 API가 될 수 있습니다.

 

권장하지 않는 코드입니다.

@Immutable
data class User(
    val name: String,
    val age: Int,
    var bio: String,  // DO NOT use mutable data structure!
)
@Immutable
class User(
    val name: String,
    val age: Int,
) {
    val bio: String  // DO NOT use custom getter!
        get() = ...  // return Random string
}

 

@Immutable 어노테이션 불변 타입을 안정적으로 플래그 지정하기 위해 존재합니다.

 

@Stable

이 어노테이션은 적용된 요소에 따라 다른 의미를 갖습니다.

이 어노테이션이 타입에 적용되면, @Immutable과는 달리 값이 변경이 가능합니다. @StableMarker에서 상속받은 함축적 의미만 가지게 됩니다. 

 

함수나 프로퍼티에 @Stable 어노테이션이 적용되면, 컴파일러에게 함수가 항상 동일한 입력에 대해 동일한 결과를 반환한다는 것(순수 함수)을 알립니다. 이는 매개변수가 @Stable, @Immutable 또는 기본형(primitive types)일 경우에만 가능합니다(이들은 stable로 간주됩니다).

 

Composable 함수에 전달되는 모든 타입이 Stable으로 표시된 경우, 위치 메모이제이션에 기반하여 매개변수 값이 동일한지 여부를 비교하교 모든 값이 이전 호출과 동일한 경우 스킵합니다.

 

Reference

  1. Jorge Castillo, Andrei Shikov. 『Jetpack Compose internals』, 2021
  2. https://sungbin.land/a-deep-dive-into-jetpack-compose-stability-38b5b109da71