[Live Study] #12 어노테이션

Jihoon Oh·2021년 2월 16일
0

Live Study

목록 보기
12/13
post-thumbnail

목표

자바의 어노테이션에 대해 학습하세요.

학습할 것 (필수)

  • 어노테이션 정의하는 방법
  • @retention
  • @target
  • @documented
  • 어노테이션 프로세서

어노테이션이란?

자바 개발을 하다 보면 클래스나 메소드 위에 @가 붙은 단어들을 볼 수 있다. 대표적으로 클래스를 상속받았을때 부모 클래스의 메소드를 오버라이딩 하면서 쓰는 @Override가 있다. 이렇게 @표시를 이용해 표시하는 것을 어노테이션(Annotation)이라고 한다. 어노테이션은 자바 SE 5 버전부터 추가되었으며, 메타데이터 라고 볼 수 있다.

메타데이터(Metadata)란?
데이터에 대한 데이터로, 어플리케이션이 처리하는 데이터가 아니라 컴파일 과정과 실행 과정에서 코드를 어떻게 처리하고 처리할 것인지 알려주는 정보

어노테이션은 클래스, 인터페이스, 메소드, 메소드 파라미터, 필드, 지역변수 위에 위치할 수 있으며, 다음과 같은 역할들을 한다.

  • 컴파일러에게 문법 에러를 체크하도록 정보 제공
    대표적으로 @Override(메소드 오버라이딩을 제대로 했는지에 대한 체크) 어노테이션이 있다.
  • 소프트웨어 개발 툴이 빌드나 배치 시 코드를 자동으로 생성할 수 있도록 정보 제공
    예를 들면 파일 압축을 jar로 할지 war로 할지에 대한 정보를 제공한다.
  • 실행 시 특정 기능을 실행하도록 정보 제공
    어떤 객체가 특별한 기능을 수행해야 할 때, 어노테이션을 붙여서 객체가 해당 기능을 수행할 수 있도록 한다. 예를 들면 스프링에서 @Controller 어노테이션을 붙여서 해당 객체가 컨트롤러 기능을 실행하도록 한다.

자바에서는 다음과 같은 빌트-인 어노테이션들을 제공한다.

  • @Override: 메소드가 제대로 오버라이딩 됐는지 검증
  • @Deprecated: 어노테이션이 붙은 메소드를 더 이상 사용하지 않도록 유도. 사용시 컴파일 에러
  • @SuppressWarnings: 컴파일 경고를 무시
  • @SafeVargs: 제네릭 같은 가변인자 매개변수를 무시 (자바 7 이상)
  • @FunctionalInterface: 람다 함수등을 위한 인터페이스를 지정. 메소드가 없거나 두개 이상 되면 컴파일 오류 발생. (자바 8이상)

어노테이션 정의하는 방법

사용자 정의 어노테이션을 정의할 때는 인터페이스를 정의하는 것과 비슷하게 정의한다. 다만 타입을 interface가 아니라 @interface로 지정한다.

public @interface AnnotationName {
    //...
}

이렇게 정의된 어노테이션은 @AnnotationName 으로 사용한다.

어노테이션은 외부의 값을 입력받을 수 있는 엘리먼트(element) 멤버를 가진다.

public @interface AnnotationName {
    타입 elementName() [default];
    
    // 예시
    String elementName1();
    int elementName2() 10;
}

이렇게 정의된 어노테이션에는 다음과 같이 값을 줄 수 있다.

@AnnotationName(elementName1="값" elementName2=5) // 가능
@AnnotationName(elementName1="값") // 가능
@AnnotationName(elementName2=10) // 불가능

여기서 1과 2가 가능하고 3이 불가능한 이유는, default 값이 정해지지 않은 elementName1에는 반드시 값이 전달되어야 하고, default값이 10으로 정해진 elementName2는 값을 전달해 주지 않아도 된다. 값을 전달할 경우 해당 값이 되고, 값을 전달하지 않을 경우 default 값이 된다.

또한 어노테이션은 기본 값 value()를 가질 수 있다.

public @interface AnnotationName {
    String value(); // 기본 값
    String elementName1 default "값";
}

어노테이션에 value() 하나만 전달해주고 싶다면, value= 을 생략할 수 있다. 하지만 2개 이상의 값을 전달하려고 하면 value에 들어갈 값 앞에 value= 를 반드시 써줘야 한다.

@AnnotationName("값") // 기본 엘리먼트인 value에 값 전달
@AnnotationName(value="값", elementName1="값2")

@Retention

커스텀 어노테이션을 만들때 자주 사용하는 어노테이션 중에 @Retention 이라는 어노테이션이 있다. @Retention 어노테이션은 해당 어노테이션이 언제까지 유효할 지 알려주는 어노테이션이다.

