Search
Duplicate

애노테이션 프로세서

롬복(ProjectLombok)은 어떻게 동작할까?

롬복이란?

@Getter, @Setter, @Builder등의 애노테이션과 애노테이션 프로세서를 제공하여 표준적으로 사용하는 라이브러리
우리 개발을 하면서 반복적이고 공통적인 코드작성을 애노테이션을 통해 자동화해주는 라이브러리라고도 할 수 있습니다.
결국 핵심은 애노테이션 프로세서 입니다. 이 애노테이션 프로세서는 컴파일 시점에 끼어들어 특정한 애노테이션이 붙어있는 소스코드를 참조해서 새로운 소스코드를 만들어 낼 수 있는 기능입니다. 이 때 새로생성되는 소스코드는 자바일수도있고 다른 어떤 코드일수도 있습니다.
애노테이션 프로세서는 애노테이션이 붙어있는 소스코드의 정보를 트리구조(AST)로 참조할 수 있습니다.
Abastract Syntax Tree
이런 트리를 원래는 참조만 가능하고 조작은 불가능한데, 실제 컴파일을 한 뒤 바이트코드를 보면 변경된 코드를 볼 수 있습니다.
기존 공개된 API를 보면 원래는 RoundEnvironmentTypeElement만 써야합니다. (하단 이미지 참고) 두 타입은 실제로 참조만 가능합니다.
근데, 해당 타입들을 하위 클래스로 캐스팅을 하는데, 타입중에는 참조 뿐 아니라 수정도 가능한 타입이 있고 그 타입으로 캐스팅을 해서 수정을 합니다. 그래서 이게 정상적인 사용법이 아니고 해킹이기 때문에 사용하면 안된다고 주장하는 개발자들도 많이 있습니다.
실제로 타당한 주장입니다만, 계속 사용되는 이유는 이 기능(롬복)에 대한 대안책이 크게 존재하지 않습니다.
AutoValueImmutables라는 오픈소스들도 있긴 하지만, 롬복을 대체할만큼의 기능제공을 해주지 못합니다.

애노테이션 프로세서 실습 1부

코드 작성을 하면서 애노테이션 프로세스에 대해 이해하도록 해보겠습니다.

1. 생성한 애노테이션을 이용할 프로젝트 생성

maven-quickstart 생성
groupId: me.hansol
artifactId: annomoja

2. 코드 작성

