스프링에서 조건에 따라 활성화될 Bean이나 다른 설정값들을 조정하는 니즈는 다들 많이 있으리라 생각됩니다. 사내 프로젝트 중에서도 여러 서비스들을 공통적으로 활성화시킬 Bean들이 꽤 많았습니다. 그럴 때 어떻게 중복을 줄이고 리팩토링을 할 수 있을까요? 제가 꺼낸 답은 어노테이션이었습니다.

예제

예제로, APM(Application Performance Logging) 기능 중 하나로, 특정 메소드의 실행 시간을 log로 남기고 싶다고 가정해봅시다.

private val repository: ItemRepository

@LogExecutionTime
fun loadItems(criteria: ItemFetchCriteria): List<Item> {
  //...
}

@LogExecutionTime 어노테이션으로 표시된 메소드, 즉 loadItems 메소드의 실행 시간을 표준 출력으로 표시해보려면 Spring AOP로 간단히 구현될 수 있습니다.

@Aspect
class LogExeucutionTimeApsect {
    @Around("@annotation(LogExecutionTime)")
    fun logExecutionTime(pjp: ProceedingJoinPoint): Any? {
        val signature = pjp.signature
        val typeName = signature.declaringType.simpleName
        val methodName = signature.name
        val start = System.currentTimeMillis()
        try {
            return pjp.proceed()
        } finally {
            val end = System.currentTimeMillis()
            println("%s.%s execution time : %dms".format(typeName, methodName, end - start))
        }
    }
}

아, 물론 Configuration에 활성화 어노테이션도 달아줘야 합니다.

@Configuration
@EnableAspectJAutoProxy // AOP is enabled
class YourAppConfiguration {
    @Bean
    fun performanceLoggingAspect() = PerformanceLoggingAspect()
}

이제 잘 작동하겠네요. 간단한 예제였지만, 관련 프로젝트에 이 코드 블록을 복붙 하는 것은 개발자로써 옳지 않아보입니다. (println 대신 slf4j와 같은 logging framework을 사용하고, fluentdlogstash, filebeat 대신 직접 API를 호출한다고 할 때, Retrofit이나 Feign 등의 설정을 더 추가하면 코드는 더 많아지겠네요.)

이 녀석들을 wrapping해서 아래와 같은 단일한 어노테이션으로 도출하고, 이를 사용하는 프로젝트에서는 dependency만 참조하는 식으로 리팩토링하고자 합니다.

@SpringBootApplication
@EnablePerformanceLogging
class YourApp

@Import 어노테이션 활용법

@Import 어노테이션은 스프링 빈 설정을 자유롭게 할 수 있는데 도움을 줍니다. 일반적으로 @Configuration 클래스들을 컨텍스트로 로드하지만, 경우에 따라서 컴포넌트 자체를 로드하거나, ImportSelector로 조건에 따라 서로 다른 Configuration을 로드할 수도 있습니다.

@Import 어노테이션을 이용해서 위에서 기술된 YourAppConfiguration을 활성화시켜보고자 합니다. 다음과 같이 설계해봅니다.

  1. Main-ClassYourApp에 어노테이트된 @EnablePerformanceLogging을 로드합니다.
  2. 해당 어노테이션에 @Import로 기술된 Configuration을 로드합니다.
  3. Configuration에 기술된 @EnableAspectJAutoProxy로 AOP를 활성화하고, Aspect를 Bean으로 로드합니다.

즉, @EnablePerformanceLogging 어노테이션과 관련된 Configuration 클래스는 아래와 같이 간단히 구현될 수 있습니다.

@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
@Import(PerformanceLoggingConfiguration::class)
annotation class EnablePerformanceLogging

@EnableAspectJAutoProxy
class PerformanceLoggingConfiguration {
    @Bean
    fun aspect() = PerformanceLoggingAspect()
}

이제 남은 일은 관련 클래스들을 하나의 java library로 package하는 일만 남았습니다.

예제 코드는 여기를 참조하세요. 실행 예제는 아래와 같습니다.