오늘은 Spring에서 제공해주는 AnnotatedElementUtils (Meta-annoation)클래스에 대해서 알아보도록 하자.
아주 예전에 메타 어노테이션에 대해서 알아본적이 있는데 그 행위들을 AnnotatedElementUtils 이라는 클래스를 이용하여 구현되었다.

어노테이션 속성의 오버라이딩기능을 사용하고 싶다면 AnnotatedElementUtils클래스를 사용하면 되고, 그렇지 않다면 AnnotationUtils 클래스만을 이용하면 된다.

AnnotationUtils

간단한 예제를 보면서 살펴보자.

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Publish {

    String value();

    String address() default "";
}

위와 같이 @Publish라는 어노테이션이 있다고 가정하자. 속성으로는 valueaddress 라는 속성을 갖고 있다.

@Publish("redis")
public class SpringAnnotatedElementApplicationTests {

    @Test
    public void annotation() throws NoSuchMethodException {
        Publish publish = AnnotationUtils.findAnnotation(this.getClass(), Publish.class); 
        //  
    }
}

일반적으로는 AnnotationUtils 클래스를 사용하면 문제 없다. publish의 value 속성에는 redis라는 값이 들어가 있다. 아주 심플하다.
위와 같은 방법으로 어노테이션을 정의해도 되겠지만 좀 더 나은 방법으로는 메타 어노테이션으로 해당 속성을 미리 정해놓는 것이다.

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Publish(value = "redis")
public @interface RedisPublish {

}

위와 같이 미리 어노테이션을 미리 정해놓을 수 있다.

@RedisPublish
public class SpringAnnotatedElementApplicationTests {

    @Test
    public void annotation() throws NoSuchMethodException {
        Publish publish = AnnotationUtils.findAnnotation(this.getClass(), Publish.class);
        //
    }
}

위와 같이 작성해도 아까 봤던 @Publish("redis")와 동일한 값을 얻을 수 있다. 물론 address 도 추가하여 넣을 수 있다.

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Publish(value = "redis", address = "127.0.0.1")
public @interface RedisPublish {

}

AnnotationUtils의 기능은 여기까지이다. 만약에 어노테이션의 속성들을 오버라이딩 해야 한다면 AnnotationUtils 클래스가 아닌 AnnotatedElementUtils 클래스를 사용해서 속성들을 오버라이딩 하면 된다.

AnnotatedElementUtils

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Publish(value = "redis")
public @interface RedisPublish {

    String address() default "";
}

다시 이런 @RedisPublish 어노테이션이 있다고 가정하자. address 속성이 RedisPublish 에 추가 되었다.

@RedisPublish(address = "localhost")
public class SpringAnnotatedElementApplicationTests {

    @Test
    public void annotation() throws NoSuchMethodException {

        Publish publish = AnnotatedElementUtils.findMergedAnnotation(this.getClass(), Publish.class);
        Publish publish1 = AnnotationUtils.findAnnotation(this.getClass(), Publish.class);
    }
}

만약 위와 같이 해당 속성을 가져온다면 어떻게 될까?
AnnotationUtils.findAnnotation을 사용할 경우에는 해당 value만 redis라는 값이 들어가 있고 address에는 아무 값이 들어가 있지 않다. 하지만 AnnotatedElementUtils.findMergedAnnotation이 경우에는 value와 address 모두 값이 들어가 있는 것을 확인 할 수 있다. 아주 유용하게 사용할 수 있을 것 같다.

또한 만약에 address라는 속성이 마음에 들지 않는다면 속성 자체도 변경하여 오버라이딩 할 수 있다.

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Publish(value = "redis")
public @interface RedisPublish {

    @AliasFor(annotation = Publish.class, attribute = "address")
    String host() default "";
}

@AliasFor 어노테이션으로 타겟 어노테이션과 속성을 정의해주면 해당 속성으로 오버라이딩 된다. 여기서는 host 에 값을 넣었지만 실제로는 Publish.address에 값이 들어 가게 된다.

AnnotatedElementUtils 클래스가 의미하는 바는 알았으니 몇가지 메서드만 알아보도록 하자.

AnnotatedElementUtils.method()

AnnotatedElementUtils.getMetaAnnotationTypes

AnnotatedElementUtils.getMetaAnnotationTypes 은 메타 어노테이션의 클래스를 Set으로 가져온다.

AnnotatedElementUtils.getMetaAnnotationTypes(this.getClass(), RedisPublish.class); // ["xxx.xxx.xxxx.Publish"]

주의할 점은 선언된 어노테이션의 메타 어노테이션만 가져온다. RedisPublishPublish만을 갖고 있기때문에 Publish만이 리턴된다.

AnnotatedElementUtils.hasMetaAnnotationTypes

해당 어노테이션이 메타 어노테이션으로 있는지 없는지 boolean 값을 리턴한다. 여기도 주의할 점은 메타 어노테이션인지만을 체크한다.

AnnotatedElementUtils.hasMetaAnnotationTypes(this.getClass(), RedisPublish.class); //false
AnnotatedElementUtils.hasMetaAnnotationTypes(this.getClass(), Publish.class); //true

AnnotatedElementUtils.isAnnotated

isAnnotated 은 자기 자신을 포함한 어노테이션이 있는지 체크한다.

AnnotatedElementUtils.isAnnotated(this.getClass(), RedisPublish.class); //true
AnnotatedElementUtils.isAnnotated(this.getClass(), Publish.class);  //true

AnnotatedElementUtils.getMergedAnnotationAttributes, findMergedAnnotationAttributes

두 메서드는 AnnotationAttributes 클래스로 리턴하는데 해당 속성과 값을 Map으로 저장하고 있다. 실제로 findMergedAnnotation를 호출하면 findMergedAnnotationAttributes 메서드를 사용한다.

