명명 패턴의 단점은 다음과 같다.
애너테이션은 이 모든 문제를 해결해 주는 멋진 개념이다.
그렇기에 이번 글에서는 애너테이션의 동작 방식을 알아보자.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
}
@Retention과 @Target은 메타애너테이션이다.
메타애너테이션이란 애너테이션 선언에 다는 애너테이션을 말한다.
위와 같은 애너테이션은 "아무 매개변수 없이 단순히 대상에 마킹"하였다는 뜻에서 마커 애너테이션이라 한다.
이러한 애너테이션을 사용하면 효과적으로 프로그래머에게 추가 정보를 제공할 수 있을 뿐 아니라 대상 코드의 의미는 그대로 둔 채 그 애너테이션에 관심 있는 도구에서 특별한 처리를 할 기회를 준다.
코드를 통해서 알아보자.
마커 애너테이션
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface HandleMethod {
}
대상 클래스
public class HandledClass {
@HandleMethod
public static void test1() {
System.out.println("test1 success");
}
@HandleMethod
public static void test2() {
throw new RuntimeException("Boom test2 failed");
}
}
실행 코드
Class<?> className = Class.forName(HandledClass.class.getName());
for(Method m : className.getDeclaredMethods()) {
if(m.isAnnotationPresent(HandleMethod.class)) {
try {
m.invoke(null); // static method 실행
} catch (InvocationTargetException wrappedExc) {
Throwable exc = wrappedExc.getCause();
System.out.println(m.getName() + " failed: " + exc);
} catch (Exception exc) {
System.out.println("Invalid @Test: " + m.getName());
}
}
}
System.out.println("Done");
실행 코드에서 isAnnotationPresent가 마킹한 메서드를 찾아준다.
그리고 마킹한 메서드가 예외를 던진다면 리플렉션 메커니즘이 InvoationTargetException으로 감싸서 다시 던진다.
InvoationTargetException이외의 예외가 발생한다면 그것은 애너테이션을 잘못 사용했다는 뜻이다.
아마 인스턴스 메서드, 매개변수가 있는 메서드, 호출할 수 없는 메서드 등에 어노테이션을 다는 경우 등 다양한 경우가 있을 수 있다.
그리고 애너테이션은 매개변수 역시 받을 수 있다.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface HandleMethod {
int value() default 0;
}
여기서 매개변수로는 여러 값을 받을 수 있는데 그 방법은 2가지가 있다.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface HandleMethod {
int[] value() default {0};
}
우선 첫번째 방법은 위와 같이 배열로 선언하는 것이다.
위와 같이 선언한 애너테이션은 아래와 같이 사용할 수 있다.
@HandleMethod({1,2})
하지만 자바 8에서는 여러 개의 값을 받는 애너테이션을 다른 방식으로도 만들 수 있다.
배열 매개변수를 사용하는 대신 애너테이션에 @Repeatable 메타애너테이션을 다는 방식이다.
@Repeatable을 단 애너테이션은 하나의 프로그램 요소에 여러 번 달 수 있다.
단, 주의할 점이 있다.
첫 번째, @Repeatable을 단 애너테이션을 반환하는 '컨테이너 애너테이션'을 하나 더 정의하고, @Repeatable에 이 컨테이너 애너테이션의 class 객체를 매개변수로 전달 해야 한다.
두 번째, 컨테이너 애너테이션은 내부 애너테이션 타입의 배열을 반환하는 value 메서드를 정의해야 한다.
마지막으로 컨테이너 애너테이션 타입에는 적절한 보존 정책과 적용 대상을 명시해야 한다.
그렇지 않으면 컴파일되지 않는다.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Repeatable(HandleMethodValueContainer.class) // 컨테이너 애너테이션 설정
public @interface HandleMethod {
int value();
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface HandleMethodValueContainer {
HandleMethod[] value(); //컨테이너를 적용할 애너테이션 타입 배열
}
위와 같이 선언한 애너테이션은 아래와 같이 사용할 수 있다.
@HandleMethod(1)
@HandleMethod(2)
이전의 방식과 사용 방법이 조금 달라졌다.
그리고 이를 처리하는 방법 역시 조금 달라진다.
반복 가능 애너테이션을 여러 개 달면 하나만 달았을 때와 구분하기 위해 해당 '컨테이너' 애너테이션이 적용된다.
이때 getAnnotationByType 메서드는 이 둘을 구분하지 않지만 isAnnoatationPresent 메서드는 이를 명확히 구분한다.
책에서는 반복 가능 애너테이션을 사용해 하나의 프로그램 요소에 같은 애너테이션을 여러 번 달 때의 코드 가독성을 높여 보였다고 한다.
하지만 본인은 아직은 그렇게 생각하지 않는다.
이는 앞으로 개발을 진행하면서 하나씩 느껴 나가야겠다는 생각이 든다.