WordPress

Spring 5.2 그리고 Spring boot 2.2

Posted by wonwoo on 2019-04-21

몇일전에 Spring 5.2.M1버전 과 Spring boot2.2.M2 버전이 릴리즈 되었다. 아직 해당 프로젝트들의 GA 버전이 나오기엔 시간이 많이 남아있지만 (대략 7월) 많이 변경되지 않을 것들만 모아서 살펴보도록 하자. (사실 필자가 아는 부분만이겠지만)

일단 Spring Framework 5.2 부터 알아보자.

Spring Framework 5.2

@Configuration

@Configuration 어노테이션의 속성이 추가 되었다. proxyBeanMethods 라는 속성이며 예전에 필자가 포스팅한 글중 Lite Mode라는 주제가 있었던 그 내용이 Spring 쪽에 들어갔다.
사용법은 아주 간단하다.

@Configuration(proxyBeanMethods = false)
public class TestConfiguration  {
  //... 
}

위와 같이 lite mode를 적용하고 싶으면 proxyBeanMethods 속성을 false로 설정하면 된다. 기본값은 true이다. 그럼 cglib을 사용하지 않는 상태에서 설정이 진행된다.
해당 설명은 위의 포스팅이 있기에 생략.

참고로 Spring boot 2.2의 AutoConfiguration들은 모두 @Configuration(proxyBeanMethods = false) 로 변경하였다.
또한 @SpringBootApplication 어노테이션에도 proxyBeanMethods 속성이 추가 되었다.

vavr의 Try와 @Transactional

io.vavr(구 javaslang) 의 Try를 @Transactional 어노테이션을 지원한다.

@Transactional
public Try<?> hello() {
    return Try.of(() -> accountRepository.save(new Account(1L, "test")))
        .filter(Account::isActive)
        .andThenTry((account) -> accountRepository.save(new Account(account.getId()))); // 예외
}

해당 라인에서 예외가 발생하면 롤백이 진행된다. 필자가 vavr를 잘 사용하지 못하는 관계로..
만약 vavr 프로젝트를 잘 사용한다면 유용하게 쓰일듯 싶다. Spring data 쪽에서는 vavr 프로젝트도 지원해주니 함께 사용하면 더 많은 지원을 받을 수 있을 듯 싶다.

import io.vavr.control.Try;
import org.springframework.data.jpa.repository.JpaRepository;

public interface AccountRepository extends JpaRepository<Account, Long> {

    Try<Account> findByName(String name);

}

MergedAnnotations

Annotation을 처리하기 위해 새로운 API가 등장했다. 기존의 있던 AnnotationUtilsAnnotatedElementUtils 대신에 사용할 수 있는 API이다.
위의 AnnotationUtils, AnnotatedElementUtils API들도 내부적으로 MergedAnnotations 으로 변경하였다.
조금 더 살펴보고 해야겠지만 일단은 기본사용법만 알아두자. 언제 바뀔지 모르니..

MergedAnnotation<SpringBootApplication> springBootApplication = MergedAnnotations.from(Application.class)
    .get(SpringBootApplication.class);
Class<?>[] scanBasePackageClasses = springBootApplication.getClassArray("scanBasePackageClasses");
String[] scanBasePackages = springBootApplication.getStringArray("scanBasePackages");
boolean proxyBeanMethods = springBootApplication.getBoolean("proxyBeanMethods");

위와 같이 쉽게 사용할 수 있다. 더 많은 API 들과 속성들이 있긴 하지만 지금 당장은 살펴볼 필요가 없어서 이정도만 알아봤다. 릴리즈 되면 그때 다시 알아보도록 하자.
근데 사용할 일이 많이 없어서..

Spring boot 2.2

지금 부터는 Spring boot 쪽의 변화이다.

lazyInitialization

Spring boot 2.2 부터는 lazyInitialization를 모든 빈의 적용할 수 있게 되었다. 기존에는 @Lazy 어노테이션을 선언하여 게으른 빈은 설정하였다.

@Bean
@Lazy
public AccountService accountService() {
    return new AccountService();
}

위와 같이 설정하면 실제 사용될 시점에 빈이 생성된다.
조금 빠른 로딩을 하기 위해서 모든 빈에 게으르게 빈을 설정 할 수 있게 되었다.

spring.main.lazy-initialization=true

application.properties에 위와 같이 spring.main.lazy-initialization 속성을 true로 주면 모든 빈들이 Lazy 빈으로 생성 된다.
Spring boot 로딩 시간이 길다면 해당 속성을 이용해서 빠르게 로딩할 수 있다.

