개요

평소 사용법만 알았던 어노테이션에 대해서 자세히 알아보고 직접 어노테이션을 만들어보자.

어노테이션이란

자바 소스 코드에 추가하여 사용할 수 있는 메타데이터의 일종

  • @ 기호를 붙여 사용한다.
  • 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

안드로이드 앱 개발 공부를 하면서 예제에서 많이 접했던 방법이었으나, 정확히 이것이 빌더패턴인지는

모르고 있었는데 "스프링 부트와 AWS로 혼자 구현하는 웹 서비스" 이 책을 읽다가 명확히 알게 되었다.

 

빌더패턴이란?

Example.builder()
	.a(a)
    	.b(b)
    	.build();

자바 또는 안드로이드 공부를 하다보면 구글링을 할 때 한번씩은 꼭 봤을 것이다. 빌더패턴과 비교되는 대상은 

여러가지가 있겠지만, 책에서는 생성자를 통해 소개했다.

 

public Example(String a, String b)
{
	this.a = a;
    	this.b = b;
}

이 두가지의 차이점은 무엇일까??

 

만약 개발자가 new Example(b,a) 처럼 a와 b의 위치를 변경해도 코드를 실행하기 전까지는

문제를 찾을 수 없다는 것이다.

 

하지만 빌더 패턴을 사용하면! 위의 예제와 같이 어느 필드에 어떤 값을 채워야 하는지 명확하게 인지할 수 있다!

 

 

'Java' 카테고리의 다른 글

AnnotationProcessor 직접 구현해보기  (1) 2024.03.25

+ Recent posts