• 목록 (128)
    • Android (62)
    • Back-End (2)
    • Java (3)
    • Kotlin (16)
    • CS (7)
    • 개발 서적 (12)
    • 문제 풀이 (26)

최근 글

티스토리

전체 방문자
오늘
어제
hELLO · Designed By 정상우.
MJ_94

한 우물만 파는 기술 블로그

Android

[Android] Compose 컴파일러 - (2)

2024. 11. 14. 13:13

Compose Annotations

Compose Compiler가 필요한 요소들을 스캔하고 변환할 수 있도록 마커의 역할을 해주는 어노테이션들은 어떻게 활용되는 걸까?

Compose Compiler는 컴파일 단계에서 소스코드를 스캔하고 제약 사항이 충족되고 있는지, 타입 시스템이 Composable 함수나 선언 표현식을 일반 함수들과는 다르게 처리하고 있는지 확인한다.

그 외에도 Compose는 추가적인 검사와 다양한 런타임 최적화를 위해 추가적인 어노테이션들을 제공한다.

모든 Compose 어노테이션은 Compose Runtime 라이브러리에서 제공된다.

@Composable

대부분의 어노테이션 프로세스는 표현식을 변형하는 행위를 할 수 없다.

그렇기에 @Composable 어노테이션이 붙어있는 대상을 Compose Compiler가 소스코드를 해석하는 과정에서 변환을 수행한다.

@Composable을 통해 선언이나 표현식의 타입을 표시하는 것은 메모리를 부여하는 것을 의미한다.

즉, remember를 호출하고 Composer를 활용할 수 있는 것을 의미하며, Composable의 범위 내에서 구동된 이펙트들이 준수할 라이프사이클을 제공한다.

이렇게 만들어진 Composable 함수들은 메모리에 저장 될 수 있도록 각각의 고유 ID를 할당받고, 구성된 트리 내에서 위치가 지정된다.

@ComposableCompilerApi

Compose에서 Compose Compiler에 의해서만 사용된다는 의도를 가지고 있으며, 사용자에게 해당 사실을 인지시키고 주의를 주기 위한 목적을 가진다.

@DisallowComposableCalls

함수 내에서 Composable 함수의 호출을 방지하기 위해 사용된다. 이 어노테이션은 Composable 함수의 인라인 람다 매개변수에서 유용하게 사용될 수 있다. 주로 recomposition 마다 호출되면 안 되는 람다식에 사용하기에 적합하다.

이 어노테이션에 대한 예시로는 Compose Runtime의 remember 함수의 구현부에서 찾아볼 수 있다. remember 함수는 caculation 블록에 의해 제공된 값을 기억하며, 최초의 composition에만 수행되며 recomposition 시에는 이미 계산된 값만을 반환 한다.

하지만 이 @DisallowComposableCalls로 마킹된 인라인 람다 내부에서 또 다른 인라인 람다를 호출하는 경우, 컴파일러에서 해당 람다도 @DisallowComposableCalls로 표시해야 한다는 점에서 “전파성“이 있으며 사용에 있어 바람직하지 않은 케이스들이 더러 있어 일반적인 클라이언트 프로젝트에선 사용되지 않을 가능성이 높다는 단점이 있다.

하지만 알아두면 좋다.

@Composable
inline fun <T> remember(calculation: @DisallowComposableCalls () -> T): T =
	currentComposer.cache(false, caculation)

@ReadOnlyComposable

해당 어노테이션이 함수에 적용되면 해당 함수는 composition에 사용되지 않고 오직 읽기만을 지원한다는 의도를 가진다.

내부 composition에 쓰이는 함수의 경우에 Compiler는 Composable 함수의 본문을 감싸는 “그룹”을 생성하기 때문에 Compose Runtime에 해당 그룹을 방출한다.

이렇게 방출된 그룹은 Composable 함수에 대한 필수 정보를 composition에 제공하므로, 필요에 따라 recomposition이 다른 Composable을 데이터로 재정의해야 할 때 이미 정의된 데이터를 정리하는 방식이나 Composable 함수의 고유성을 유지하며 데이터를 이동시키는 방식을 알 수 있다.

Composable 함수가 composition에 쓰이지 않으면, 데이터가 교체되거나 이동되지 않게 되므로 아무런 가치가 없게 된다.

따라서 @ReadOnlyComposable 어노테이션은 이와 같은 상황을 방지하게 된다.

@ReadOnlyComposable의 사용 예로는 Colors, Typography, LocalContext과 같은 CompositionLocal을 통해 읽어 들이는 필드의 디폴트 값이나 이를 위임하는 유틸리티들이 있다.

@NonRestartableComposable

해당 어노테이션은 함수나 프로퍼티의 getter에 사용하면 기본적으로 재시작이 불가능한 Composable 함수가 된다.

즉 해당 어노테이션이 붙으면 Compiler는 함수가 recomposition 되는 동안 함수를 재구성하거나 생략하는데 필요한 보일러 플레이트 코드를 생성하지 않는다.