하지만 여기서 주의할점은 web 기준으로 http 첫 요청이 조금 길다는점. 첫 요청시 사용하는 빈들이 그 순간 만들어져 요청이 조금 느릴 수 있다.
두 번쨰로는 일반적으로는 빈을 찾을 수 없을 때 에러가 나지만 lazyInitialization을 설정하면 서버가 기동될때 에러를 찾을 수 없다. 빈을 사용할 때 그 때 빈을 찾으므로 런타임시 에러가 발생할 수 있으므로 조심해야 된다.

개발할 때만 설정하고 운영할 때는 설정 하지 않는것이 좋아 보인다.

만약 위의 설정을 해놓고 특정빈은 Lazy 로딩을 하고 싶지 않다면 다음과 같이 설정하면 된다.

@Bean
@Lazy(false)
public AccountService accountService() {
    return new AccountService();
}

이 기술(@Lazy)은 오래된 기술이다. 실제로는 대략 11년전에 개발된 기술이다. 그래서 우리도 간단한 구현만으로도 위와 비슷한 모든 빈에 Lazy 초기화를 할 수 있다.

@Bean
static BeanFactoryPostProcessor beanPostProcessor() {
    return beanFactory -> {
        String[] beanDefinitionNames = beanFactory.getBeanDefinitionNames();
        for(String beanName : beanDefinitionNames) {
            beanFactory.getBeanDefinition(beanName).setLazyInit(true);
        }
    };
}

Spring boot 2.2를 사용하지 않더라도 그 이하버전에서도 위와 같이 구현하면 게으른 초기화를 진행 할 수 있다.

@ConfigurationPropertiesScan

@ConfigurationPropertiesScan 어노테이션이 추가 되었다. 애노테이션명 그대로 ConfigurationProperties를 스캔하는 용도이다.

@ConfigurationProperties("foo")
public class FooProperties {

    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

위와 같은 Properties가 존재한다면 우리는 아래와 같이 설정하였다.

@Configuration
@EnableConfigurationProperties(FooProperties.class)
public class Application {

}

이제는 EnableConfigurationProperties을 사용하지 않고 @ConfigurationPropertiesScan 어노테이션을 사용하여 scan 할 수 있다.

@Configuration
@ConfigurationPropertiesScan("ml.wonwoo")
public class Application {

}

위와 같이 적용하면 ml.wonwoo 패키지 아래의 모든 프로퍼티들이 자동으로 등록이 된다.
Spring boot 2.2에서는 @SpringBootApplication 애노테이션에 메타 애노테이션으로 @ConfigurationPropertiesScan 애노테이션이 추가 되었으므로 추가적인 설정은 할필요 없다.

scan할 패지키를 지정하지 않는다면 선언된 애노테이션 이하의 패지키를 검색한다.

이제 @EnableConfigurationProperties의 사용처는 자동설정을 커스텀하게 만들때 사용하면 된다.

Immutable @ConfigurationProperties

불변의 Properties 를 지원한다. 기존에는 기본생성자와 setter가 존재 했어야 했지만 이제는 그럴 필요 없다.

@ConfigurationProperties("foo")
public class FooProperties {

    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

기존에는 위와 같이 javaBeans 스펙에 따라야 했지만 이제는 setter 또한 필요 없고 기본생성자 또한 필요 없다.

@ConfigurationProperties("foo")
public class FooProperties {

    private final String name;

    public FooProperties(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

}

만약 해당 프로퍼티에 디폴트값을 원한다면 @DefaultValue 어노테이션을 이용해서 기본값을 설정 할 수 있다.

@ConfigurationProperties("foo")
public class FooProperties {

    private final String name;

