Java/라이브스터디

애노테이션

학습할 것

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

 


 

애노테이션이란?

 

애노테이션이란 주석의 한 형태로 데이터에 대한 데이터 즉, 메타 데이터를 나타냅니다.
기본적으로 자바에서 제공하는 7개의 애노테이션이 존재하며 그 종류는 다음과 같습니다.

 

java.lang 패키지에서 제공하는 애노테이션

  • @Deprecated : 더이상 사용되지 않음
  • @Override : 재정의 된 메소드
  • @SuppressWarnings : 특정 컴파일러 경고를 억제

java.lang.annotation 에서 가져오는 애노테이션 (유저 애노테이션에 사용)

  • @Retention : 애노테이션의 유지시점
  • @Documented : javadoc 으로 문서 생성 시 애노테이션의 설명 추가
  • @Target : 애노테이션을 달 수 있는 곳
  • @Inherited : 애노테이션의 상속가능 유무

출처 : https://www.geeksforgeeks.org/annotations-in-java/

애노테이션에 대한 정보는 리플랙션을 사용하여 얻을 수 있습니다.

각 애노테이션에 대해 살펴봅시다.



자바에서 제공하는 애노테이션

@Deprecated

public class DeprecatedTest {

    @Deprecated
    int val = 10;

    @Deprecated
    public void Deprecated_method(){
        System.out.println("Deprecated Method Call");
    }

    @Deprecated
    DeprecatedTest(int val){
        this.val = val;
    }
}

이 애노테이션이 달린 요소가 사용되지 않음을 의미합니다.

 

해당 애노테이션이 달린 요소를 사용하려고 하면 취소선으로 표현됩니다.


@Override

public class SUPERCLASS {

    public void Method(){
        System.out.println("Hello Super?");
    }
}


public class OverrideTest extends SUPERCLASS{

    @Override
    public void Method(){
        System.out.println("Hello Sub?");
    }
}

이 애노테이션은 메소드에만 달 수 있으며, 해당 메소드가 재정의되었음을 나타냅니다.


@SuppressWarnings

public class App
{

    @SuppressWarnings("deprecation")
    public static void main( String[] args ) {
        DeprecatedTest deprecatedTest = new DeprecatedTest();
        deprecatedTest.Deprecated_method();
        deprecatedTest = new DeprecatedTest(10);
        System.out.println(deprecatedTest.val);
    }
}

경고가 표기되지 않게 해주는 애노테이션입니다.

 

 

@SuppressWarnings 애노테이션이 달리지 않았을 경우 취소선 을 이용하여 deprecated된 요소를 표시해주지만 애노테이션이 달리게 되면 사라지는 모습을 확인할 수 있습니다.

자바에서 경고는 deprecation과 unchecked 두 분류로 나뉩니다.

  • deprecation : 사용되지 않는 요소에 대한 경고
  • unchecked : 미확인된 연산 같은 것에 대한 경고


@SafeVarargs

자바 7에서 추가된 애노테이션으로 메소드나 생성자에 적용됩니다.

파라미터가 가변인자인 경우에 발생하는 경고를 무시하는 애노테이션입니다.

제네릭과 관련되어서 추가된 애노테이션으로 보이며, 지금은 그냥 이런게 있구나 라고 생각하며 넘어가겠습니다.


@FunctionalInterface

@FunctionalInterface
public interface Funtional {
    public void showme();
}

Java SE 8 에서 제공된 애노테이션으로 함수형 인터페이스를 나타내는 인터페이스 입니다.

 

함수형 인터페이스란, 하나의 추상메소드만 지니고있는 인터페이스를 의미하며
사용이유는 자바의 람다식이 함수형 인터페이스로만 접근이 되기 때문입니다.

 

public static void main( String[] args ) {
    Funtional f = new Funtional(){

        @Override
        public void showme() {
            System.out.println("Hello");
        }
    };
}

이 코드에 람다식을 적용하면 아래와 같이 변합니다.

 

public static void main( String[] args ) {
    Funtional f = () -> System.out.println("Hello");
}

출처 : https://codechacha.com/ko/java8-functional-interface/

 

Java8 - 함수형 인터페이스(Functional Interface) 이해하기

함수형 인터페이스는 1개의 추상 메소드를 갖고 있는 인터페이스를 말합니다. Single Abstract Method(SAM)라고 불리기도 합니다. 함수형 인터페이스를 사용하는 이유는 자바의 람다식은 함수형 인터페