@Retention 어노테이션의 내부를 보면 다음과 같이 정의되어 있다.

package java.lang.annotation;

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
    /**
     * Returns the retention policy.
     * @return the retention policy
     */
    RetentionPolicy value();
}

@Retention 자기 자신에게도 @Retention이 붙어있는데, @Retention이 어노테이션의 범위를 결정하는 어노테이션이기 때문에 @Retention 어노테이션 또한 자기 자신의 유효 범위를 결정하기 위해 @Retention 어노테이션이 필요하다. @Documented와 @Target 어노테이션은 후술한다. 내부의 RetentionPolicy는 @Retention의 메모리 보유 범위를 결정하는 엘리먼트로, enum 타입이다. RetentionPolicy의 정의는 다음과 같다.

package java.lang.annotation;

public enum RetentionPolicy {
    /**
     * Annotations are to be discarded by the compiler.
     */
    SOURCE,

    /**
     * Annotations are to be recorded in the class file by the compiler
     * but need not be retained by the VM at run time.  This is the default
     * behavior.
     */
    CLASS,

    /**
     * Annotations are to be recorded in the class file by the compiler and
     * retained by the VM at run time, so they may be read reflectively.
     *
     * @see java.lang.reflect.AnnotatedElement
     */
    RUNTIME
}

RetentionPolicy enum의 구성 요소는 SOURCE, CLASS, RUNTIME으로, 각각의 속성은 @Retention의 엘리먼트로 전달되어 어노테이션의 범위를 결정한다.

SOURCE

커스텀 어노테이션을 주석처럼 사용하기 위해 어노테이션을 소스 코드 범위에서만 유지한다. 즉, RetentionPolicy 값이 SOURCE일 경우, 자바 파일을 컴파일한 .class 파일(바이트코드)에 어노테이션 정보가 사라진다.

CLASS

어노테이션을 클래스 범위에서 유지한다. 어노테이션에 @Retention 옵션을 지정하지 않으면 기본적으로 @Retention(RetentionPolicy.CLASS)가 적용되며, .class 파일에도 해당 정보가 유지된다. 하지만 해당 클래스를 런타임 시에 메모리로 로드하게 되면 어노테이션의 정보는 사라진다.

RUNTIME

당연히 .class 파일에도 정보가 유지되며, 클래스를 메모리로 읽어와서도 어노테이션의 정보가 유지된다. 이 정보를 통해 특정한 로직을 실행시킬 수 있다.

@Target

@Target 어노테이션은 어노테이션이 적용될 위치를 지정하는 어노테이션이다.

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
    /**
     * Returns an array of the kinds of elements an annotation type
     * can be applied to.
     * @return an array of the kinds of elements an annotation type
     * can be applied to
     */
    ElementType[] value();
}

@Target의 기본 엘리먼트로는 ElementType[]이 오는데, ElementType 역시 enum이며, 다음과 같은 요소들을 가진다.

  • ElementType.ANNOTATION_TYPE
  • ElementType.CONSTRUCTOR
  • ElementType.FIELD
  • ElementType.LOCAL_VARIABLE
  • ElementType.METHOD
  • ElementType.PACKAGE
  • ElementType.PARAMETER
  • ElementType.TYPE

@Target의 기본 엘리먼트로 어떤 enum이 들어가느냐에 따라서 해당 어노테이션이 적용되는 대상이 달라진다.

package java.lang;

import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

예를 들어, @Override 어노테이션은 @Target(ElementType.METHOD)로 지정되어 있기 때문에, @Override 어노테이션은 메소드의 선언부 위에만 사용할 수 있다. 만약 클래스의 멤버 변수에 @Override 를 붙이면 오류가 발생한다.

@Documented

@Documented는 javadoc과 같은 툴을 통해 문서 생성 시 현재 어노테이션에 대한 설명을 문서화 시키는 어노테이션이다. @Documented 어노테이션이 붙을 경우, 해당 어노테이션은 어노테이션 요소의 공개 API가 된다.

어노테이션 프로세서

어노테이션을 사용하기 위해서는 어노테이션 프로세서라는 훅(hook: 소프트웨어 구성 요소 간에 간섭된 함수 호출, 이벤트 또는 메시지를 처리하는 코드)이 필요하다. 어노테이션 프로세서는 자바 플러그인의 일종으로, 자바 컴파일 단계에서 어노테이션의 소스코드를 분석하고 처리한다.

이런 어노테이션 프로세서들은 어노테이션을 파악하여 코드를 자동으로 생성한다. 따라서 불필요한 boilerplate 코드들을 없앨 수 있다. 자바 컴파일을 하면 어노테이션 프로세서에서 어노테이션에 대한 처리를 하고, 자바 컴파일러가 모든 어노테이션에 대해서 어노테이션 프로세서가 실행되었는지에 대해 검사한다. 이 때 검사를 통과하지 못하면 다시 실행한다.

