롬복(ProjectLombok)은 어떻게 동작할까?
롬복이란?
⇒ @Getter, @Setter, @Builder등의 애노테이션과 애노테이션 프로세서를 제공하여 표준적으로 사용하는 라이브러리
우리 개발을 하면서 반복적이고 공통적인 코드작성을 애노테이션을 통해 자동화해주는 라이브러리라고도 할 수 있습니다.
결국 핵심은 애노테이션 프로세서 입니다. 이 애노테이션 프로세서는 컴파일 시점에 끼어들어 특정한 애노테이션이 붙어있는 소스코드를 참조해서 새로운 소스코드를 만들어 낼 수 있는 기능입니다. 이 때 새로생성되는 소스코드는 자바일수도있고 다른 어떤 코드일수도 있습니다.
애노테이션 프로세서는 애노테이션이 붙어있는 소스코드의 정보를 트리구조(AST)로 참조할 수 있습니다.
Abastract Syntax Tree
이런 트리를 원래는 참조만 가능하고 조작은 불가능한데, 실제 컴파일을 한 뒤 바이트코드를 보면 변경된 코드를 볼 수 있습니다.
기존 공개된 API를 보면 원래는 RoundEnvironment와 TypeElement만 써야합니다. (하단 이미지 참고) 두 타입은 실제로 참조만 가능합니다.
근데, 해당 타입들을 하위 클래스로 캐스팅을 하는데, 타입중에는 참조 뿐 아니라 수정도 가능한 타입이 있고 그 타입으로 캐스팅을 해서 수정을 합니다. 그래서 이게 정상적인 사용법이 아니고 해킹이기 때문에 사용하면 안된다고 주장하는 개발자들도 많이 있습니다.
실제로 타당한 주장입니다만, 계속 사용되는 이유는 이 기능(롬복)에 대한 대안책이 크게 존재하지 않습니다.
애노테이션 프로세서 실습 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
복사
•
@AutoService(Processor.class)
public class MagicMojaProcessor extends AbstractProcessor {
...
}
Java
복사
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 / annotations 를 Sources 로 선택해 마크해줍니다.
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)