[Java] Annotation (feat. Reflection)
Annotation?
Annotate는 어떤 문서에 간단한 설명이나 의견을 달아놓는 것을 뜻한다. "주석을 달다"라고 번역할 수 있다. 이런 추가 정보는 어떤 데이터를 설명하기 위한 데이터이며 우리는 것을 메타데이터라고 부른다.
어노테이션(Annotation)을 사용하는 이유
자바에서 어노테이션은 코드에 메타데이터(코드를 설명하는 추가적인 데이터)를 표현하기 위한 방법 중에 하나이다.
이런 메타데이터를 통해 프로그램은 추가적인 정보를 알 수 있고 그에 따라 여러가지 기능을 지원할 수 있다.
대표적으로 여러 프레임워크에서는 메타데이터와 리플렉션을 통해 런타임에 다양한 기능을 지원해주고는 한다. 우리는 몇 가지 어노테이션으로 의존성 주입, 트랜젝션 관리, 테스트 코드 등을 깔끔하게 구현할 수 있다.
단순히 메타데이터를 제공하는 용도면 성능에 직접적인 영향을 미치지 않지만 런타임에 리플렉션으로 처리될 때는 주의해야 한다. 어노테이션이 런타임에 처리하여 프레임워크는 여러가지 편의 기능을 제공하고는 한다. 하지만 너무 남용하면 알아야 할 정보가 잘보이지 않아서 디버깅할 때 많이 힘들어질 수 있다.
경험해본 어노테이션의 사용 예시
- 컴파일 시 정보 제공
- @Override처럼 컴파일러에게 상위 메서드를 오버라이드 함을 알려서 시그니처 검사를 위한 추가 정보를 제공한다
- 코드 문서화
- 같이 개발하는 동료를 위해 가독성을 높이는 용도로 사용한다
- 테스트 코드
- 테스트코드 프레임워크에서도 어노테이션을 기반으로 실행과정을 자주 제어하는 것을 볼 수 있다
- 객체에 대한 추가적인 런타임 작업
- 주로 프레임워크에서 리플렉션과 결합하여 보일러 플레이트를 제거하기 위해 이러한 것을 제공하는 것을 볼 수 있다.
말고도 데이터 모델링, 보안을 위한 권한 정보 제공 등 여러 가지 사용법이 있다. 주로 다른 객체/사용자에게 기능/정보을 제공하고 싶을 때 사용할 수 있다고 생각한다.
그럼 나만의 어노테이션은 어떻게 만들까?
커스텀 어노테이션 - 나만의 어노테이션을 개발해보자
자바는 나만의 어노테이션을 만들기 위한 문법으로 @interface
키워드를 사용한다. 이 키워드로부터 시작하여 어노테이션을 정의하고 세부적으로 제어할 수 있다.
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyCustomAnnotation {
String description() default "나의 작고 소중한 어노테이션!";
}
어노테이션 세부 설정법
1. 어노테이션의 Retention - 언제까지 보유할 것인가?
위해서 말했듯이 런타임, 컴파일타임에도 쓰일 수 있다. 이런 정보는 @Retention
을 사용하여 어느 시점까지 유지할지 정의할 수 있다.
리플렉션에서 접근할 것이면 RUNTIME
으로 정의하자! 그래야지 런타임에 접근할 수 있다.
2. 어노테이션의 Target - 어디에 메타데이터를 달 것인가?
@Target
은 어노테이션을 적용할 수 있는 대상을 지정한다.
나는 일단은 메서드를 대상으로 지정했다.
커스템 어노테이션의 사용
class Example {
@MyAnnotation(description = "나만의 어노테이션 설명!")
public static void main(String[] args) {
}
}
위의 코드처럼 메서드에 대한 메타데이터를 추가할 수 있다.
리플렉션으로 메인메서드에 어노테이션으로 준 메타데이터를 읽어와보자.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyAnnotation {
String description() default "나의 작고 소중한 어노테이션!";
}
class Example {
@MyAnnotation(description = "메인 메소드")
public static void main(String[] args) {
try {
Method mainMethod = Example.class.getMethod("main", String[].class);
if (mainMethod.isAnnotationPresent(MyAnnotation.class)) {
MyAnnotation annotation = mainMethod.getAnnotation(MyAnnotation.class);
System.out.println("메타데이터: " + annotation.description());
}
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
}
}
리플렉션과 함께 사용한다면 런타임에 강력하면서 위험한 짓을 할 수 있다. 메타데이터를 보고 원래는 못 건들이는 클래스에 관한 동작을 동적으로 변경할 수 있다. 이건 합의가 되지 않으면 누군가의 코드를 깨부수는 행위일 수 있다. 하지만 누군가에게 여러 기능을 쉽게 제공하려다보면 필요할 때도 있다.
간단하게 어노테이션의 메타데이터를 기반으로 public
/private
같은 접근 제어자를 깨부수는 코드이다.
import java.lang.annotation.*;
import java.lang.reflect.Method;
// 실행할 메소드를 지정하기 위한 어노테이션
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface RunMe {}
// 리플렉션을 사용하여 특정 어노테이션이 붙은 메소드만 실행하는 클래스
class ReflectionExample {
@RunMe
private void method1() {
System.out.println("Method 1 실행");
}
private void method2() {
System.out.println("Method 2 실행");
}
@RunMe
private void method3() {
System.out.println("Method 3 실행");
}
// 모든 메소드를 순회하며 @RunMe 어노테이션이 붙은 메서드만 실행하는 메서드
public static void runAnnotatedMethods(Object obj) {
Class<?> objClass = obj.getClass();
for (Method method : objClass.getDeclaredMethods()) {
if (method.isAnnotationPresent(RunMe.class)) {
try {
method.setAccessible(true); // 접근 제한을 무시하고 실행!
method.invoke(obj);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
ReflectionExample example = new ReflectionExample();
runAnnotatedMethods(example);
}
}
ReflectionClass
의 method1
, method2
, method3
은 private
이지만 @RunMe
어노테이션이 달려 있는 경우 접근 제어자를 무시하고 실행하도록 했다.
이런 코드는 여러 기능을 제공해주는 프레임워크나 라이브러리 등을 공부하다보면 종종 볼 수 있다. 더 궁금하다면 테스트 프레임워크나 DI(Dependency Injection) 컨테이너 등에서 찾아보자!
댓글
이 글 공유하기
다른 글
-
서로 닮아 보이는 Decorator, Composite Pattern - 1편
서로 닮아 보이는 Decorator, Composite Pattern - 1편
2025.01.15 -
IntelliJ 자주 쓰는 단축키 모음
IntelliJ 자주 쓰는 단축키 모음
2024.10.23 -
[Java] 람다식과 익명 클래스
[Java] 람다식과 익명 클래스
2024.02.21