학습할 것
- 애노테이션 정의하는 방법
- @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/
다른 애노테이션에 사용되는 애노테이션
이 애노테이션들은 유저가 만드는 애노테이션에 붙는 애노테이션입니다.
애노테이션 정의 방법
@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인터페이스를 구현하는 클래스의 동작
- Processor 객체가 사용되지 않는경우 기본생성자를 호출하여 인스턴스를 생성할 수 있습니다.
- 이후 적절한 ProcessingEnvironment와 함께 init메소드를 호출합니다.
- 그 후에 getSupportedAnnotationTypes, getSupportedOptions, getSupportedSourceVersion메소드를 호출합니다. (단 한번 호출됩니다.)
- getSupportedAnnotationTypes() : 애노테이션 프로세서에서 지원하는 애노테이션 이름을 Set<String>형식으로 반환
- getSupportedOptions() : 도구가 제공하는 옵션들 반환
- getSupportedSourceVersion() : 애노테이션 프로세서가 지원하는 소스버전
- 별다른 문제가 없다면 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의 이름을 알 수 있습니다.
자세한건 여기에서 확인할 수 있습니다.
뭘 할 수 있을까?
애노테이션 프로세서는 애노테이션이 붙어있는 소스를 참조하고 원한다면 새로운 소스를 생성해내는 것이 가능합니다.
관련해서 유명한 API로 lombok이 존재합니다.
애노테이션 프로세서를 만들 때 도움이 되는 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 {
}