개요
평소 사용법만 알았던 어노테이션에 대해서 자세히 알아보고 직접 어노테이션을 만들어보자.
어노테이션이란
자바 소스 코드에 추가하여 사용할 수 있는 메타데이터의 일종
- @ 기호를 붙여 사용한다.
- JDK 1.5 이상 버전에서 사용가능하다.
- 클래스 파일에 임베디드 되어 컴파일러에 의해 생성된 후 자바 가상머신에 포함되어 작동한다.
Annotation의 용도
- compiler를 위한 정보 : Annotation은 컴파일러가 에러를 감지하는데 사용
- 컴파일 시간 및 배포 시간 처리 : Annotation 정보를 처리해 코드, XML 파일 등을 생성
- 런타임 처리 : 일부 Annotation은 런타임에 조사됨
Annotation의 종류
- Built in Annotation : 자바에서 기본 제공하는 어노테이션 ex. @Override, @Deprecated
- Meta Annotation : 커스텀 어노테이션을 만들 수 있게 제공된 어노테이션 ex. @Retention, @Target
- Custom Annotation : 개발자의 의도에 의해 생성된 어노테이션
어노테이션 생성 방법
인터페이스 선언과 동일하나 @ 기호를 앞에 붙여준다.
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME) // Runtime까지 해당 어노테이션을 삭제하지 않음
@Target(ElementType.TYPE) //
public @interface MyAnnotation {
int type() // 어노테이션 선언시에 필요한 데이터를 여기다 선언하기
// 클래스, 메서드 또는 필드에 선언한다면, @MyAnnotation(type = 1) 이런 형식으로 사용됨
}
@Retention
어노테이션이 언제까지 살아있을 것인지를 정하는 어노테이션
- RetentionPolicy.SOURCE : 소스코드(.java) 까지 남아있는다.
- RetentionPolicy.CLASS : 바이트코드(.class) 까지 남아있는다.
- RetentionPolicy.RUNTIME : 런타임까지 남아있는다.
어노테이션 활용하기
Annotation Processor를 통해 어노테이션을 처리할 수 있다.
컴파일 시점에 끼어들어 특정한 어노테이션이 붙어있는 소스코드를 참조해서 새로운 소스코드를 만들어 낼 수 있다
이를 확인해보기 위해 커스텀 어노테이션을 만들어 보자.
커스텀 어노테이션
필자의 경우 메서드의 성능 측정을 위한 어노테이션을 한번 만들어 봤다.
패키지를 생성하기에 앞서 패키지 구조를 아래와 같이 설정 했다. 한 프로젝트 내에서 사용가능한 자료를 찾지 못했다.
root
├─ annotation
└─ application
annotation 모듈 gradle 설정
동적으로 소스코드를 추가하기 위해 javapoet을 이용했다.
또한 autoservice도 함께 추가해줬다. autoservice는 아래와 같은 기능을 한다.
- javax.annotation.processing.Processor 과같은 manifest 파일을 자동으로 생성
⇒ 컴파일 시점에 애노테이션 프로세서를 사용하여 javax.annotation.processing.Processor 파일을 자동으로 생성
plugins {
id 'java'
}
group = 'org.example'
version = '1.0-SNAPSHOT'
repositories {
mavenCentral()
}
dependencies {
implementation 'com.squareup:javapoet:1.13.0'
// <https://mvnrepository.com/artifact/com.google.auto.service/auto-service>
implementation 'com.google.auto.service:auto-service:1.1.1'
annotationProcessor 'com.google.auto.service:auto-service:1.1.1'
}
test {
useJUnitPlatform()
}
PerformanceTest 어노테이션 생성
어노테이션 패키지 내에 PerformanceTest라는 이름의 어노테이션을 생성해줬다. 클래스를 대상으로 할 것이기 때문에, Target을 ElementType.TYPE으로 지정해줬다.
또한, 컴파일 이 후에는 어노테이션 정보가 필요없기 때문에 Retention을 RetentionPolicy.SOURCE로 지정해줬다.
package com.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface PerformanceTest {
}
어노테이션 처리부분
어노테이션 처리의 경우, 필드 값과 메서드를 그대로 가져오지만, 메서드에 한해 해당 메서드의 body 앞 뒤로 System.currentTimeMillis() 함수 실행부분을 넣고 마지막에 body를 실행하는데에 걸린 시간을 출력하도록 구성했다.
@AutoService(Processor.class)
public class PerformanceProcessor extends AbstractProcessor
{
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv)
{
Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(PerformanceTest.class);
List<FieldSpec> fieldSpecList = new ArrayList<>();
List<MethodSpec> methodSpecList = new ArrayList<>();
processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE,
"list all : " + elements.toString());
for (Element element : elements)
{
processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE,
"CustomGetter process : " + element.getSimpleName());
TypeElement typeElement = (TypeElement) element;
for (Element field : typeElement.getEnclosedElements())
{
processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE,
"field : " + field.getKind());
if (field.getKind() == ElementKind.METHOD) {
ExecutableElement executableElement = (ExecutableElement) field;
Trees trees = Trees.instance(processingEnv);
String methodBody = trees.getTree(executableElement).getBody().toString();
methodBody = methodBody.substring(2, methodBody.length());
methodBody = methodBody.substring(0, methodBody.length()-3);
processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE,
"element body : " + methodBody);
String methodNm = String.format("p%s", field.getSimpleName());
MethodSpec methodSpec = MethodSpec.methodBuilder(methodNm)
.addModifiers(Modifier.PUBLIC)
.addStatement("long start = System.currentTimeMillis()")
.addStatement(methodBody)
.addStatement("long end = System.currentTimeMillis()")
.addStatement("System.out.println(String.format(\\"method running time : %d\\", end-start))")
.build();
methodSpecList.add(methodSpec);
} else if (field.getKind() == ElementKind.FIELD) {
String fieldNm = field.getSimpleName().toString();
TypeName fieldTypeName = TypeName.get(field.asType());
FieldSpec fieldSpec = FieldSpec.builder(fieldTypeName, fieldNm)
.build();
fieldSpecList.add(fieldSpec);
}
}
ClassName className = ClassName.get(typeElement);
String getterClassName = String.format("P%s", className.simpleName());
TypeSpec getterClass = TypeSpec.classBuilder(getterClassName)
.addModifiers(Modifier.PUBLIC)
.addFields(fieldSpecList)
.addMethods(methodSpecList)
.build();
try
{
JavaFile.builder(className.packageName(), getterClass)
.build()
.writeTo(processingEnv.getFiler());
}
catch (IOException e)
{
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "ERROR : " + e);
}
}
return true;
}
}
application 모듈 gradle 설정
annotation 모듈을 dependency에 추가해준다.
compileOnly를 통해 해당 모듈이 컴파일시에만 포함하도록 하고
annotationProcessor를 통해 어노테이션 처리를 할 수 있도록 해준다.
plugins {
id 'java'
}
group = 'org.example'
version = '1.0-SNAPSHOT'
repositories {
mavenCentral()
}
dependencies {
compileOnly project(':annotation')
annotationProcessor project(':annotation')
}
test {
useJUnitPlatform()
}
annotation 테스트
테스트용으로 클래스를 하나 생성해주고, 아까 생성한 PerformanceTest 어노테이션을 붙여준다.
import com.annotation.PerformanceTest;
@PerformanceTest
public class AnnotationTest {
public String s;
public int a;
public void run() {
for (int i = 0; i < 1000000000; i++) {
a+=i;
}
}
}
이후 빌드를 하게 되면 아래와 같이 어노테이션 프로세서에서 생성했던 클래스가 추가된다.
이후 클래스를 생성 할 때에는 AnnotationProcessor에 의해 생성된 클래스로 접근하여 사용할 수 있다.
package com.main;
import java.lang.String;
public class PAnnotationTest {
String s;
int a;
public void prun() {
long start = System.currentTimeMillis();
for (int i = 0; i < 1000000000; i++) {
a += i;
};
long end = System.currentTimeMillis();
System.out.println(String.format("method running time : %d", end-start));
}
}
package com.main;
public class Main {
public static void main(String[] args) {
PAnnotationTest pAnnotationTest = new PAnnotationTest();
pAnnotationTest.prun();
}
}
정리
annotation은 소스코드를 동적으로 수정할 수 있는 만큼 잘 다룰 경우 개발에 있어 편리함을 줄 수 있을 것 같다. 지금보니 QueryDSL도 비슷한 방식으로 구현이 되어있는 것 같은데, 잘 활용할 수 있도록 좀 더 공부해두는 것이 좋을 것 같다.
전체 코드는 https://github.com/gogoadl/gradle-annotation-processor 에 업로드 되어 있습니다.
Reference
https://jeong-pro.tistory.com/234
https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=vefe&logNo=222072718782
https://catsbi.oopy.io/78cee801-bb9c-44af-ad1f-dffc5a541101
https://stackoverflow.com/questions/74546841/get-method-source-code-on-annotation-processor
'Java' 카테고리의 다른 글
[자바] 빌더패턴이란? (0) | 2020.07.16 |
---|