동기
DND 8기에 참여하여 프로젝트를 진행했을 때, 팀 동료분께서 먼저 앱 구조를 구축해주셨습니다. 그 때 궁금했던 것이 @Retention Annotation 입니다. CoroutineDispatcher 여러에 대해 서로 다른 Annotation 을 붙여주셔서, 어떤 기준으로 결정하신 건지 여쭤봤었는데 속 시원한 답변을 구하지 못했던 기억이 납니다.
문득 궁금해져, 이번 기회에 이에 관해 학습하고 정리하려 합니다.
Hilt 사용 시 사용되는 Annotation 에 관해서는 이미 정리해 둔 포스팅이 있습니다. 심지어 이번 포스팅에서 다룰 @Retention 에 관해서도 간략하게 작성했습니다.
🤔 근데 왜 또 포스팅하나요?
간단한 사용법과 각 항목에 대한 개요만 설명하였을 뿐, 어떻게 동작하는지 학습 및 작성하지 않았기 때문에, 기억도 잘 안 나고 어딘가 찜찜했기 때문입니다.
@Retention
Java 또는 Kotlin 에서 annotation class 를 생성할 때 사용할 수 있는 Meta-Annotation 입니다. 즉, 'Annotation 의 Annotation' 이라고 말할 수 있습니다. 사용자 정의 Annotation 을 작성할 때, 해당 Annotation 의 보존 정책을 지정하기 위해 사용됩니다.
@Retention(AnnotationRetention.RUNTIME)
@Qualifier
annotation class DefaultDispatcher
...
보존 정책
Annotation 의 정보가 컴파일 후에 얼마나 오래 유지되어야 하는지를 나타내는 것으로, 다음과 같은 세 가지 주요 값이 있습니다.
public enum class AnnotationRetention {
/** Annotation isn't stored in binary output */
SOURCE,
/** Annotation is stored in binary output, but invisible for reflection */
BINARY,
/** Annotation is stored in binary output and visible for reflection (default retention) */
RUNTIME
}
kotlin.annotation 패키지에 포함된 클래스입니다. 간단한 주석이 포함되어 있는데, 이를 포함하여 다음과 같이 정리할 수 있습니다.
1. SOURCE
해당 Annotation 은 컴파일 중에만 유지됩니다. 컴파일 후에는 Annotation 정보가 사라집니다.
여기서 '컴파일 후' 란, 컴파일러에 의해 .class 파일이 되었을 때를 의미합니다.
2. BINARY or CLASS
해당 Annotation 은 컴파일된 바이너리 코드 (.class 파일) 에 포함되지만, 런타임에는 그 정보에 접근할 수 없습니다.
3. RUNTIME
해당 Annotation 은 컴파일된 바이너리 코드에 포함되며, 런타임에 Reflection 을 통해 그 정보에 접근할 수 있습니다. 사용자가 런타임에 Annotation 정보에 접근해야 하는 경우에 사용하면 됩니다.
위를 표로 정리하면 다음과 같습니다.
그래서, 어떻게?
동작 방식을 확인하기 위해, @Retention Annotation 이 붙은 코드를 바이트 코드로 변환해 보았습니다.
public final class com/jh/retention/ui/splash/SplashViewModel extends androidx/lifecycle/ViewModel {
@Ldagger/hilt/android/lifecycle/HiltViewModel;() // invisible
// access flags 0x12
private final Lkotlinx/coroutines/CoroutineDispatcher; ioDispatcher
// access flags 0x1
public <init>(Lkotlinx/coroutines/CoroutineDispatcher;)V
@Ljavax/inject/Inject;()
// annotable parameter count: 1 (visible)
// annotable parameter count: 1 (invisible)
@Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0
L0
ALOAD 1
LDC "ioDispatcher"
INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)V
L1
LINENUMBER 10 L1
ALOAD 0
INVOKESPECIAL androidx/lifecycle/ViewModel.<init> ()V
ALOAD 0
ALOAD 1
PUTFIELD com/jh/retention/ui/splash/SplashViewModel.ioDispatcher : Lkotlinx/coroutines/CoroutineDispatcher;
RETURN
L2
LOCALVARIABLE this Lcom/jh/retention/ui/splash/SplashViewModel; L0 L2 0
LOCALVARIABLE ioDispatcher Lkotlinx/coroutines/CoroutineDispatcher; L0 L2 1
MAXSTACK = 2
MAXLOCALS = 2
@Lkotlin/Metadata;(mv={1, 8, 0}, k=1, d1={"\u0000\u0012\n\u0002\u0018\u0002\n\u0002\u0018\u0002\n\u0000\n\u0002\u0018\u0002\n\u0002\u0008\u0002\u0008\u0007\u0018\u00002\u00020\u0001B\u000f\u0008\u0007\u0012\u0006\u0010\u0002\u001a\u00020\u0003\u00a2\u0006\u0002\u0010\u0004R\u000e\u0010\u0002\u001a\u00020\u0003X\u0082\u0004\u00a2\u0006\u0002\n\u0000\u00a8\u0006\u0005"}, d2={"Lcom/jh/retention/ui/splash/SplashViewModel;", "Landroidx/lifecycle/ViewModel;", "ioDispatcher", "Lkotlinx/coroutines/CoroutineDispatcher;", "(Lkotlinx/coroutines/CoroutineDispatcher;)V", "presentation_debug"})
// compiled from: SplashViewModel.kt
}
1. SOURCE
public <init>(Lkotlinx/coroutines/CoroutineDispatcher;)V
@Ljavax/inject/Inject;()
앞서 기술하였듯이, SOURCE 의 경우 소스 코드에만 남아있고, 컴파일되면 지워집니다. 그렇기 때문에, Inject 되었다는 코드만 남아있을 뿐입니다.
2. BINARY
public <init>(Lkotlinx/coroutines/CoroutineDispatcher;)V
@Ljavax/inject/Inject;()
@Lcom/jh/retention/di/coroutine/IoDispatcher;() // invisible, parameter 0
BINARY 는 컴파일 후에 .class 파일에는 남지만, 런타임에 Reflection 을 통해 접근할 수는 없습니다. 그러므로, @IoDispatcher 가 남아있지만 invisible 이라는 주석도 함께 작성되어 있습니다. 즉, 런타임에는 invisible 하기 때문에 런타임에는 접근할 수 없다는 것을 의미합니다.
3. RUNTIME
public <init>(Lkotlinx/coroutines/CoroutineDispatcher;)V
@Ljavax/inject/Inject;()
@Lcom/jh/retention/di/coroutine/IoDispatcher;() // parameter 0
BINARY 와 거의 같은데, invisible 주석만 삭제 되었습니다. 그러므로, 런타임에 Reflection 으로 접근할 수 있습니다.
SOURCE 와 나머지 값들 사이의 차이는 알 수 있었습니다. SOURCE 의 경우, 실제로 .class 파일에 해당 Annotation 이 삭제되어 있으니까요. 그러나 BINARY 와 RUNTIME 간에는 주석 차이 밖에 없습니다. 주석은 해석되지 않을테니, @Retention 을 선언한 부분에서 그 답을 찾을 수 있을 것 같습니다.
SOURCE 로 구현하고 바이트 코드로 변환했습니다.
@Lkotlin/annotation/Retention;(value=Lkotlin/annotation/AnnotationRetention;.SOURCE)
@Ljavax/inject/Qualifier;()
@Ljava/lang/annotation/Retention;(value=Ljava/lang/annotation/RetentionPolicy;.SOURCE)
다음은 BINARY 입니다.
@Lkotlin/annotation/Retention;(value=Lkotlin/annotation/AnnotationRetention;.BINARY)
@Ljavax/inject/Qualifier;()
@Ljava/lang/annotation/Retention;(value=Ljava/lang/annotation/RetentionPolicy;.CLASS)
마지막으로 RUNTIME 입니다.
@Lkotlin/annotation/Retention;(value=Lkotlin/annotation/AnnotationRetention;.RUNTIME)
@Ljavax/inject/Qualifier;()
@Ljava/lang/annotation/Retention;(value=Ljava/lang/annotation/RetentionPolicy;.RUNTIME)
즉, 설정해 둔 값에 따라 AnnotationRetention 과 RetentionPolicy 가 변경됩니다.
계속해서 가려웠던 부분인데, 이번 기회에 긁어 줄 수 있어 좋았습니다. 물론 여전히 찜찜한 부분이 약간 남았지만요.
동작 방식을 알아내기 힘들 때는 바이트 코드로 변환해서 읽어 보는 편이 방향성에 대한 인지에 도움이 되는 것 같다는 것을 느낍니다.
'Android > Tech' 카테고리의 다른 글
RecyclerView 의 업데이트와 DiffUtil (0) | 2023.09.29 |
---|---|
Android ViewModel 의 onCleared() 는 언제 호출되는가? (0) | 2023.09.14 |
Kapt, 그리고 KSP (0) | 2023.09.01 |
내부 리소스 접근에 왜 Context 가 필요한 걸까? (0) | 2023.08.21 |
우리는 어떻게 Modularization 해야 하는가? (0) | 2023.08.13 |