스프링 커스텀 어노테이션
스프링에서 조건에 따라 활성화될 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을 사용하고, fluentd나 logstash, filebeat 대신 직접 API를 호출한다고 할 때, Retrofit이나 Feign 등의 설정을 더 추가하면 코드는 더 많아지겠네요.)
이 녀석들을 wrapping해서 아래와 같은 단일한 어노테이션으로 도출하고, 이를 사용하는 프로젝트에서는 dependency만 참조하는 식으로 리팩토링하고자 합니다.
@SpringBootApplication
@EnablePerformanceLogging
class YourApp
@Import
어노테이션 활용법
@Import
어노테이션은 스프링 빈 설정을 자유롭게 할 수 있는데 도움을 줍니다. 일반적으로 @Configuration
클래스들을 컨텍스트로 로드하지만, 경우에 따라서 컴포넌트 자체를 로드하거나, ImportSelector
로 조건에 따라 서로 다른 Configuration
을 로드할 수도 있습니다.
@Import
어노테이션을 이용해서 위에서 기술된 YourAppConfiguration
을 활성화시켜보고자 합니다. 다음과 같이 설계해봅니다.
Main-Class
인YourApp
에 어노테이트된@EnablePerformanceLogging
을 로드합니다.- 해당 어노테이션에
@Import
로 기술된 Configuration을 로드합니다. - 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하는 일만 남았습니다.
예제 코드는 여기를 참조하세요. 실행 예제는 아래와 같습니다.