classValuesAsString, nestedAnnotationsAsMap 두개의 파라미터가 더 있는데 classValuesAsString는 어노테이션에 클래스 속성이 있다면 이것을 class로 담을지 string으로 담을지 결정하는 것이고 nestedAnnotationsAsMap 파라미터는 속성중 어노테이션이 있다면 어노테이션 그대로 사용할지 아니면 AnnotationAttributes 클래스로 변환할지 결정하는 파라미터이다. (true이면 변환 아니면 그대로)

AnnotatedElementUtils.getMergedAnnotationAttributes(this.getClass(), Publish.class.getName(), false, false)
AnnotatedElementUtils.findMergedAnnotationAttributes(this.getClass(), Publish.class, false, false)

AnnotatedElementUtils.getAllMergedAnnotations, findAllMergedAnnotations

만약 메타 어노테이션이 여러개 선언되어있다면 해당 메서드를 사용하면 된다.

@RedisPublish(host = "localhost")
@KafkaPublish
public class SpringAnnotatedElementApplicationTests {
}

AnnotatedElementUtils.getAllMergedAnnotations(this.getClass(), Publish.class);
AnnotatedElementUtils.findAllMergedAnnotations(this.getClass(), Publish.class);

각각 2개의 어노테이션들이 리턴된다.

AnnotatedElementUtils.getMergedRepeatableAnnotations, findMergedRepeatableAnnotations

@Repeatable이 선언되었다면 선언된 어노테이션의 의 정보를 가져올 수 있다.

@Publish("foo")
@Publish("bar")
@Publish("name")
public class SpringAnnotatedElementApplicationTests {
   ///....
}

AnnotatedElementUtils.getAllMergedAnnotations(this.getClass(), Publish.class)
AnnotatedElementUtils.findAllMergedAnnotations(this.getClass(), Publish.class)

이 외도 많은 메서드가 있지만 이 정도만 알아봐도 문제 없을 듯하다.
하지만 여기에서 조금 궁금한점이 있다. 같은 메서드같은데 getxxx, findxxx 메서드가 종종 보인다. 하는 역할은 같아보이는데 무슨 차이가 있을까?
차이가 있으니 저렇게 나눠놨을 것이라고 판단되어서 찾아봤다.

getxxx 와 findxxx의 차이는 다음과 같다.

  • 클래스가 인터페이스를 상속받고 인터페이스의 해당 어노테이션이 있는 경우
@RedisPublish
interface Foo {

}

class FooClazz implements Foo {

}

AnnotatedElementUtils.findMergedAnnotation(FooClazz.class, Publish.class) // not null
AnnotatedElementUtils.getMergedAnnotation(FooClazz.class, Publish.class) // null
  • 클래스가 클래스를 상속받고 상위 클래스에 해당 어노테이션이 있는 경우
@RedisPublish
class Foo {

}

class FooClazz extends Foo {

}

AnnotatedElementUtils.findMergedAnnotation(FooClazz.class, Publish.class) // not null
AnnotatedElementUtils.getMergedAnnotation(FooClazz.class, Publish.class) // null
  • 해당 메서드가 브릿지 메서드일 경우

interface Comparable<T> { @RedisPublish int compareTo(T o); } class ComparableValues implements Comparable<String> { @Override public int compareTo(String o) { return 0; } } AnnotatedElementUtils.findMergedAnnotation(ComparableValues.class.getMethod("compareTo", Object.class), Publish.class); //not null AnnotatedElementUtils.getMergedAnnotation(ComparableValues.class.getMethod("compareTo", Object.class), Publish.class); //null
  • 클래스가 인터페이스를 상속받고 상위 메서드에 해당 어노테이션이 있는 경우
interface Foo {
    @RedisPublish
    void bar();
}


class FooClazz implements Foo {

    @Override
    public void bar() {

    }
}

AnnotatedElementUtils.findMergedAnnotation(FooClazz.class.getMethod("bar"), Publish.class); //not null
AnnotatedElementUtils.getMergedAnnotation(FooClazz.class.getMethod("bar"), Publish.class); // null
  • 클래스가 클래스를 상속받고 상위 메서드에 해당 어노테이션이 있는 경우
class Foo {
    @RedisPublish
    public void bar(){

    }
}


class FooClazz extends Foo {

    @Override
    public void bar() {

    }
}

AnnotatedElementUtils.findMergedAnnotation(FooClazz.class.getMethod("bar"), Publish.class); //not null
AnnotatedElementUtils.getMergedAnnotation(FooClazz.class.getMethod("bar"), Publish.class); // null

이하 default 메서드도 포함.

findxxxx가 좀 더 기능이 추가된 메서드라고 생각하면 되겠다. 상위 클래스, 인터페이스, 메서드들까지 검색하여 적절하게 리턴해준다.

오늘은 이렇게 util성의 클래스에 대해서 알아봤다. 어노테이션을 활용하여 개발한다면 해당 클래스를 참고하면 되겠다.

Spring은 @Inherited 어노테이션을 존중하므로 해당 어노테이션에 @Inherited 선언되어 있다면 getxxx 메서드 또한 적절하게 가공해서 제공해주고 있다.

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Publish(value = "redis")
@Inherited
public @interface RedisPublish {

    @AliasFor(annotation = Publish.class, attribute = "address")
    String host() default "";
}

@RedisPublish
class Foo {

    public void bar() {
    }
}

class FooClazz extends Foo {

    @Override
    public void bar() {

    }
}

AnnotatedElementUtils.getMergedAnnotation(FooClazz.class, Publish.class) //not null