우테코 8기/Kotlin

Kotlin Annotation Retention

Reddish 2026. 9. 20. 18:37
반응형

Retention 어노테이션


Retention은 보유, 유지력이라는 명사이다.

/**
 * This meta-annotation determines whether an annotation is stored in binary output and visible for reflection. By default, both are true.
 *
 * @property value necessary annotation retention (RUNTIME, BINARY or SOURCE)
 */
@Target(AnnotationTarget.ANNOTATION_CLASS)
public annotation class Retention(val value: AnnotationRetention = AnnotationRetention.RUNTIME)

@Retention을 타고 들어가보면 위와 같은 annotation class가 나온다. 어노테이션 클래스를 대상으로 하는 어노테이션 클래스이다. 그럼 어노테이션을 보유하거나 유지하는 것과 관련된 어노테이션일 것이다.

주석을 보면 어노테이션이 바이너리 출력에 저장될지 또는 리플렉션에 보여질지를 결정한다고 이해했다. 디폴트로는 둘 다 true로 된다고 했다.

 

Kotlin language specification

그대로 retention을 찾아봤는데, 컴파일러에 대해 어떤 컴파일러 아티팩트가 어노테이션을 보존할지 지정하는 것이라고 한다.

그 방법엔 세 가지가 있는데,

  1. Source Retention (소스 처리 툴을 통한 접근)
    1. 컴파일 타임에 이용 가능하고, 컴파일에 의해 폐기된다.
    2. 컴파일된 .class 파일에 저장되지 않는다.
  2. Binary Retention (컴파일 아티팩트를 통한 유지)
    1. 컴파일 된 .class 파일에 저장된다.
    2. 런타임일 때는 리플렉션을 통해 이용할 수 없다
  3. Runtime Retention (런타임 접근)
    1. 컴파일된 클래스파일에 저장된다.
    2. 런타임일 때 리플렉션을 통해 이용할 수 있다.
    3. 해당 방식을 기본 값으로 제공한다.

 

위 세 가지를 좀 자세하게 이해하려면 컴파일 과정을 알아야한다.

우리가 작성한 .kt 파일은 코틀린 컴파일러에 의해 .class파일(바이트코드)로 변환된다. JVM의 클래스 로더가 .class 파일을 읽고 그 이후에 런타임 데이터 영역(힙, 스택 등)에 올리게 된다.

 

근데…언제 사용하는지, Binary가 정말 실제로 자주 사용되는지 모르겠다. 그래서 리뷰어에게 몰래 헬프를 쳤다.

질문 요약

  • 커스텀 Annotation을 직접 만드는 일이 실제 개발에서 흔한가?
    • 언제 만들고 어떤 용도로 사용하는지 궁금함.
    • 실무에서도 자주 쓰는지 궁금함.
  • Retention(SOURCE, BINARY, RUNTIME)에 대한 내 이해가 맞는가?
    • SOURCE: 컴파일 과정에서만 사용되고 클래스 파일에는 남지 않음.
    • BINARY: 클래스 파일에는 남지만 런타임에는 접근 불가.
    • RUNTIME: 클래스 파일에 남고 런타임 리플렉션으로 접근 가능.
    • 이렇게 이해한 게 맞는지 확인받고 싶음.
  • @Serializable은 왜 RUNTIME인가?
    • 직렬화 코드를 컴파일 시점에 생성해주는 것처럼 보이는데,
    • 그렇다면 SOURCE나 BINARY여야 할 것 같은데 왜 RUNTIME인지 이해가 안 됨.
  • BINARY Retention은 언제 사용하는가?
    • RUNTIME처럼 리플렉션으로 읽을 수도 없는데 왜 존재하는지,
    • 어떤 상황에서 사용하는지 궁금함.

리뷰어님이 말해준거랑 코틀린 컴파일 방식을 보고 Retention들을 다시 한번 공부해보기로 했다.

  • Source

첫 번째 방식인 Source Retention의 경우, .kt에서 어노테이션을 사용하면 컴파일러가 이를 보고 해석 한 후 컴파일러에서 어떤 처리가 일어나고, 이 처리 후 .class에선 어노테이션은 사라질 것으로 예상할 수 있다.

대표적으로는 @Suppress 처럼 IDE, 컴파일러 경로를 억제할 때 사용할 수 있다. 또한, @VisibleForTesting 처럼 테스트용이라는 의도를 개발자에게 전달할 때 사용하기도 한다.

KSP, KAPT 같은 코드 생성 도구가 소스 분석 단계에서만 참조하는 마커로 쓸 수 있다고 한다.

  • Binary

두 번째 방식인 Binary Retention의 경우, .kt에서 컴파일러가 이를 보고 그대로 둔다고 생각할 수 있다. 다만 런타임에 올라가지 않아 .class와 런타임 사이에 해당하는 부분을 버리는 방법이 있을 것이다. 라고 유추 할 수 있을 것 같다.

즉, 컴파일이 끝난뒤에도 살아있지만, 런타임에 올라가기 전에는 지운단 뜻이 될 것이다.

JVM 클래스 파일 명세에는 RuntimeInvisibleAnnotations랑 RuntimeVisibleAnnotations가 있는데, 런타임에 보여질 어노테이션과 보여지지 않을 어노테이션을 의미한다. 여기서 Binary Retention은 컴파일러에게 해당 어노테이션을 RuntimeInvisibleAnnotations 영역에 작성써달라고 요청하는 것이다.