    public FooProperties(@DefaultValue("wonwoo") String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

이렇게 하면 프로퍼티의 값이 존재 하지 않아도 기본값으로 wonwoo가 설정이 된다.

오늘은 이렇게 Spring과 Spring boot에 변화에 대해서 알아봤다. 이 보다 많은 변화가 있었지만 개발자들이 직접적으로 사용하기에는 위의 내용으로도 충분해 보여 몇가지만 준비했다.
더 많은 내용은 해당 문서를 통해 알아보도록 하고, 위의 내용 또한 언제 바뀔지 모르니 현재는 그냥 참고만 하면 되겠다.
나중에 릴리즈 될때 다시 보자!

Java ThreadPoolExecutor

Posted by wonwoo on 2019-03-12

오늘은 java의 ThreadPoolExecutor에 대해서 알아보도록 하자. 많은 내용은 아니지만 기본적으로 사용하려면 알아두어야 할 것들만 알아보자.

ThreadPoolExecutor은 클래스명 그대로 스레드풀을 편하게 관리해주는 클래스이다. ThreadPoolExecutor의 최상위 인터페이스는 Executor 이며 그에 따른 구현체들은 아주 많으니 문서를 살펴보는 것이 좋을 것 같다. 그 중 가장 많이 사용될 만한 클래스가 ThreadPoolExecutor라 오늘은 해당 클래스에 대해 살펴보고 나중에 기회가 된다면.. ForkJoinPool에 대해서도 알아보도록 하자.

ThreadPoolExecutor

ThreadPoolExecutor은 4개의 생성자가 있다. 비슷비슷해서 눈이 아프다. 일단 아래의 코드는 ThreadPoolExecutor의 생성자이다.

ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue)
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler)
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory)
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)

공통적으로 corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue 가 존재하면 실제로 이 다섯가지의 파라미터가 가장 중요한 역할을 한다. 나머지가 중요하지 않다는걸 의미한건 아니다.

일단 하나씩 살펴보자.
- corePoolSize : 풀 사이즈를 의미한다. 최초 생성되는 스레드 사이즈이며 해당 사이즈로 스레드가 유지된다. 해당 Job의 맞게 적절히 선택해야 한다. 많다고 성능이 잘나오는 것도 아니고 적다고 안나오는 것도 아니다. 충분히 테스트하면서 적절한 개수를 선택해야 한다.
- maximumPoolSize : 해당 풀에 최대로 유지할 수 있는 개수를 의미한다. 이 역시 Job에 맞게 적절히 선택해야 한다.
- keepAliveTime : corePoolSize보다 스레드가 많아졌을 경우 maximumPoolSize까지 스레드가 생성이 되는데 keepAliveTime 시간만큼 유지했다가 다시 corePoolSize 로 유지되는 시간을 의미한다. (그렇다고 무조건 maximumPoolSize까지 생성되는 건 아니다.)
- unit : keepAliveTime 의 시간 단위를 의미한다.
- workQueue : corePoolSize보다 스레드가 많아졌을 경우, 남는 스레드가 없을 경우 해당 큐에 담는다.

해당 파라미터가 어떤 역할을 하는지 알아봤다.

여기서 조금 더 알아볼게 있는데 바로 maximumPoolSizeworkQueue 파라미터이다. 필자가 위에서 그렇다고 무조건 maximumPoolSize까지 생성되는 건 아니다.를 주목하자.

실제로 corePoolSize가 스레드 개수보다 많다고 해서 maximumPoolSize 개수 까지 바로 생성하지 않는다. 그 전에 큐(workQueue)에 담고 대기한다. (구현체마다 다르지만, 일반적으로) 그리고 나서 workQueue에도 담을 공간이 부족하다면 그때 maximumPoolSize 까지 스레드를 늘려 작업을 한다. 그 후 keepAliveTime에 도달하면 다시 corePoolSize 로 유지 된다.

여기서 잠시 코드를 보자.

public static void main(String[] args) throws Exception {
    LinkedBlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(9);
    ThreadPoolExecutor executorService = new ThreadPoolExecutor(1, 3 ,3, SECONDS, queue);
    for (int i = 0; i < 10; i++) {
        executorService.execute(new Task());
    }
    executorService.awaitTermination(5, SECONDS);
    executorService.shutdown();
}

private static class Task implements Runnable {
    @Override
    public void run() {
        try {
            System.out.println(Thread.currentThread().getName());
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
        }
    }
}

위와 같은 코드가 있다고 가정해보자. corePoolSize는 1, maximumPoolSize는 3, 그리고 9개의 큐가 있다고 가정해보자. 그리고 나서 10개의 스레드를 만들어 돌려보면 어떻게 될까?

다시 해당 옵션에 대해 보면 1개의 core size와 9개의 큐가 있으니 1개의 잡이 실행되며 9개는 큐에 대기 상태가 된다. 그래서 1초에 하나씩 Thread Name이 출력 된다.

이번엔 큐 사이즈를 8개로 줄여보자.

LinkedBlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(8);

그렇다면 어떻게 실행될까? corePoolSize 는 1개와 8개의 큐가 있어 maximumPoolSize 동작한다. 1개 잡과 8개가 대기하므로 1개의 스레드가 추가되어 1초에 2개씩 Thread Name이 출력 된다.

이벤엔 6개로 줄여보자.

LinkedBlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(6);

이번엔 1개의 잡이 실행되고 큐에 6개가 쌓여 대기하여 maximumPoolSize 만큼 스레드가 증가하나 1개의 스레드가 갈곳이 없어 에러가 발생한다.

Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task Example$Task@50040f0c rejected from java.util.concurrent.ThreadPoolExecutor@2dda6444[Running, pool size = 3, active threads = 3, queued tasks = 6, completed tasks = 0]
	at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2063)
	at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:830)
	at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1379)

