[Java] 어노테이션(Annotation), @

 

📝 Contents

어노테이션 개념과 샘플 코드

✍️ Annotation 

자바 어노테이션은 잘만 사용하면 매우 유용한 자바의 문법이다. 기본적인 종류는 한정되지만 원하는 대로 커스텀 어노테이션을 만들 수 있어서 적재적소에 활용 가능하다.

 

먼저 어노테이션은 1. 문서화 2. 컴파일러 체크 3. 메타데이터 용도로 사용된다. 문법적으로 @기호가 붙은 심볼을 사용하며 패키지, 클래스, 메서드, 프로퍼티, 변수에 명시할 수 있다. 어노테이션이 붙은 코드를 컴파일 시에 수집해서 API 문서화에 사용되기도 하지만 JavaDoc이라는 좋은 문서화 도구가 있기에 문서화는 가장 비중이 낮은 어노테이션 사용법이다. 이외에도 컴파일 타임에 에러나 경고를 발생시켜 개발자에게 위험 요소를 알리는 목적으로도 사용된다. (= @Override)

 

가장 큰 비중을 갖는 것은 메타데이터로서의 용도이다. 메타데이터란 데이터를 위한 데이터, 데이터를 설명하는 데이터를 의미한다. 메타데이터로서 부가적인 표현뿐만 아니라 리플렉션을 접목하면 특정 클래스의 객체를 생성하고 주입하는 것이 가능하다. (= @Autowired)

 

🍊 Built-in Annotation

자바 SDK에서 기본적으로 제공하는 빌트인 어노테이션들이 존재한다.

 

@Override 

현재 메서드가 슈퍼 클래스 혹은 인터페이스의 메서드를 오버라이드 했음을 컴파일러에게 명시한다. 만약 형식에 맞지 않게 메서드를 구현했다면 컴파일러가 인지하고 에어를 발생한다.

 

@Deprecated  

마커 어노테이션으로 메서드를 사용하지 않도록 유도한다. 만약 사용한다면 컴파일 경고를 일으킨다.

 

@FunctionalInterface

해당 인터페이스가 함수형 인터페이스임을 명시한다. 만약 추상 메서드가 없거나 두 개이상 있다면 컴파일 에러가 발생한다.

 

⚾ Meta Annotation

메타 어노테이션이란 다른 어노테이션에서 사용되는 어노테이션을 말하며 후에 서술할 커스텀 어노테이션을 생성할 때 주로 사용된다. 가령 @Service 어노테이션은 @Component 어노테이션을 내포하고 있는데, 여기서 @Component가 메타 어노테이션에 해당한다.

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Service {

   @AliasFor(annotation = Component.class)
   String value() default "";
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Indexed
public @interface Component {

   String value() default "";
}

 

이외에도 대표적인 메타 어노테이션으로

@Retention

어노테이션이 적용되고 유지되는 범위를 명시하기 위해 사용한다.

@Retention(RetentionPolicy.RUNTIME) // 컴파일 이후에도 JVM에 의해서 참조가 가능하다.
@Retention(RetentionPolicy.CLASS)   // 컴파일러가 클래스를 참조할 때까지 유효하다.
@Retention(RetentionPolicy.SOURCE)  // 어노테이션 정보는 컴파일 이후 없어진다.


@Target 

어노테이션을 적용할 위치를 결정한다.

@Target({ ElementType.PACKAGE, 			// 패키지 선언
		ElementType.TYPE, 				// 타입 선언
		ElementType.CONSTRUCTOR, 		// 생성자 선언
		ElementType.FIELD, 				// 멤버 변수 선언
		ElementType.METHOD, 			// 메소드 선언
		ElementType.ANNOTATION_TYPE, 	// 어노테이션 타입 선언
		ElementType.LOCAL_VARIABLE, 	// 지역 변수 선언
		ElementType.PARAMETER, 			// 매개 변수 선언
		ElementType.TYPE_PARAMETER,		// 매개 변수 타입 선언
		ElementType.TYPE_USE 			// 타입 사용
})

 

@Inherited

자식 클래스가 어노테이션을 상속받을 수 있다. 

@Repeatable 

반복적으로 어노테이션을 선언할 수 있다.

 

Custom Annotation

자바에서 커스텀 어노테이션을 선언하는 것은 간단하다.

public @interface SimpleAnnotation {

}

 

여기에 메타 어노테이션을 붙여 적용 대상, 유지 정책 등을 결정하면 여러 상황에서 어노테이션을 메타데이터로써 활용이 가능하다.

가령, 멤버 변수에 명시 가능하고 @Target(ElementType.FIELD)

컴파일 이후에도 JVM에 의해 참조가 가능한 어노테이션 @Retention(RetentionPolicy.RUNTIME)

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SimpleAnnotation {

	/* enum 타입을 선언 */
	public enum Status {
		STOP, MOVE
	}

	/* enum 사용 */
	Status quality() default Status.STOP;

	/* String 사용 */
	String value();

	/* 배열 사용 */
	int[] values();
}

참고로 어노테이션도 마치 클래스처럼 일종의 멤버 변수인 Element를 갖는 것이 가능하다.

공식 문서에 따르면 아래와 같은 타입만 허용한다.

  • A primitive type
  • String
  • Class or an invocation of Class
  • An enum type
  • An annotation type
  • An array type whose component type is one of the preceding types

후에 예시로 등장하겠지만 어노테이션 자체는 메타데이터를 담는 표식에 불과하지만 리플렉션을 통한 어노테이션의 적용 여부, 엘리먼트 값 처리 등을 할 수 있기에 매우 유용 사용된다.

 

🖥️ Annotation & Reflection Sample Code

샘플 코드 깃 주소

 

@StringInjector 어노테이션

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface StringInjector {

    String value() default "Default name";
}

 

@StringInjector를 적용한 MyObject 클래스 

@Data
public class MyObject {

    @StringInjector("Bart")
    private String name;

    @StringInjector
    private String defaultName;
}

 

Annotation & Reflection 처리 로직

import java.lang.reflect.Field;

public class MyContextContainer {
    public <T> T get(Class<T> clazz) throws IllegalAccessException, InstantiationException {
        T instance = clazz.newInstance();
        instance = invokeAnnonations(instance);
        return instance;
    }

    private <T> T invokeAnnonations(T instance) throws IllegalAccessException {
        // 클래스 필드를 가져오고
        Field[] fields = instance.getClass().getDeclaredFields();
        for (Field field : fields) {
            // 필드에 붙은 어노테이션을 가져오고
            StringInjector annotation = field.getAnnotation(StringInjector.class);

            if (annotation != null && field.getType() == String.class) {
                // 어노테이션의 엘리먼트 값으로 필드 값을 설정
                field.setAccessible(true);
                field.set(instance, annotation.value());
            }
        }
        return instance;
    }
}

 

테스트 코드

class MyContextContainerTest {

    @Test
    public void test_ANNOTATION_REFLECTION() throws Exception {
        MyContextContainer myContextContainer = new MyContextContainer();
        MyObject myObject = myContextContainer.get(MyObject.class);

        assertThat(myObject.getName()).isEqualTo("Bart");
        assertThat(myObject.getDefaultName()).isEqualTo("Default name");
    }
}