codechacha.com

 

다른 애노테이션에 사용되는 애노테이션

이 애노테이션들은 유저가 만드는 애노테이션에 붙는 애노테이션입니다.


애노테이션 정의 방법

@interface MyAnnotation{

}

@interface로 생성된 클래스는 애노테이션을 의미합니다.

 

이 애노테이션은 @Retection, @Target, @Inherited 애노테이션들을 이용해여 애노테이션의 정보가 유지되는 시점, 애노테이션의 적용요소, 상속유무 등을 설정할 수 있습니다.

 

애노테이션은 값을 가지는 것이 가능하며 그 방법은 아래와 같습니다.

@interface MyAnnotation{
    String name() default "HI";
    
    int a() default 10;

    
}@interface MyAnnotation{ String name() default "HI"; int a() default 10; }

애노테이션의 값은 애노테이션을 달아줄 때 지정해줄 수 있으며, 기본값을 설정하기 위해서는 default를 이용해야 합니다.

애노테이션의 변수는 프리미티브 타입의 변수만 사용가능합니다.

 

@MyAnnotation(name = "ddings", a = 20)
public class Cafe {

}

애노테이션을 달 때에는 위와 같이 내부의 필드값을 정해줄 수 있습니다.

 

public @interface MyAnnotation {
    String value();

}

@MyAnnotation("ddings")
public class Cafe {
    
}

value라는 이름으로 만들어진 필드 하나만 존재한다면 따로 필드명을 사용하지 않아도 됩니다.

 

public @interface MyAnnotation {
    String value();
    int number();
}

@MyAnnotation(value = "ddings", number = 10)
public class Cafe {

}

필드가 두 개 이상일 경우 필드명을 이용하여 값을 정해주어야 합니다.


@Retention

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

@Retention(RetentionPolicy.CLASS)
public @interface Cafe_menu {


}

애노테이션의 데이터가 어디까지 유지되는지를 설정합니다.

데이터의 유지시점은 3가지가 있습니다.

  • RetentionPolicy.SOURCE
    • 소스레벨까지 애노테이션이 유효하다는 것을 나타냅니다.
    • 컴파일 시 사라집니다.
  • RetentionPolicy.CLASS
    • 컴파일 타임까지 애노테이션이 유효하다는 것을 나타냅니다.
    • JVM의 동작 시 사라집니다. (바이트 코드로 바뀌면 사라짐)
  • RetentionPolicy.RUNTIME
    • 프로그램의 동작 시 까지 애노테이션이 유효하다는 것을 나타냅니다.


@Target

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

@Target(ElementType.ANNOTATION_TYPE)
public @interface Cafe_menu {
    
}

해당 애노테이션의 적용지점을 나타냅니다.

 

Target Type 의미
ElementType.ANNOTATION_TYPE 다른 애노테이션
ElementType.CONSTRUCTOR 생성자
ElementType.FIELD 필드
ElementType.LOCAL_VARIABLE 지역변수
ElementType.METHOD 메소드
ElementType.PACKAGE 패키지
ElementType.PARAMETER 파라미터
ElementType.TYPE 클래스나 인터페이스, Enum


@Inherited

클래스의 상속 시에 애노테이션이 같이 상속될 수 있음을 의미합니다.

 


@Documented

이 애노테이션이 달린 애노테이션을 Javadoc 도구를 이용하여 문서화 해야됨을 나타냅니다.

 

더보기

JavaDoc이 뭐지?

JavaDoc은 자바 소스파일에서 API 문서를 HTML페이지로 생성해주는 툴입니다.

 

이런 페이지를 생성해주는 툴입니다.

@Repeatable

Java SE 8 에서 추가된 애노테이션으로, 애노테이션을 여러개 달 수 있음을 나타냅니다.

 

package me.ddings73;


import java.lang.annotation.Annotation;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Method;


@Retention(RetentionPolicy.RUNTIME)
@Repeatable(MyAnnos.class)
@interface MyAnno {
    String name();
    int value();
}

@Retention(RetentionPolicy.RUNTIME)
@interface MyAnnos {
    MyAnno[] value();
}

public class App
{

