Spring 프레임워크를 사용하면 대부분의 클래스들에 어노테이션을 선언하고는 한다
대표적으로 많이 사용하는 어노테이션이 Lombok, Spring Context 에 Bean 으로 등록하기 위한 어노테이션이 있다
@Getter, @Setter, @Service, @Component, @RestController 등...
개발을 하며 여러가지 어노테이션을 사용하고 있을 것이라고 생각한다
그리고 위 어노테이션을 선언함으로써 어떠한 동작을 할수있게 된다.
나는 문득 궁금해졌다.
내가 이 어노테이션이 어떻게 동작하는지 알고 쓰는걸까?
어노테이션을 내가 만들어서 사용할 수 있을까?
라는 생각을 하였다.
위 생각을 행동으로 옮기기 위해 생각이 났을 때 공부를 해보고 기록을 해보았다
1) 어노테이션 동작 원리 및 예시
어노테이션은 설정에 따라 컴파일 or 런타임 시에 적용될 수 있다
어노테이션은 '메타 데이터'로 프로그램의 동작에 직접적인 영향을 주지는 않지만,
코드에 대한 데이터를 제공하거나, 코드를 어떻게 사용할지에 대한 가이드를 제공할 수 있다
그리고 위 어노테이션을 처리하기 위해선 'Annotation Processor' 를 통해서 처리가 된다
- Java 컴파일시 컴파일러가 돌면서 어노테이션을 처리 하여 byte 코드로 바꿔준다.
위 Annotation Processor 'javax.annotation.processing' 에 'Processor' 라는 인터페이스로 정의되어 있다
자바에서는 대표적으로 @Override, @FunctionalInterface 등 기본 JDK 에서 내장 어노테이션을 제공한다
이러한 어노테이션들은 자바 컴파일러에게 특정 작업을 수행하도록 명령한다.
그리고 Java 는 어노테이션을 내가 만들어서 사용또한 가능하다
보통 코드의 가독성 높이거나, 명시적으로 무언갈 선언하기 위해서 Custom 하여 사용하고는 한다
만드는 방법은 간단한다.
클래스를 생성할 떄 Annotation 으로 생성하면 된다.
위 어노테이션을 생성하면 아래와 같이 interface 에 @ 가 붙은 것이 생성된다
public @interface ExampleAnnotation {
}
위 어노테이션에 또다른 어노테이션을 선언할 수 있고, 내부적으로 변수를 선언하여 어노테이션 자체에 어떠한 설정을 줄 수도있다
아래는 스프링에서 동작하는 대표적인 어노테이션 예시 이다
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Service {
/**
* Alias for {@link Component#value}.
*/
@AliasFor(annotation = Component.class)
String value() default "";
}
그러면 어노테이션에 설정한 내용은 내부적으로 어떻게 동작하며 적용이 되는걸까?
어노테이션은 Java Reflection 을 사용하여 일반적으로 런타임에 해석된다
이를 통해 동적으로 코드의 동작을 변경하거나 설정한 정보를 수행한다
런타임시 동작시키지 않고, 다른 동작을 원한다면 아래와 같은 설정 하면 된다
@Retention(RetentionPolicy.SOURCE)
@Retention(RetentionPolicy.CLASS)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
RetentionPolicy value();
}
public enum RetentionPolicy {
SOURCE,
CLASS,
RUNTIME
}
- SOURCE : 컴파일 시 제거됨 ex) @Override
- CLASS : 컴파일된 클래스 파일에 포함되지만, 런타임에는 사용불가
- RUNTIME : 런타임에 리플렉션에 의해 조회 가능
위 enum 에 정의된 코드를 사용하여 명시적으로 선언을 할 수 있다
자바 리플렉션을 사용하기 위해선 Runtime 에 실행되어야 한다, 그래서 RUNTIME 설정을 적용해야 한다
어플리케이션 런타임시 아래와 같은 코드가 실행되며 모든 어노테이션을 조회하여 설정을 적용한다
Class<?> clazz = ExampleAnnotation.class;
Annotation[] annotations = clazz.getAnnotations(); // 클래스에 선언된 모든 어노테이션 조회
우리가 아는 대부분 어노테이션은 Runtime 기반으로 동작한다고 생각하면 된다
추가적으로 어노테이션이 적용될 타겟또한 정의할 수 있다
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
ElementType[] value();
}
public enum ElementType {
/** Class, interface (including annotation interface), enum, or record
* declaration */
TYPE,
/** Field declaration (includes enum constants) */
FIELD,
/** Method declaration */
METHOD,
/** Formal parameter declaration */
PARAMETER,
}
위 enum 4개 말고도 여러 타입이 존재하지만 많이 사용되는 것들로만 일단은 추렸다
위 Target 지정을 함으로써 위 어노테이션이 어디서 동작할지를 설정한다
- TYPE : 클래스, 인터페이스, enum, record 등 클래스 레벨에서 동작 ex) @Service
- FIELD : 변수에 선언 ex) @Column
- METHOD : 메소드에 선언 ex) @Transactional
- PARAMETER : 파라미터에 선언 ex) @Valid
등이 있다.
어노테이션을 직접 만들기 위해서 일단은 이정도만 알아도 충분하다고 생각한다.
이제 실습으로 들어가서 내가 Custom 어노테이션을 만들어보자
2) 실습 - Bean 등록을 해주는 어노테이션을 만들어 보자
목표
- 런타임시 동작해야한다.
- 클래스에서 동작해야 한다.
- 이름이 같은 클래스를 Bean 으로 등록 할 수 없으니, 이름을 지정할 수 있는 기능이 필요하다.
- 빈 등록을 할 수 있어야 한다.
1. 어노테이션 생성
public @interface CustomBean {
}
2. 설정
Bean 등록은 클래스를 하기 때문에 Target 은 'TYPE' 으로 설정한다.
@Target(ElementType.TYPE) // 클래스에서 동작
@Retention(RetentionPolicy.RUNTIME) // 런타임시 동작
@Component // 자동 bean 등록을 위한 컴포넌트 스캔 대상으로 만들기
public @interface CustomBean {
String value() default "";
}
그리고 실제로 Spring Container 에 Bean 으로 등록하는 내부 구현 로직을 작성해야 한다
3. 실제 Bean 등록 내부 구현
package org.hyeonqz.springlab.annotation;
@Configuration
public class CustomBeanRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions (AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry,
BeanNameGenerator importBeanNameGenerator) {
ClassPathScanningCandidateComponentProvider scanner =
new ClassPathScanningCandidateComponentProvider(false);
// CustomBean 어노테이션이 붙은 클래스만 검색
scanner.addIncludeFilter(new AnnotationTypeFilter(CustomBean.class));
String basePackage = "org.hyeonqz.springlab";
Set<BeanDefinition> candidateComponents = scanner.findCandidateComponents(basePackage);
for (BeanDefinition beanDefinition : candidateComponents) {
try {
// 클래스 이름 가져오기
String className = beanDefinition.getBeanClassName();
if (className == null) continue;
// 빈 이름 가져오기
Class<?> clazz = Class.forName(className);
CustomBean customBeanAnnotation = clazz.getAnnotation(CustomBean.class);
String beanName = customBeanAnnotation.value().isEmpty()
? clazz.getSimpleName() // 값이 없으면 클래스 이름으로 등록
: customBeanAnnotation.value();
// 빈 정의 생성
GenericBeanDefinition definition = new GenericBeanDefinition();
definition.setBeanClass(clazz);
// 빈 등록
registry.registerBeanDefinition(beanName, definition);
System.out.println("Bean registered: " + beanName);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
}
4. 객체 Bean 등록
@CustomBean
public class AnnotationService {
}
5. 스프링부트 실행 후 확인
@SpringBootApplication
public class SpringLabApplication {
private final ApplicationContext applicationContext;
public SpringLabApplication (ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
@PostConstruct
public void init() {
if (applicationContext.containsBean("annotationService")) {
System.out.println("AnnotationService is successfully registered.");
} else {
System.out.println("AnnotationService is NOT registered.");
}
}
public static void main (String[] args) {
SpringApplication.run(SpringLabApplication.class, args);
}
}
결과
내가 만든 어노테이션이 실제로 Bean 으로 등록이 되는 것을 확인하였다
결론
Custom 어노테이션을 적절하게 만들어서 사용한다면 프로젝트에 가독성 향상 및 유지보수에 도움을 줄 수있다
지금까지 내 경험상 '이런거 기능 있나?' 라는 생각을 하고 찾아보면 대부분 존재했다
그래서 나는 무언가 명시적으로 사용하기 위해서 어노테이션을 직접 만들어서 사용하고는 한다
ex) @Converter
제 글이 누군가에게 도움이 되기를 바라며..
참조