결국 JVM 클래스 로더는 클래스를 메모리에 로드할 때 해당 영역을 읽은 뒤 힙에 올리지 않는다.

Java Development Kit Version 24 API Specification(RuntimeInvisibleAnnotations에 대한 명세서)

따라서 .class파일을 바이트코드 뷰어 javap -verbose로 열면 어노테이션이 보이겟지만, 실행중에 reflection으로 접근하면 찾을 수 없을 것이다.

컴파일 타임 도구인 바이트코드 조작 라이브러리, IDE 플러그인, 빌드 도구가 .class파일을 직접 분석할 때이다. 도구들은 JVM 없이 바이트코드를 읽으므로 RuntimeInvisibleAnnotation에 접근할 수 있고, 리플렉션에 대한 비용은 지불하지 않기 때문이다.

사실 이것만 듣고 Binary의 존재 이유에 대해서는 설명이 잘 안된다고 생각했는데, 아래의 블로그 댓글을 보고 이해했다.

Binary Annotation Retention의 존재 이유

아무 관심 없던 @Retention 어노테이션 정리(RetentionPolicy SOURCE vs CLASS vs RUNTIME)

대종훈님의 댓글에 해당 내용이 나와있다.

자바 CLASS 정책이 코틀린의 Binary 정책과 대응한다고 생각하면 된다. IDE 에서는 null이 아닌 값에 null을 경우 경고를 준다. 물론 이는 Source 로 해결할 수 있을 것이라는 생각이 든다.

그러나, 외부 라이브러리를 사용하면서 Gradle, Maven으로 의존성을 추가하게 되면, JAR파일이 오게된다. 그러면 .class 파일만 있다. 소스코드 없이. 따라서 .class 파일만 존재하는 라이브러리 같은 경우에, 타입 확인, IDE 부가기능등을 이용할 수 있으려면 Class 정책, 즉 Binary 정책이 필요하다고 한다.

 

  • Runtime

Binary는 JVM의 클래스 로더가 힙에 해당 어노테이션을 올리지 않는다면, Runtime은 반대로 올라가게 된다. 프로세스가 살아있는 동안 해당 정보가 소멸되지 않아 리플렉션이 런타임에 그 값을 읽어올 수 있다.

리플렉션이 가능하려면, retention이 Runtime으로 설정되어야 하고, 또, 대상 선언을 KClass, KFunction 등 KReflect 객체로 가져오고 .annotations 또는 findAnnotation<T>와 같은 API를 호출해야한다. → 무슨말인지 모르겠다. 일단 넘어가도록 하자

길게 살아남지만, 비용이 따른다. RUNTIME 은 모든 어노테이션 클래스가 힙에 로딩할 때 상주하고, 리플렉션을 사용하면 리플렉션 체인을 타서 직접 필드 접근보다 수십~수백 배 느릴 수 있다.

Kotlin Reflection 라이브러리를 의존성에 추가해야하는 문제도 있다. 해당 라이브러리는 약 2.8MB 정도 되므로 또한, 리플렉션을 사용하는 클래스가 난독화 된다는 문제도 있다.

Retrofit, Room, 직렬화가 위와 같은 케이스에 해당한다. 런타임에 동적으로 클래스를 읽어야 하는 경우이다.

 

아는 어노테이션 뜯어보기

Deprecated

@Target(CLASS, FUNCTION, PROPERTY, ANNOTATION_CLASS, CONSTRUCTOR, PROPERTY_SETTER, PROPERTY_GETTER, TYPEALIAS)
@MustBeDocumented
public annotation class Deprecated(
    val message: String,
    val replaceWith: ReplaceWith = ReplaceWith(""),
    val level: DeprecationLevel = DeprecationLevel.WARNING
)

위를 보면 Retention이 따로 지정되지 않았다. 그러면 기본값인 RUNTIME이라고 생각할 수 있을 것이다.

근데 Deprecated의 의도는 내가 알기론 다른 개발자에게 해당하는 대상이 버그가 있거나, 보안 상 문제가 있어서, 또는 더 나은 방법이 있어서 더 이상 지원하지 않아 다른 것으로 대체하는 것을 추천한다 이런 느낌으로 알고 있는데,

이 정보를 다른 개발자들에게 알려주는 것이라면 RUNTIME으로 리플렉션까지 알려줘야하나? 라는 생각이 들었다.

나랑 비슷한 의문을 가진 사람을 찾았다. 역시 내가 했던 생각들은 이미 다른 누군가가 많이 했나보다.

Why the RetentionPolicy for @deprecated is RUNTIME?

Why RetentionPolicy.RUNTIME is used for @Deprecated annotation?

위 내용들은 자바 기반이긴 하다. 댓글들 대부분이 “결국 다~ 쓸데가 있다!” 이 말을 하고 있다. 프레임워크랑 툴에서 저걸 검사해야 할 때가 있다는데, 사실 잘 와닿지는 않는다.

정말 모르겠다. 나중의 숙제로...

반응형

'우테코 8기 > Kotlin' 카테고리의 다른 글

Flow, StateFlow, SharedFlow  (0) 2026.10.04
Kotlin coroutine - suspend 함수  (0) 2026.09.05
expect / actual 키워드 (Kotlin Multiplatform, KMP)  (0) 2026.08.17
가변성을 제한하라  (0) 2026.07.19
Annotation과 Annotation Target  (2) 2026.06.03