    @MyAnno(name = "First", value = 1)
    @MyAnno(name = "Second", value = 2)
    public static void method(){
        App app = new App();

        try {
            Class<? extends App> aClass = app.getClass();

            Method m = aClass.getMethod("method");

            Annotation anno = m.getAnnotation(MyAnnos.class);
            System.out.println(anno);

            anno = m.getAnnotation(MyAnno.class);
            System.out.println(anno);

        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        method();
    }
}



== 출력 결과 == 
@me.ddings73.MyAnnos(value={@me.ddings73.MyAnno(name="First", value=1), @me.ddings73.MyAnno(name="Second", value=2)})
null

반복해서 애노테이션을 사용 시에 해당 값을 저장할 애노테이션(MyAnnos)을 지정하는 방식으로 사용됩니다.

 

 

package me.ddings73;


import java.lang.annotation.Annotation;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Method;


@Retention(RetentionPolicy.RUNTIME)
@Repeatable(MyAnnos.class)
@interface MyAnno {
    String name();
    int value();
}

@Retention(RetentionPolicy.RUNTIME)
@interface MyAnnos {
    MyAnno[] value();
}

public class App
{

    @MyAnno(name = "First", value = 1)
    public static void method(){
        App app = new App();

        try {
            Class<? extends App> aClass = app.getClass();

            Method m = aClass.getMethod("method");

            Annotation anno = m.getAnnotation(MyAnnos.class);
            System.out.println(anno);

            anno = m.getAnnotation(MyAnno.class);
            System.out.println(anno);

        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        method();
    }
}

= = = = = =
null
@me.ddings73.MyAnno(name="First", value=1)

반복 가능한 애노테이션이라도 한번만 달았을 경우 해당 애노테이션 타입에 값이 저장됩니다.



애노테이션 프로세서

애노테이션 프로세서는 컴파일 시에 특정 애노테이션이 붙어있는 소스를 참조해서 새로운 코드를 생성해내는 기능입니다.

애노테이션 프로세싱은 라운드 방식으로 동작합니다.
매 라운드마다 프로세스는 이전 라운드에서 생성된, 애노테이션이 달린 소스나 클래스파일에 대한 처리를 할 수 있습니다.

 

boolean process(Set<? extends TypeElement> annotations,
                    RoundEnvironment roundEnv);

Processor 인터페이스의 process 메소드를 통하여 애노테이션이 달려있는 소스나 클래스파일에 대한 작업을 할 수 있습니다.

 

라운드방식으로 작업을 처리하기때문에 현재 라운드에서 특정 애노테이션에 대한 처리를 할 수 없다면 이후 라운드에서 처리하도록 요청합니다.

 

Processor의 각 구현체들은 반드시 기본생성자를 가지고 있어야합니다.


Processor인터페이스를 구현하는 클래스의 동작

