본문으로 건너뛰기
안동민 개발노트 아이콘

안동민 개발노트

본문 시작
9장 : 고급 타입 모델링

애너테이션 정의와 사용

애너테이션은 프로그램 요소에 구조화된 메타데이터를 붙입니다.

컴파일러의 @Override, 테스트 도구의 표식, 프레임워크 설정처럼 다른 도구가 선언을 해석하게 합니다.

애너테이션을 붙였다고 동작이 저절로 생기는 것은 아니며 읽고 처리하는 컴파일러, 런타임 코드, 빌드 도구가 필요합니다.

대상과 유지 기간을 넓게 잡으면 의도하지 않은 위치에서 사용될 수 있습니다.

이 절에서는 메타데이터 선언과 소비를 한 쌍으로 봅니다.

애너테이션은 호출을 가로채거나 메서드를 실행하는 기능 자체가 아닙니다. 클래스 파일에 남은 표식을 누군가 읽고 행동할 뿐입니다. 따라서 애너테이션을 설계할 때는 문법보다 먼저 소비자가 컴파일러인지, 빌드 도구인지, 실행 중 프레임워크인지 정해야 합니다.


애너테이션 요소는 제한된 값 타입을 사용한다

기본 타입, String, Class, enum, 다른 annotation과 그 배열 등을 요소로 선언할 수 있습니다.

null을 기본값으로 사용할 수 없습니다.


@Target이 붙을 수 있는 선언을 제한한다

TYPE, METHOD, FIELD, PARAMETER, RECORD_COMPONENT 등 실제 소비자가 조사하는 위치만 허용합니다.

RuntimeAnnotations.java는 지금 설명한 규칙을 실제 값의 변화로 보여 줍니다.

RuntimeAnnotations.java
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

public class RuntimeAnnotations {
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    @interface Audited { String action(); }

    static final class Service {
        @Audited(action = "save")
        public void save() { }
    }

    public static void main(String[] args) throws Exception {
        var method = Service.class.getMethod("save");
        Audited audited = method.getAnnotation(Audited.class);
        System.out.println(audited.action());
        method.invoke(new Service());
    }
}

save가 출력되는 이유는 @Audited가 RUNTIME까지 남고, 처리 코드가 정확히 Service.save의 메타데이터를 조회했기 때문입니다. method.invoke는 애너테이션 때문이 아니라 예제가 별도로 호출했기 때문에 실행됩니다.

save

RUNTIME 유지와 METHOD 대상이므로 실행 중 save 메서드에서 Audited 값을 읽을 수 있습니다.

선언에 애너테이션 부착

기본 타입, String, Class, enum, 다른 annotation과 그 배열 등을 요소로 선언할 수 있습니다.

Retention에 따라 클래스 파일·런타임까지 유지

TYPE, METHOD, FIELD, PARAMETER, RECORD_COMPONENT 등 실제 소비자가 조사하는 위치만 허용합니다.

처리 코드가 메타데이터를 읽어 의미 부여

SOURCE는 컴파일 뒤 사라지고 CLASS는 클래스 파일에 남으며 RUNTIME은 리플렉션으로 읽을 수 있습니다.


@Retention이 남아 있는 시점을 정한다

SOURCE는 컴파일 뒤 사라지고 CLASS는 클래스 파일에 남으며 RUNTIME은 리플렉션으로 읽을 수 있습니다.

필요 이상으로 런타임 유지하지 않습니다.

아래 선언을 비교하면 사용 위치와 보존 시점이 서로 독립된 축이라는 점을 알 수 있습니다.

비교 1
@Target({ElementType.TYPE, ElementType.RECORD_COMPONENT})
@Retention(RetentionPolicy.CLASS)
@interface GeneratedModel { }
비교 2
// @GeneratedModel void method() { }
// 컴파일 오류: Target에 METHOD가 없음
비교 3
// SOURCE는 소스 처리까지만, RUNTIME은 리플렉션 조회까지 유지

처리 코드가 실제 의미를 부여한다

리플렉션이나 annotation processor가 값을 읽고 검증하거나 코드를 생성합니다.

처리기가 없다면 메타데이터일 뿐 실행 흐름을 바꾸지 않습니다.


대상과 유지 기간을 좁힌다

@Target은 붙일 수 있는 선언을, @Retention은 소스·클래스·런타임 가운데 언제까지 남길지 정합니다.

리플렉션으로 읽으려면 RUNTIME이 필요합니다.

@Audited의 유지 정책을 CLASS로 바꿔 다시 실행하면 getAnnotation은 null을 반환합니다. 애너테이션 자체는 클래스 파일에 남아도 런타임 리플렉션의 입력은 아니라는 뜻입니다. 반대로 컴파일 시 코드 생성만 필요하다면 RUNTIME을 고집하지 않아도 됩니다.

프레임워크용 애너테이션에는 기본값을 많이 두기보다 필수 의미를 요소로 드러내세요. 기본값이 실제 정책과 어긋나면 선언은 짧아져도 소비자가 잘못 해석한 설정을 찾기 어려워집니다.


애너테이션은 선언을 줄이는 마법이 아니라 누가 언제 어떤 위치의 메타데이터를 읽는지 정한 프로토콜입니다.