이왕 한김에 keepAliveTime도 테스트를 해보자. 해당 테스트는 대략적인 테스트이므로 정확하지 않을 수 있다.

public static void main(String[] args) throws Exception {
    LinkedBlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(7);
    ThreadPoolExecutor executorService = new ThreadPoolExecutor(1, 3 ,3, SECONDS, queue);
    for (int i = 0; i < 10; i++) {
        executorService.execute(new Task());
    }
    SECONDS.sleep(7);
    for (int i = 0; i < 5; i++) {
        executorService.execute(new Task());
    }
    executorService.awaitTermination(5, SECONDS);
    executorService.shutdown();
}

keepAliveTime 를 3초로 주고 테스틀을 해보자. 처음에는 10개 스레드를 3개의 코어 돌리니 대략 4초가 걸리고 중간에 7초 정도 대기를 타고 있으니 3초가 조금 넘는 시간이 될 것 같다.
위의 코드를 테스트를 해보면 두번째 execute 에서는 다시 1초에 한개의 Thread Name이 출력 되는 걸 볼 수 있다.

SECONDS.sleep(6)로 주면 3초 이전에 다시 execute를 하기에 maximumPoolSize로 스레드가 실행되는 걸 알 수 있다.

이정도만 알아도 충분히 ThreadPoolExecutor을 사용할 수 있을 것 같다.

몇가지 참고

Executors

Executors.newSingleThreadExecutor()
Executors.newFixedThreadPool()
Executors.newCachedThreadPool()
Executors.newWorkStealingPool()

해당 factory 메서드로 Executor등을 만들 수 있다. 해당 건은 다른 블로그를 참고했으면 좋겠다. 필자는 없다……..

Spring

Spring을 사용한다면 ThreadPoolTaskExecutor를 살펴보는 것도 좋다. 내부 구현은 ThreadPoolExecutor로 구현되어 있다. ThreadPoolExecutor 보다 조금 더 간편하며, 추가적인 return 타입도 있다.

@Bean
public Executor threadPoolTaskExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setCorePoolSize(10);
    executor.setQueueCapacity(100);
    executor.setMaxPoolSize(30);
    executor.set...
    return executor;
}

LinkedBlockingQueue vs SynchronousQueue

LinkedBlockingQueueSynchronousQueue는 BlockingQueue의 구현체들이다. 다른점이 있다면 SynchronousQueue 버퍼공간이 존재 하지 않는다. 그래서 스레드가 넘칠 경우 에러가 발생한다.
만약 대기 큐를 쓰고 싶다면 LinkedBlockingQueue 구현체를 사용해야 하며 동적으로 스레드를 만들고 싶다면 SynchronousQueue 를 이용해야 한다.

Spring 의 ThreadPoolTaskExecutorqueueCapacity 0 보다 크다면 LinkedBlockingQueue로 그렇지 않다면 SynchronousQueue으로 BlockingQueue 의 구현체를 설정한다.

protected BlockingQueue<Runnable> createQueue(int queueCapacity) {
    if (queueCapacity > 0) {
        return new LinkedBlockingQueue<>(queueCapacity);
    }
    else {
        return new SynchronousQueue<>();
    }
}

submit, execute

ThreadPoolExecutor에는 많은 메서드가 있지만 submit, execute 메서드의 차이는 return 이 되냐 되지 않는냐의 차이 이다. 실제 구현은 동일하다. (submit 은 execute를 호출한다)
또한 submit 에는 Runnable 파라미터 타입과 Callable 타입이 존재하는데 Callable 타입은 checked exception 을 throws 하고 Runnable는 그렇지 않다.

@FunctionalInterface
public interface Callable<V> {
    V call() throws Exception;
}

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

오늘 이렇게 Java ThreadPoolExecutor에 대해서 알아봤다. 많은 내용은 아니였지만 그래도 조금이나마 이해를 하며 사용하는 것이 좋아 작성해봤다.

그럼 오늘은 이만!!

Spring AnnotatedElementUtils (meta-annotation)

Posted by wonwoo on 2019-03-03

오늘은 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