  1. Processor 객체가 사용되지 않는경우 기본생성자를 호출하여 인스턴스를 생성할 수 있습니다.
  2. 이후 적절한 ProcessingEnvironment와 함께 init메소드를 호출합니다.
  3. 그 후에 getSupportedAnnotationTypes, getSupportedOptions, getSupportedSourceVersion메소드를 호출합니다. (단 한번 호출됩니다.)
    • getSupportedAnnotationTypes() : 애노테이션 프로세서에서 지원하는 애노테이션 이름을 Set<String>형식으로 반환
    • getSupportedOptions() : 도구가 제공하는 옵션들 반환
    • getSupportedSourceVersion() : 애노테이션 프로세서가 지원하는 소스버전
  4. 별다른 문제가 없다면 Processor객체의 process메소드를 호출합니다.
    • Processor객체는 매 라운드마다 생성되지 않음.


ProcessingEnvrionment


애노테이션 프로세스는 init의 호출과정에서 ProcessingEnvrionment를 구현한 객체를 사용합니다.

ProcessingEnvrionment에서는 파일의 생성, 에러메시지와 같은 기능을 제공합니다.

애노테이션 프로세스의 전체 환경설정이라고 볼 수 있습니다.

 

제공기능

반환 값 메소드 기능
Elements getElementUtils() Element에 대한 유틸리티
Filer getFiler() 새로운 소스나 클래스등을 생성할 수 있는 filer
Locale getLocale 현재 Locale이나 null
Messager getMessager() 에러메시지, 경고메시지, 또는 기타 메시지들을 사용가능
Map<String,String> getOptions Tool 사용에 있어서 줄 수 있는 옵션과 설명들
SourceVersion getSourceVersion 클래스파일이나 소스의 버전
Type getTypeUtils Type에 대한 유틸리티



RoundEnvironment

애노테이션 프로레서는 라운드 방식으로 동작하므로, 매 라운드별 환경설정을 의미합니다.

 

제공기능

반환 값 메소드 기능
boolean errorRaised() 이전 라운드에서의 에러발생 유무에 따라 true or false 반환
Set<? extends Element> getElementsAnnotatedWith(Class<? extends Annotation> a) 주어진 애노테이션이 붙어있는 Elements 반환
Set<? extends Element> getElementsAnnotatedWith(TypeElement a) 주어진 애노테이션이 붙어있는 Elements 반환
Set<? extends Element> getRootElements() 이전 라운드에 의해 생성된 애노테이션 처리를 위한 root elements 반환, 없으면 null
boolean processingOver() 이 라운드에서 생성된 타입이 이후의 애노테이션처리에 관계없으면 true 반환



Element는 무엇일까요

element란 패키지, 클래스, 메소드와 같은 요소들을 의미합니다.

getKind()를 통하여 해당 element가 어떤 유형인지 알 수 있으며, getSimpleName()을 통해 element의 이름을 알 수 있습니다.

자세한건 여기에서 확인할 수 있습니다.

 

Element (Java Platform SE 8 )

Returns the type defined by this element. A generic element defines a family of types, not just one. If this is a generic element, a prototypical type is returned. This is the element's invocation on the type variables corresponding to its own formal type

docs.oracle.com


뭘 할 수 있을까?

 

애노테이션 프로세서는 애노테이션이 붙어있는 소스를 참조하고 원한다면 새로운 소스를 생성해내는 것이 가능합니다.

관련해서 유명한 API로 lombok이 존재합니다.

 

Project Lombok

 

projectlombok.org

애노테이션 프로세서를 만들 때 도움이 되는 API로 javapoet과 AutoService가 있습니다.

javapoet : 소스파일 생성 시 도움을 주는 API
AutoService : 애노테이션 프로세서생성 시 자동으로 서비스를 만들어주는 API

더보기

애노테이션 프로세서로 삽질한 결과물

 

@Magic
public interface Fruit {
    public String getName();
    public void setName(String name);
}


public class App 
{
    public static void main( String[] args )
    {
        Fruit fruit = new FruitMagic();
        fruit.setName("Banana");
        System.out.println(fruit.getName());
    }
}
package me.ddings73;

import com.google.auto.service.AutoService;
import com.squareup.javapoet.*;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.*;
import javax.tools.Diagnostic;
import java.io.IOException;
import java.util.Arrays;
import java.util.Set;

@AutoService(Processor.class)
public class FruitMagicProcessor extends AbstractProcessor {

    @Override
    public Set<String> getSupportedAnnotationTypes(){
        return Set.of(Magic.class.getName());
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(Magic.class);
        for (Element element : elements) {
            Name simpleName = element.getSimpleName();

            if(element.getKind() != ElementKind.INTERFACE){
                processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Magic can't be used " + simpleName);
            }else {
                processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "Processing " + simpleName);
            }

            TypeElement typeElement = (TypeElement) element;
            ClassName className = ClassName.get(typeElement);


            FieldSpec name = FieldSpec.builder(String.class, "name")
                    .addModifiers(Modifier.PRIVATE)
                    .build();

            MethodSpec setName = MethodSpec.methodBuilder("setName")
                    .addModifiers(Modifier.PUBLIC)
                    .returns(void.class)
                    .addParameter(String.class, "name")
                    .addStatement("this.name = name")
                    .build();

            MethodSpec getName = MethodSpec.methodBuilder("getName")
                    .addModifiers(Modifier.PUBLIC)
                    .returns(String.class)
                    .addStatement("return this.name")
                    .build();

            TypeSpec FruitMagic = TypeSpec.classBuilder("FruitMagic")
                    .addModifiers(Modifier.PUBLIC)
                    .addField(name)
                    .addMethods(Arrays.asList(getName, setName))
                    .addSuperinterface(className)
                    .build();


            Filer filer = processingEnv.getFiler();
            try {
                JavaFile.builder(className.packageName(), FruitMagic)
                        .build()
                        .writeTo(filer);
            } catch (IOException e) {
                processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "FATAL ERROR " + e);
            }
        }
        
        return true;
    }
}
package me.ddings73;

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

@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
public @interface Magic {

}

출처