사용성은 매우 드물지만, 이런 특성을 통해 최적화를 할 수 있는 경우도 있지 않을까 싶다.

@StableMarker

Compose Runtime은 타입 안정성을 보장하기 위해 @StableMarker, @Immutable, @Stable 어노테이션을 사용한다.

@StableMarker는 @Immutable, @Stable에 사용되는 메타 어노테이션으로, 재사용이라는 의미를 내포하고 약간 중복되는 의미 같겠지만 어노테이션을 위한 어노테이션으로써 존재한다.

@StableMarker은 궁극적으로 해당 어노테이션으로 마킹된 타입의 데이터 안정성을 결정짓는데 다음 요구사항이 있다.

  • equals 함수의 호출 결과는 두 인스턴스가 동일해야 한다.
  • 어노테이션이 적용된 public 프로퍼티에 변경이 발생하면 composition에 알린다.
  • 어노테이션이 적용된 public 프로퍼티는 stable 하다고 간주한다.

@StableMarker은 어노테이션의 안정성을 위한 마커이기 때문에 @Immutable이나 @Stable로 마킹된 모든 타입은 위의 요구 사항을 만족해야 한다.

사실 타입 안정성과 관련된 어노테이션을 사용하지 않아도 컴파일러의 기능만으로도 정확성을 보장할 수 있기 때문에 굳이 사용하지 않아도 되지만, 개발자가 직접 어노테이션을 달아주어야 하는 두 가지의 경우가 있다.

  1. 인터페이스 또는 추상클래스에서 필수적으로 구현해야 하는 요구사항이 있을 경우 해당 어노테이션은 컴파일러와 약속일뿐만 아니라 선언에 대한 구현하기 위한 요구 사항이 된다.
  2. 구현체는 가변적이지만, 개발자의 판단하에 안정성을 가정하고 안정적인 타입으로 처리하고 싶은 경우에 해당 타입의 public API가 캐시 상태와 독립적인 경우

@Immutable

이 어노테이션은 클래스를 대상으로 사용되며, 해당 클래스는 인스턴스 생성 이후에 외부로 노출된 프로퍼티의 필드가 변경되지 않을 것임을 컴파일러에게 약속해 준다.

이는 Kotlin의 Val(Value) 보다 훨씬 강력한 약속으로, val은 프로퍼티가 setter를 통해 재할당 될 수 없지만 가변적인 데이터 구조를 참조할 수 있기 때문에 데이터가 val일지라도 사실상 데이터는 가변적일 수 있다.

이러한 동작은 Compose Runtime의 예상 범주를 벗어나는 동작을 초래할 수 있으므로 @Immutable 어노테이션은 스마트 recomposition 혹은 recomposition을 생략할 수 있는 Compose에게 필수적이다.

@Immutable을 마킹할 수 있는 클래스의 좋은 예는 모든 속성이 val로 구성되고 원시타입인 데이터 클래스이다.

@Stable

@Stable은 @Immutable에 비해선 조금 덜 엄격한 약속이다.

이 어노테이션이 타입에 적용되면 해당 타입은 가변임을 의미하고 @StableMarker에 의한 상속의 의미만 지니게 된다.

대신 @Stable 어노테이션을 함수나 프로퍼티에 적용하면 함수가 항상 동일한 입력값에 대해 동일한 결과를 변환한다는 사실을 컴파일러에게 약속한다.

이는 함수의 매개변수가 @Stable이거나 @Immutable로 마킹되어 있거나, 기본 타입(primitive)인 경우에만 가능하다.

 


 

@Immutable과 @ Stable은 서로 다른 의미를 가진 약속이더라도, 현재의 Compose Compiler는 스마트 recomposition과 recomposition을 생략하는 기능을 활성화하기 위해 두 어노테이션을 동일한 방식으로 취급한다.

향후 Compose Compiler와 Runtime에서 두 어노테이션의 차이점을 활용하기 위해 서로 다른 의미로 적용하고 미래의 Jetpack Compose를 위해 가능성을 열어두기 위해 @Immutable 과 @ Stable로 나누어져 있는 것이다.

저작자표시 비영리 변경금지 (새창열림)

'Android' 카테고리의 다른 글

[Android] Compose 컴파일러 - (4)  (0) 2024.11.18
[Android] Compose 컴파일러 - (3)  (0) 2024.11.15
[Android] Compose 컴파일러 - (1)  (0) 2024.11.13
[Android] Compose Composable 함수들 - (3)  (0) 2024.11.12
[Android] Compose Composable 함수들 - (2)  (0) 2024.11.12
    'Android' 카테고리의 다른 글
    • [Android] Compose 컴파일러 - (4)
    • [Android] Compose 컴파일러 - (3)
    • [Android] Compose 컴파일러 - (1)
    • [Android] Compose Composable 함수들 - (3)
    MJ_94
    MJ_94
    안드로이드, 개발 관련 기술 블로그

    티스토리툴바