어노테이션 프로세서의 대표적인 예로 Lombok이 있다.

어노테이션 프로세서를 직접 만들 수 있는데, 이 때 어노테이션 프로세서 클래스는 반드시 추상 클래스 AbstractProcessor를 상속받는다. AbstractProcessor는 다음과 같이 구성되어 있다.

package javax.annotation.processing;

import java.util.Set;
import java.util.HashSet;
import java.util.Collections;
import java.util.Objects;
import javax.lang.model.element.*;
import javax.lang.model.SourceVersion;
import javax.tools.Diagnostic;

public abstract class AbstractProcessor implements Processor {

    protected ProcessingEnvironment processingEnv;
    private boolean initialized = false;

    // 어노테이션 프로세서는 반드시 빈 생성자를 필요로 함
    protected AbstractProcessor() {}

    public Set<String> getSupportedOptions() {
        SupportedOptions so = this.getClass().getAnnotation(SupportedOptions.class);
        if  (so == null)
            return Collections.emptySet();
        else
            return arrayToSet(so.value(), false);
    }
    
    // 이 어노테이션 프로세서가 처리할 어노테이션 타입을 명시 
    public Set<String> getSupportedAnnotationTypes() {
            SupportedAnnotationTypes sat = this.getClass().getAnnotation(SupportedAnnotationTypes.class);
            boolean initialized = isInitialized();
            if  (sat == null) {
                if (initialized)
                    processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING,
                                                             "No SupportedAnnotationTypes annotation " +
                                                             "found on " + this.getClass().getName() +
                                                             ", returning an empty set.");
                return Collections.emptySet();
            } else {
                boolean stripModulePrefixes =
                        initialized &&
                        processingEnv.getSourceVersion().compareTo(SourceVersion.RELEASE_8) <= 0;
                return arrayToSet(sat.value(), stripModulePrefixes);
            }
        }

    // 이 어노테이션 프로세서가 사용하는 소스 버전을 특정
    // SourceVersion.latestSupported()를 리턴하면 최신 버전 리턴
    // 자바 6을 고수해야 한다면 SourceVersion.RELEASE_6를 리턴
    public SourceVersion getSupportedSourceVersion() {
        SupportedSourceVersion ssv = this.getClass().getAnnotation(SupportedSourceVersion.class);
        SourceVersion sv = null;
        if (ssv == null) {
            sv = SourceVersion.RELEASE_6;
            if (isInitialized())
                processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING,
                                                         "No SupportedSourceVersion annotation " +
                                                         "found on " + this.getClass().getName() +
                                                         ", returning " + sv + ".");
        } else
            sv = ssv.value();
        return sv;
    }
    
    // processingEnv필드를 processingEnv인수 값으로 설정하여 처리 환경으로 프로세서를 초기화
    public synchronized void init(ProcessingEnvironment processingEnv) {
        if (initialized)
            throw new IllegalStateException("Cannot call init more than once.");
        Objects.requireNonNull(processingEnv, "Tool provided null ProcessingEnvironment");

        this.processingEnv = processingEnv;
        initialized = true;
    }
    
    // 프로세서의 main 메소드 역할
    public abstract boolean process(Set<? extends TypeElement> annotations,
                                    RoundEnvironment roundEnv);
                                    
    public Iterable<? extends Completion> getCompletions(Element element,
                                                         AnnotationMirror annotation,
                                                         ExecutableElement member,
                                                         String userText) {
        return Collections.emptyList();
    }

    private static Set<String> arrayToSet(String[] array,
                                          boolean stripModulePrefixes) {
        assert array != null;
        Set<String> set = new HashSet<>(array.length);
        for (String s : array) {
            if (stripModulePrefixes) {
                int index = s.indexOf('/');
                if (index != -1)
                    s = s.substring(index + 1);
            }
            set.add(s);
        }
        return Collections.unmodifiableSet(set);
    }
}

커스텀 어노테이션 프로세서를 만들면, 위 추상 클래스를 상속받아서 필수적으로 init() 메소드를 오버라이딩 하고, 추가적으로 어노테이션 프로세서가 담당할 어노테이션 타입을 지정하는 getSupportedAnnotationTypes(), 자바 버전을 지정하는 getSupportedSourceVersion() 등을 오버라이딩해서 사용한다.

참고 자료
https://docs.oracle.com/javase/7/docs/api/javax/annotation/processing/AbstractProcessor.html
https://hamait.tistory.com/314

profile
Backend Developeer

0개의 댓글