더 자바, 코드를 조작하는 다양한 방법

애노테이션 프로세서

애노테이션 프로세서

Lombok(롬복)은 어떻게 동작하는 걸까?

Lombok

  • @Getter, @Setter, @Builder 등의 애노테이션과 애노테이션 프로세서를 제공하여 표준적으로 작성해야 할 코드를 개발자 대신 생성해주는 라이브러리.

롬복 사용하기

  • 의존성 추가

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.8</version>
    <scope>provided</scope>
</dependency>

  • IntelliJ lombok 플러그인 설치
  • IntelliJ Annotation Processing 옵션 활성화

롬복 동작 원리

논란 거리

  • 공개된 API 가 아닌 컴파일러 내부 클래스를 사용하여 기존 소스 코드를 조작한다.
  • 특히 이클립스의 경우엔 java agent 를 사용하여 컴파일러 클래스까지 조작하여 사용한다. 해당 클래스들 역시 공개된 API 가 아니다보니 버전 호환성에 문제가 생길 수 있고 언제라도 그런 문제가 발생해도 이상하지 않다.
  • 그럼에도 불구하고 엄청난 편리함 때문에 널리 쓰이고 있으며 대안이 몇가지 있지만 롬복의 모든 기능과 편의성을 대체하진 못하는 현실이다.

참고

애노테이션 프로세서 1부

Processor 인터페이스

  • 여러 라운드(rounds)에 거쳐 소스 및 컴파일 된 코드를 처리 할 수 있다.

유틸리티

  • AutoService : 서비스 프로바이더 레지스트리 생성기

<dependency>
    <groupId>com.google.auto.service</groupId>
    <artifactId>auto-service</artifactId>
    <version>1.0-rc6</version>
</dependency>


@AutoService(Processor.class)
public class FraudProcessor extends AbstractProcessor {
...
}

  • 컴파일 시점에 애노테이션 프로세서를 사용하여 META-INF/services/javax.annotation.processor.Processor 파일 자동으로 생성해 줌.

소스

  • 어노테이션 생성
    • 컴파일 타임에 쓰고 바이트 코드에는 필요가 없음
    • Retention SOURCE 레벨 까지만 유지 설정

@Retention(RetentionPolicy.SOURCE)
public @interface Fraud {

}

  • Processor 생성

@AutoService(Processor.class)
public class FraudProcessor extends AbstractProcessor {

    /**
     * 어떤 에노테이션을 처리할 것인가
     *
     * @return 애노테이션의 문자열
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return Set.of(Fraud.class.getName());
    }

    /**
     * 소스 버전 지정, 상위에 지정된거 사용해서 상관 없음
     *
     * @return
     */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    /**
     * @param annotations the annotation types requested to be processed
     * @param roundEnv    environment for information about the current and prior round
     *                    라운드 개념으로 동작,
     *                    여러 라운드에 걸처서 처리 각라운드마다 프로세서에게 특정한 엘리먼트를 찾으면 처리를 시킨다, 처리 결과가 다름 라운드 넘어갈수 있다
     * @return 만약에 true 를 리턴하면 해당 애노테이션 타입을 처리한것 , 다음 프로세서들 한테 에노테이션을 처리하라고 부탁하지 않음
     */
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {

        Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(Fraud.class); // 애노테이션 찾아옴

        for (Element element : elements){
            if (element.getKind() != ElementKind.INTERFACE){ // 인터페이스 체크
                processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "can'tScam " + element.getSimpleName()); // 인터페이스가 아닐경우 에러 발생
            }else {
                processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "Procession "+ element.getSimpleName());
            }
        }


        return true;
    }
}

ServiceProvider

참고

애노테이션 프로세서 2부

Filer 인터페이스

  • 소스 코드, 클래스 코드 및 리소스를 생성할 수 있는 인터페이스

유틸리티

  • javapoet : 소스 코드 생성 유틸리티

@AutoService(Processor.class)
public class FraudProcessor extends AbstractProcessor {

    /**
     * 어떤 에노테이션을 처리할 것인가
     *
     * @return 애노테이션의 문자열
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return Set.of(Fraud.class.getName());
    }

    /**
     * 소스 버전 지정, 상위에 지정된거 사용해서 상관 없음
     *
     * @return
     */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    /**
     * @param annotations the annotation types requested to be processed
     * @param roundEnv    environment for information about the current and prior round
     *                    라운드 개념으로 동작,
     *                    여러 라운드에 걸처서 처리 각라운드마다 프로세서에게 특정한 엘리먼트를 찾으면 처리를 시킨다, 처리 결과가 다름 라운드 넘어갈수 있다
     * @return 만약에 true 를 리턴하면 해당 애노테이션 타입을 처리한것 , 다음 프로세서들 한테 에노테이션을 처리하라고 부탁하지 않음
     */
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {

        Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(Fraud.class); // 애노테이션 찾아옴

        for (Element element : elements){
            if (element.getKind() != ElementKind.INTERFACE){ // 인터페이스 체크
                processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "can'tScam " + element.getSimpleName()); // 인터페이스가 아닐경우 에러 발생
            }else {
                processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "Procession "+ element.getSimpleName());
            }


            TypeElement typeElement = (TypeElement) element;
            ClassName className = ClassName.get(typeElement);

            MethodSpec takeItOut = MethodSpec.methodBuilder("takeItOut")
                    .addModifiers(Modifier.PUBLIC)
                    .returns(String.class)
                    .addStatement("return $S", "APPLE!")
                    .build();

            TypeSpec testServiceImpl = TypeSpec.classBuilder("testServiceImpl")
                    .addModifiers(Modifier.PUBLIC)
                    .addSuperinterface(className)
                    .addMethod(takeItOut)
                    .build();


            Filer filer = processingEnv.getFiler();
            try {
                JavaFile.builder(className.packageName(), testServiceImpl)
                        .build()
                        .writeTo(filer);
            } catch (IOException e) {
                processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "FATAL ERROR: "+ e);
            }

        }
        return true;
    }
}

애노테이션 프로세서 정리

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

애노테이션 프로세서 장점

  • 런타임 비용이 제로

애노테이션 프로세서 단점

  • 기존 클래스 코드를 변경할 때는 약간의 hacking 이 필요하다

© 2020. All rights reserved.

SIKSIK