Moja.java
package me.hansol; @Magic public interface Moja { String pullOut(); }
Java
복사
App
package me.hansol; /** * Hello world! */ public class App { public static void main(String[] args) { // Moja moja = new MagicMoja(); // System.out.println(moja.pullOut()); } }
Java
복사
위 코드를 작성하면 Moja.java@Magic 어노테이션이 에러발생으로 컴파일이 안 될 것입니다. 이제 애노테이션 프로세서를 만들어서 @Magic annotation 이 동작하도록 구현합니다.

3. 애노테이션 프로세서용 프로젝트 생성

⇒ 구성은 위와 동일하게 하되 artifactId만 다른이름(magicmoja)로 생성해 줍니다.

4. 코드 작성

Magic.java
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 Magic { }
Java
복사
해당 어노테이션은 클래스나 런타임 단위까지 살아있을 필요가 없기때문에 RetentionPolicy.SOURCE를 사용합니다.
MagicMojaProcessor.java
import javax.annotation.processing.AbstractProcessor; import javax.annotation.processing.Processor; import javax.annotation.processing.RoundEnvironment; import javax.lang.model.SourceVersion; import javax.lang.model.element.Element; import javax.lang.model.element.ElementKind; import javax.lang.model.element.Name; import javax.lang.model.element.TypeElement; import javax.tools.Diagnostic; import java.util.Set; @AutoService(Processor.class) public class MagicMojaProcessor 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) { //Magic 이라는 애노테이션을 가지고있는 엘리먼츠를 가지고 온다. Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(Magic.class); for (Element element : elements) { Name elementName = element.getSimpleName(); if (element.getKind() != ElementKind.INTERFACE) { // 엘리먼츠가 인터페이스가 아닐 경우 에러 출력 processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Magic annotation can not be used on " + elementName); } else { processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "Processing "+ elementName); } } return true; } }
Java
복사
이 클래스가 바로 Processor 를 구현해도 되지만, 자바에서 제공하는 AbstractProcessor 를 상속하도록 합니다.
오버라이딩 메소드
getSupportedAnnotationTypes : 어떤 어노테이션들을 처리할지 지정합니다. 위 코드에선 Magic 애노테이션을 처리합니다.
getSupportedSourceVersion : 어떤 소스코드 버전을 지원하는지도 설정할 수 있습니다. 위 코드에서는 가장 최신버전 지원.
process 의 반환타입은 boolean 이고 true를 반환하면 다음 프로세서에게 해당 애노테이션을 처리하라고 부탁하지 않습니다.
참고: Processor(or AbstractProcessor)는 여러 라운드에 걸쳐서 작업을 처리합니다. 라운드마다 이 프로세서한테 특정한 애노테이션들이 이 프로세서가 처리 할 애노테이션을 가지고 있는 엘리먼트를 찾으면 이 프로세서한테 작업을 수행시킵니다. 스프링 시큐리티의 FilterChain과 유사합니다.

5. 배포

5-1. 기존의 정석적인 배포 방식

resources / META-INF /javax.annotation.processing.Processor
me.hansol.MagicMojaProcessor
Plain Text
복사
terminal 에 mvn clean install 수행
(important) 여기까지 수행하면 에러가 발생 할 것입니다. 그 이유는 메이븐이 소스를 컴파일하는 시점에 프로세서가 동작을 하려고하니, 아직 컴파일 되지 않은 소스를 읽으려 하면서 에러가 발생하게 됩니다.
해결책
1.
javax.annotation.processing.Processor 의 내용을 주석처리
#me.hansol.MagicMojaProcessor
Plain Text
복사
2.
terminal에서 mvn clean install
3.
javax.annotation.processing.Processor 주석 해제
4.
terminal에서 mvn install 수행 (clean을 하지않았기에 기존 생성된 target의 소스를 찾아서 쓸 수 있게 된다.)

5-2 외부 라이브러리(AutoService) 사용

⇒ 라이브러리를 사용하면 javax.annotation.processing.Processor 과같은 manifest 파일을 자동으로 생성해 줍니다.
⇒ 컴파일 시점에 애노테이션 프로세서를 사용하여 javax.annotation.processing.Processor 파일을 자동으로 생성해 주는 것 입니다.
의존성 주입
<dependency> <groupId>com.google.auto.service</groupId> <artifactId>auto-service</artifactId> <version>1.0-rc7</version> </dependency>
XML
복사

6. 기존 사용 프로젝트에서 배포된 애노테이션 적용

의존성 주입
<dependency> <groupId>me.hansol</groupId> <artifactId>magicmoja</artifactId> <version>1.0-SNAPSHOT</version> </dependency>
XML
복사
위에서 만든 magicmoja 를 의존성 주입해줍니다.
class type에서 에러가 발생하는지 확인
@Magic public class MyMoja { }
Java
복사
→ 프로젝트 컴파일 결과
의도한대로 인터페이스가 아닌 곳에서의 @Magic 애노테이션 선언은 에러가 발생하는 것을 볼 수 있습니다.

애노테이션 프로세서 실습 2부

1부에서는 애노테이션이 허용한 곳에 제대로 붙어있는지 검사를 했습니다.
이번에는 새로운 소스코드를 생성해 애노테이션을 붙힌 애노테이션을 상속받아 해당 메서드의 반환값을 변경하는 로직을 구현합니다.

1. 의존성 주입

<dependency> <groupId>com.squareup</groupId> <artifactId>javapoet</artifactId> <version>1.13.0</version> </dependency>
XML
복사

2. 코드 작성

@Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(Magic.class); for (Element element : elements) { Name elementName = element.getSimpleName(); if (element.getKind() != ElementKind.INTERFACE) { processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Magic annotation can not be used on " + elementName); } else { processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "Processing "+ elementName); } /* 2부 작성 신규 코드 */ TypeElement typeElement = (TypeElement) element; ClassName className = ClassName.get(typeElement); //className각종 클래스의 정보들을 조회해 사용할 수 있습니다. //메소드를 만들기 위해 MethodSpec을 사용한다. MethodSpec pullOut = MethodSpec.methodBuilder("pullOut") //'pullOut' 이라는 메소드명으로 메소드 생성 .addModifiers(Modifier.PUBLIC) // 접근제한자는 PUBLIC .returns(String.class) //반환타입은 String.class .addStatement("return $S", "Rabbit!") //statement를 정의할 수 있다. 여기서는 바로 RABBIT을 반환한다. .build(); TypeSpec magicMoja = TypeSpec.classBuilder("MagicMoja")//'MagicMoja'라는 클래스타입 생성 .addModifiers(Modifier.PUBLIC) //접근제한자는 PUBLIC .addSuperinterface(className) //Magic 어노테이션을 붙힌 인터페이스('Moja')를 상속받는다. .addMethod(pullOut)//위에서 정의한 pullOut 메소드를 추가한다. .build(); //위에서 메서드를 통해 구현한 객체를 실제로 소스파일로 만들도록 로직을 수행한다. Filer filer = processingEnv.getFiler(); try { JavaFile.builder(className.packageName(),magicMoja) .build() .writeTo(filer); } catch (IOException e) { processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "FATAL ERROR: "+e); } /* //2부 작성 신규 코드 */ } return true; }
Java
복사
Javapoet 라이브러리에서 제공하는 JavaFile 를 통해 filer를 직접적으로 사용안하고 좀 더 손쉽게 소스코드를 작성합니다.
builder(className.packageName(),magicMoja).build()
@Magic 애노테이션이 붙은 인터페이스(element) 와 동일한 패키지경로에 magicMoja를 만듭니다.
.writeTo(filer)
filer 를 통해 생성합니다.
참고: Filer 는 소스 코드, 클래스 코드, 및 리소스를 생성할 수 있는 인터페이스 입니다. createClassFile(),createResource(),createSourceFile() 메서드 등을 통해 각종 코드및 리소스를 생성할 수 습니다.

3. 배포

mvn clean install
Bash
복사

4. 사용 전 준비

설정에서 enable annotation processor 체크 확인
롬복(Lombok) 사용시에도 해당 설정을 체크해야 사용할 수 있습니다.
compile을 통해 target 생성
mvn clean compile
Bash
복사
annotations 설정 변경
기존에는 예외처리되있어서 소스에서 사용을 할 수 없기에 애노테이션 프로세서를 통해 생성된 클래스가 위치한 generated-sources / annotationsSources 로 선택해 마크해줍니다.

5. 테스트

테스트코드 작성
public class App { public static void main(String[] args) { Moja moja = new MagicMoja(); System.out.println(moja.pullOut()); } }
Java
복사
실행 결과

애노테이션 프로세서 정리

애노테이션 프로세서 사용 예

롬복(Lombok)
AutoService: java.util.ServiceLoader용 파일 생성 유틸리티
@Override
Dagger2: 컴파일 타임 DI제공
안드로이드 라이브러리
BUtterKinfe: @BindVIew (뷰 아이디와 애노테이션 붙인 필드 바인딩)
DeepLinkDispatch: 특정 URI 링크를 Activity로 연결할 때 사용

애노테이션 프로세서 장점

런타임 비용이 제로

애노테이션 프로세서 단점

기존 클래스 코드를 변경할 때는 약간의 hack이 필요하다(ex: Lombok)