Annotation Processing 101 (번역)

Jason Kim
28 min readMay 4, 2016

--

Hannes Dorfmann 가 작성한 “Annotation Processing 101” 을 원작자의 동의를 받아 번역한 글입니다.

이번 블로그 글에서는 어노테이션 프로세서를 작성하는 방법에 대해 설명하려고 합니다. 첫번째로, 어노테이션 프로세싱이 무엇인지 설명하고, 이 강력한 툴로 할 수 있는 것과 할 수 없는 것을 설명할 것입니다. 두번째 단계로 단계별로 간단한 어노테이션 프로세서를 구현할 것입니다.

The Basics

이번 글에서 런타임에 리플렉션을 사용하는 어노테이션 에 대한 평가는 하지 않을 것입니다. (런타임 = 어플리케이션이 실행하는 시점) 어노테이션 프로세싱은 컴파일 타임에 이뤄집니다. (컴파일 타임 = 자바 컴파일러가 자바 소스 코드를 컴파일 하는 시점)

어노테이션 프로세싱은 컴파일 시간에 어노테이션들을 스캐닝하고 프로세싱하는 javac 에 속한 빌드툴입니다. 특정 어노테이션들을 위해 어노테이션 프로세서를 만들어서 등록할 수 있습니다. 이번 글에서는 독자들이 어노테이션이 무엇이고 어떻게 어노테이션 타입을 선언할 수 있는지는 미리 알고 있다고 가정하겠습니다. 어노네이션들이 익숙하지 않는다면 공식 자바 문서 를 참고해주세요. 어노테이션 프로세싱은 자바 5 부터 가능하지만 유용한 API 들은 자바 6 (2006년 12월에 출시) 부터 사용 가능합니다. 자바 세계에서 어노테이션 프로세싱의 힘이 널리 사용되기까지는 시간이 꽤 걸렸습니다. 그래서 지난 몇년 동안 어노테이션 프로세싱이 유명해졌습니다.

특정 어노테이션을 위한 어노테이션 프로세서는 자바 코드(또는 컴파일된 바이트 코드)를 인풋으로 받아서 아웃풋으로 파일(보통 .java 파일)을 생성합니다. 이게 정확히 무엇을 의미할까요? 여러분이 자바 코드를 생성할 수 있음을 뜻합니다. 생성된 자바코드는 .java 파일 입니다. 하지만 이미 존재하는 자바 파일을 수정해서 메서드를 추가하는 것은 할 수 없습니다. 생성된 자바 파일은 javac 에 의해 컴파일 될 것입니다.

AbstractProcessor

이제 프로세서 API 를 살펴보도록 하겠습니다. 모든 프로세서들은 AbstractProcessor 를 상속받아야 합니다.

  • init(ProcessingEnvironment env): 모든 어노테이션 프로세서 클래스는 empty 생성자를 반드시 가져야합니다. 대신, ProcessingEnvironment 를 파라미터로 받아 어노테이션 프로세싱 툴이 호출하는 특별한 init() 메서드를 가지고 있습니다. ProcessingEnvironment 는 Elements, Types, Filer 과 같이 유용한 유틸 클래스들을 제공합니다. 글의 후반에 이것들을 사용해볼 것입니다.
  • process(Set<? extends TypeElement> annotations, RoundEnvironment env): 이것은 각각의 프로세서의 main() 메서드의 역할을 합니다. 여기에 scanning, evaluating, 어노테이션 프로세싱, 자바 파일 생성을 위한 코드를 작성합니다. RoundEnvironment 를 파라미터를 가지고 여러분은 특정 어노티이션이 달린 것들을 찾을 수 있습니다.
  • getSupportedAnnotationTypes(): 여기에 이 어노테이션 프로세서가 처리할 어노테이션들을 명시합니다. 리턴 타입은 이 어노테이션 프로세서가 처리하길 원하는 어노테이션의 full qualified name 을 포함한 Set<String> 입니다. 다시말해서, 여기에 여러분이 어노테이션 프로세서가 처리하길 원하는 어노테이션들을 정의해야합니다.
  • getSupportedSourceVersion(): 여러분이 사용하는 특정 자바버전을 명시하는데 사용합니다. 보통 SourceVersion.latestSupported() 를 리턴하면 됩니다. 하지만 자바 6을 고수해야되는 이유가 있다면 SourceVersion.RELEASE_6 을 리턴하면 됩니다. 저는 SourceVersion.latestSupported() 를 사용하길 추천합니다.

자바 7 을 이용하면 getSupportedAnnotationTypes() 와 getSupportedSourceVersion() 를 사용하는 대신 아래와 같이 어노테이션을 사용할 수 있습니다.

특정 안드로이드의 호환성을 위해 @SupportedAnnotationTypes 와 @SupportedSourceVersion 대신에 getSupportedAnnotationTypes() 와 getSupportedSourceVersion() 를 사용할 것을 추천 합니다.

다음으로 여러분이 알아야 할 것은 어노테이션 프로세서가 JVM 위에서 돌아간다는 것 입니다. javac 는 실행중인 어노테이션 프로세서들을 위해 자바 가상 머신을 시작시킵니다. 이게 어떤 의미일까요? 다른 어떤 자바 프로그램에서 사용하는 것들을 사용할 수 있다는 것을 의미합니다. Guava 를 사용할 수 있습니다. Dagger 와 같은 의존성 주입 툴이나 여러분이 원한는 어떤 라이브러리들이던 사용할 수 있습니다. 그러나 잊지마세요. 단지 작은 프로세서일지라도 여러분이 다른 자바 프로그램에서 하는것 처럼 효율적인 알고리즘과 디자인 패턴들에 대해서도 고민해야 합니다.

Register Yor Processor

여러분은 “어떻게 내가 만든 프로세서를 javac 에 등록하지?” 라고 생각할 수 있습니다. .jar 파일을 제공해야합니다. 여러분이 작성한 (컴파일된) 어노테이션 프로세서를 .jar 파일로 패키징하세요. 그리고 META-INF/services 에 위치하는 javax.annotation.processing.Processor 로 불리는 특정 파일도 여러분의 .jar 파일에 같이 패킹징 해야합니다. 아래와 같이 .jar 파일이 생겨야 합니다.

MyProcessor.jar
- com
- example
- MyProcessor.class

- META-INF
- services
- javax.annotation.processing.Processor

javax.annotation.processing.Processor 파일의 내용은 new line 을 구분자로 사용한 프로세서들의 full qualified class name 의 목록입니다.

com.example.MyProcessor
com.foo.OtherProcessor
net.blabla.SpecialProcessor

javac 가 MyProcessor.jar 를 이용해서 자동으로 javax.annotation.processing.Processor 파일을 발견하고 읽어서 MyProcessor 를 어노테이션 프로세서로 등록합니다.

Example: Factory Pattern

이제 구체적인 예제를 살펴보겠습니다. 빌드 시스템과 의존성 관리 툴로 maven 을 사용하겠습니다. maven 에 익숙하지 않아도 걱정하지마세요 필수적인 것은 아닙니다. 전체 코드는 github 에 있습니다.

시작하기에 앞서 말하자면 튜토리얼을 위해서 어노테이션 프로세서로 해결할만한 간단한 문제를 찾는게 쉽지 않았습니다. 여기에서 우리는 간단한 팩토리 패턴을 구현해볼 것 입니다. 예제를 통해서 여러분에게 어노테이션 프로세싱 API 에 대한 간단한 소개를 할 것입니다. 문제해결 코드가 짤막하고 실제 현업에서 쓰는 것같지 않을 수 있습니다. 다시 한번 말하자면 디자인 패턴을 배우는 것이 아니라 어노테이션 프로세싱을 배우는 것입니다.

이제 문제를 살펴봅시다. 우리는 피자 가게를 구현하려고 합니다. 피자 가게는 고객들에게 2개의 피자(“Margherita” 와 “Calzone”)와 디저트로 Tiramisu 를 제공합니다.

아래의 코드 스니펫을 살펴보겠습니다.

PizzaStore 에 주문을 하기 위해서는 음식의 이름을 입력해야합니다:

보다시피 order() 메서드에 많은 if 문이 존재합니다. 그리고 새로운 타입의 피자를추가할 때 마다 우리는 새로운 if 문을 추가해야 합니다. 하지만 우리는 어노테이션 프로세싱와 팩토리 패턴을 이용해서 어노테이션 프로세서가 if 문을 생성하게 할 수 있습니다. 결과적으로 우리가 원하는 것은 아래 코드와 같습니다.

MealFactory는 아래와 같습니다.

@Factory Annotation

우리는 어노테이션 프로세싱을 이용해서 MealFactory 를 생성하기 원합니다. 더 일반적으로 보자면, 우리는 팩토리 클래스를 생성하기 위해 어노테이션과 프로세서를 제공하길 원합니다.

@Factory 어노테이션을 살펴봅시다.

type() 의 값이 같으면 같은 팩토리에 속하고 “Calzone” 을 CalzonePizza 클래스에 매핑시키는 id() 를 가지도록 클래스에 어노테이션을 달아주는 아이디어를 생각할 수 있습니다. @Factory 를 아래와 같이 클래스에 적용해 보겠습니다.

@Factory 를 Meal 인터페이스에 적용될 수 있는지 궁금할 수 있습니다. 어노테이션들은 상속 받지 않습니다. class X 에 어노테이션을 달았다고 class Y extends X 처럼 X 를 상속하는 Y 에 자동으로 어노테이션이 붙지 않습니다. 프로세서를 작성하기전에 우리는 몇가지 룰을 정하겠습니다.

  1. 인터페이스나 abstract 클래스들은 new 로 인스턴스화 될 수 없기 때문에 @Factory 는 오직 클래스에만 붙을 수 있습니다.
  2. @Factory 가 붙은 클래스들은 적어도 하나의 empty default (파라미터가 없는)생성자를 제공해야 합니다. 그렇지 않으면 새로운 인스턴스가 생성될 수 없습니다.
  3. @Factory 가 붙은 클래스들은 특정 type 으로 부터 직접, 간접적으로 상속되야합니다. (인터페이스가 있다면 구현해야합니다.)
  4. 같은 type 을 가진 @Factory 가 붙은 클래스는 그룹으로 묶이고 하나의 팩토리 클래스를 생성합니다. 생성된 클래스의 이름에 접미사로 “Factory”가 붙습니다. 예를 들면 type = Meal.class 라면 MealFactory 가 생성됩니다.
  5. id 는 String 으로 제한되고 type 그룹안에서 유일해야합니다.

The Processor

이제 코드를 추가하며 차례차례 살펴보도록 하겠습니다. 말 줄임표(…) 는 이전 단락에서 설명했거나 다음 단계에서 설명하기 위한 코드의 생략을 의미합니다. 읽기 좋은 단위로 만드는게 목적입니다. 앞서 언급한거 처럼 전체 코드는 github 에서 볼 수 있습니다. 이제 FactoryProcessor 의 스켈레톤을 살펴보겠습니다.

첫 줄에서 @AutoService(Processor.class) 를 볼 수 있습니다. 이게 무엇일까요? 이것은 다른 어노테이션 프로세서의 어노테이션 입니다. AutoService 는 구글에서 개발된 어노테이션 프로세서이고 META-INF/services/javax.annotation.processing.Processor 를 생성합니다. 우리가 작성하는 어노테이션 프로세서 안에서 다른 어노테이션 프로세서를 사용할 수 있습니다. getSupportedAnnotationTypes() 에서 우리가 프로세서에서 다룰 @Factory 를 명시합니다.

Elements and TypeMirrors

  • Elements: Element 를 이용하는 유틸 클래스이다.
  • Types: TypeMirror 를 이용하는 유틸 클래스이다.
  • Filer: 이름에서 볼 수 있듯이 파일을 만드는데 도와준다.

어노테이션 프로세싱에서 자바 소스코드 파일을 스캔할 수 있습니다. 소스코드의 모든 부분들은 특정 Element 의 타입입니다. 다시말해서, Element 는 패키지, 클래스, 메서드와 같은 프로그램 요소들을 나타냅니다. 각각의 요소들은 static, language-level construct 를 나타냅니다. 다음 예제에서 주석을 통해 요소들을 나눠봤습니다.

여러분이 소스 코드를 보는 방법을 바꿔야합니다. 코드는 단지 잘 구성된 텍스트입니다. 실행가능하지 않습니다. 여러분이 파싱하려는 XML 파일같이 생각할 수 있습니다. XML 파서에는 몇가지 DOM 종류가 있습니다. Element 들의 부모나 자식 Element 로 이동할 수 있습니다.

public cass foo 를 나타내는 TypeElement 가 있다고 생각해봅시다. 여러분은 이것의 자식들을 다음과 같이 순환할 수 있습니다.

여러분이 보다시피 Element들은 소스코드를 나타냅니다. TypeElement 는 클래스와 같은 소스코드를 나타냅니다. 하지만, TypeElement 는 그 자체로 클래스의 정보들을 가지지는 않습니다. TypeElement 로 부터 여러분은 클래스의 이름을 얻을 수 있습니다., 하지만 superclass 와 같은 클래스의 정보들을 얻을 수 는 없습니다. 이런 종류의 정보는 TypeMirror 를 통해서 접근가능합니다. element.asType() 을 호출해서 Element 의 TypeMirror 에 접근할 수 있습니다.

Search For @Factory

이제 process() 메서드를 단계별로 구현해보겠습니다. 첫번째로 @Factory 가 달린 클래스들을 찾아야합니다.

여기에는 로켓 공학이 사용되지 않습니다. roundEnv.getElementsAnnotatedWith(Factory.class)) 가 @Factory 가 달린 Element 들의 List 를 반환합니다. 여러분은 @Factory 가 달린 클래스들의 목록을 반환하라는 요청을 피해야합니다. 왜냐하면 실제로 반환되는 것은 Element 목록이기 때문입니다. Element 는 클래스, 메서드, 변수등등 다양한 것들이 될 수 있습니다. 그래서 우리는 Element 가 클래스인지 확인하는 과정을 거쳐야합니다.

여기서 무슨일이 일어날까요? 우리는 프로세서가 오직 클래스 타입의 Element 만 다루기를 원합니다. 이전에 우리는 클래스가 TypeElement 였던 것을 살펴보았습니다. 왜 if(!(annotatedElement instanceof TypeElement)) 같이 체크하면 안될까요. 인터페이스도 TypeElement 이기 때문에 안됩니다. 어노테이션 프로세싱에서 여러분은 instanceof 사용을 피해야합니다. 대신에 ElementKind 나 TypeKind 를 사용하세요.

Error Handling

init() 에서 우리는 Messager 를 봤습니다. Messager 는 어노테이션 프로세서가 에러 메세지나 경고 문구나 다른 주의사항을 리포트하는 법을 제공합니다. 이것은 여러분을 위한 로그 기록기가 아닙니다. 어노테이션 프로세서의 개발자들을 위한것입니다. Messager 는 여러분이 작성한 어노테이션 프로세서를 사용하는 third party 개발자들에게 메세지를 제공할 때 사용됩니다. 공식문서에 메세지의 다양한 레벨들이 설명되 있습니다. 어노테이션 프로세서가 프로세싱에 실패했을때를 알릴 때 사용하는 Kind.ERROR 는 중요합니다. 아마 third party 개발자가 @Factory 어노테이션을 잘못사용했을 경우가 있을 수 있습니다(인터페이스에 @Factory 를 단 경우). Exception 을 발생시키는 전통적인 자바 어플리케이션과는 약간 다른 컨셉입니다. process() 에서 exception 을 발생시키면 jvm은 annotation processing (일반 자바 어플리케이션처럼) 크래쉬가 발생합니다 그리고 우리가 만든FactoryProcessor 를 사용하는 third party 개발자들은 javac 로 부터 알기 힘든 Exception 이 포함된 에러를 받습니다. 왜냐하면 FactoryProcessor 의 stack trace 가 포함되기 때문입니다. 그러므로 어노테이션 프로세서는 Messager 클래스를 가지고 있습니다. 이것으로 보기좋은 에러 메세지를 출력합니다. 여러분은 에러를 발생시키는 element 를 연결시켜 보여줄 수 있습니다. IntelliJ 와 같은 요즘의 IDE 들에서 third party 개발다 들은 에러메세지를 클릭할 수 있고 IDE 는 소스 파일과 third party 개발자들가 작성한 라인으로 이동시켜줍니다..

process() 메서드 구현을 다시 살펴보겠습니다. @Factory 가 클래스가 아닌 부분에 달리면 에러 메세지를 발생하게 했습니다.

Messager 의 메세지를 보여주기 위해서 크래쉬 없이 어노테이션 프로세서가 완료되는 것은 중요합니다. 그래서 error() 가 호출된 이후에 return 을 호출합니다. 여기서 우리가 return 하지 않는다면, process() 는 messager.printMessage( Diagnostic.Kind.ERROR) 가 프로세스를 중지시키지 못하기 때문에 계속 실행됩니다. 그래서 에러 출력이후에 return 하지 않는다면내부적인 NullPointerException 과 같은 에러 환경에서도 계속 프로세서가 멈추지 않고 있게됩니다. 전에 말한것 처럼, process() 에서 다루지 못하는 예외가 발생한다면 javac는 Messager 의 에러 메세지가 아니라 내부적인 NullPointerException 의 stack trace 를 출력할 것입니다.

DataModel

@Factory가 달린 클래스들이 위에서 말한 5가지 규칙을 만족시키는지 체크하는 것을 계속하기전에, 데이터 구조에 대해서 설명하겠습니다. 가끔은 문제나 프로세서가 너무 간단해서 프로그래머들이 절차적인 방식으로 전체 프로세서를 작성하는 경우가 있습니다. 그러나 어노테이션 프로세서는 여전히 자바 어플리케이션입니다. 그래서 객체지형 프로그래밍, 인터페이스, 디자인패턴, 자바 어플 케이션에서 사용하는 것들을 적용해야 합니다.

FactoryProcessor 는 꽤 간단합니다. 그러나 객체로 저장하길 원하는 몇가지 정보들이 있습니다. FactoryAnnotatedClass를 이용해서 어노테이션이 달린 클래스의 qualified class name 과 같은 데이터를 저장합니다. 그래서 우리는 TypeElement 를 저장합니다.

코드가 많지만 가장 중요한 것은 다음 코드에서 따라나오는 생성자 부분입니다.

여기서 우리는 @Factory 어노테이션에 접근해서 id 가 비어있는지 확인합니다. id 가 비어있다면 IllegalArgumentException 를 발생시킵니다. 이전에는 예외를 발생시키지 않고 Messager 를 사용한다고 해서 헷갈릴 수 있습니다. 그 내용은 여전히 맞습니다. 우리는 내부적으로 예외를 발생시키고 process() 에서 예외를 잡아서 처리할 것입니다. 여기에는 2가지 이유가 있습니다.

  1. 다른 자바 어플리케이션에서 하는것처럼 코드를 작성하기를 원합니다. 자바에서 예외를 발생시키고 캐치하는 것은 좋은 방법입니다.
  2. FactoryAnnotatedClass 로 부터 받은 메세지를 출력하기를 원한다면 Messager 에 넘겨줘야합니다. “Error Handling” 섹션에서 말한것 처럼 MEssager 가 에러메세지를 출력하기 위해서는 성공적으로 프로세서가 종료되야 합니다. 그래서 Messager 를 사용해서 에러 메세지를 출력하려면 process() 에게 어떻게 이러가 발생했는지 알려줘야할까요? 제 관점에서 가장 쉽고 직관적인 방법은 Exception 을 발생시키고 process() 가 그것을 잡아서 처리하는 것입니다.

다음으로는 @Factory 어노테이션의 type 필드를 얻기를 원합니다. 우리는 full qualified name 를 알기 원합니다.

여기서 type 이 java.lang.Class 이기 때문에 약간의 트릭을 사용합니다. type 이 진짜 Class 객체임을 의미합니다. 어노테이션 프로세싱은 자바 소스 코드가 컴파일 되기 전에 일어나므로 우리는 2가지 경우를 고려해야합니다.

  1. 클래스가 미리 컴파일 되어있는 경우: third party .jar 는 컴@Factory 어노테이션과 함께 컴파일된 .class 파일들을 포함하는 경우가 있습니다. 이 경우에 우리는 try-block 에서 직접 Class 에 접근할 수 있습니다.
  2. 클래스가 아직 컴파일 되지 않은 경우: @Factory 어노테이션을 가지고 있는 소스 코드를 컴파일하려 시도하는 경우가 있을 수 있습니다. 이런 경우 Class 에 직접 접근하면 MirroredTypeException 이 발생합니다. 운좋게도 MirroredTypeException 는 아직 컴파일되지 않은 클래스를 표현하는 TypeMirror 를 포함하고 있습니다. 우리는 이것이 클래스의 타입은 것을 알고있기 때문에 (전에 미리 클래스 인지 체크했습니다.) DeclaredType 으로 캐스트 하고 TypeElement 에 접근해서 qualified name 을 읽어옵니다.

이제 우리는 FactoryAnnotatedClass 들을 포함하는 FactoryGroupedClasses 라는 데이터 구조에 대해서 알아보겠습니다.

보다시피 기본적으로 Map<String, FactoryAnnotatedClass> 을 가지고 있습니다. 이 맵은 @Factory.id() 를 키로 해서 FactoryAnnotatedClass 를 매핑하는 맵입니다. 각각의 id 가 유일하기 때문에 Map 이라는 자료구조를 선택했습니다. generateCode() 는 팩토리 코드를 작성할때 불려집니다. (뒤에서 자세히 살펴보겠습니다.)

Matching Criteria

process() 의 구현에 대해서 더 살펴보겠습니다. 다음으로 우리는 어노테이션이 달린 클래스가 추상클래스가 아니고 특정한 type 을 상속하며 적어도 하나의 public 생성자를 가지는지 public 클래스 인지 체크할 것입니다.

규칙이 맞는지 체크하기 위해서 isValidClass() 메서드를 추가했습니다.

  • 클래스는 public 해야 합니다: classElement.getModifiers().contains(Modifier.PUBLIC)
  • 클래스는 추상클래스가 아니어야합니다: classElement.getModifiers().contains(Modifier.ABSTRACT)
  • 클래스는 subclass 이어야하고 @Factory.type() 에 명시된 클래스를 구현해야합니다. 첫번째로 elementUtils.getTypeElement(item.getQualifiedFactoryGroupName()) 를 사용해서 Element 를 생성합니다. qualified class name 을 앎으로써 TypeElement 를 만들 수 있습니다. 다음으로 superClassElement.getKind() == ElementKind.INTERFACE 를 이용해서 이것이 interface 인지 class 인지 체크합니다. 그리고 두가지 경우가 존재합니다. interface 인 경우 classElement.getInterfaces().contains(superClassElement.asType()) 를 이용해서 인터페이스가 구현됬는지 확인합니다. class 인 경우 currentClass.getSuperclass() 를 호출해서 상속 계층을 스캔 해야합니다. 그리고 typeUtils.isSubtype() 을 이용해 (@Factory 의 type 이 구현 됬는지) 확인합니다. (번역자: 코드에는 typeUtils.isSubtype() 가 사용되지 않습니다. 원문에서 코드 수정이 있었던것 같습니다. 하지만 @Factory 의 type 필드에 해당하는 코드가 구현됬는지 확인하는 과정은 이뤄져야합니다)
  • 클래스가 public empty 생성자를 가져야 합니다. classElement.getEnclosedElements() 를 이용해 모든 element 들을 순환하면서 ElementKind.CONSTRUCTOR, Modifier.PUBLIC, constructorElement.getParameters().size() == 0 에 해당하는 public empty 생성자가 있는지 확인합니다.

조건이 모두 만족 한다면 isValidClass() 가 true 를 반환하고, 그렇지 않다면 false 를 리턴하고 에러 메세지를 출력합니다.

Grouping The Annotated Classes

isValidClass() 를 체크하고 FactoryAnnotatedClass 를 아래와 같이 해당하는 FactoryGroupedClasses 에 추가합니다.

Code Generation

@Factory 가 달린 모든 클래스들을 수집하고 FactoryAnnotatedClass 로 저장하고 FactoryGroupedClasses 에 그룹화 시켰습니다. 이제 각각의 Factory 를 위해 자바 파일들을 생성해보겠습니다.

자바 파일을 쓰는 것은 자바에서 어떤 다른 파일을 쓰는것과 매우 유사합니다. Filer 에 의해 제공되는 Writer 를 사용하겠습니다. String 을 붙임으로써 코드를 작성하겠습니다. 운좋게도 멋진 오픈소스들로 잘 알려진 Square Inc. 에서 자바 코드를 생성하기 위한 라이브러리인 JavaWriter 를 제공합니다.

Tipp: JavaWriter 가 많은 프로세서들에게 인기를 얻은 이후로, 라이브러리들이나 툴들이 JavaWriter 에 의존하고있습니다. 메이븐이나 그래들과 같은 의존성 관리 툴을 사용한다면 어떤 한 라이브러리가 새로운 버전의 JavaWriter 에 의존한다면 문제가 생길 수 있습니다. 그러므로 JavaWriter 를 여러분의 어노테이션 프로세서 코드에 복사해오거나 새로 패킹징 하는 것을 추가합니다.

Update: JavaWriter 가 JavaPoet 으로 대체되었습니다.

Processing Rounds

어노테이션 프로세싱은 한번의 프로세싱 라운드이상 일어납니다. 공식 javadoc 에는 프로세싱을 다음과 같이 정의하고 있습니다.

“어노테이션 프로세싱은 연속적인 라운드에서 일어납니다. 각각의 라운드에서, 프로세서는 소스나 이전 라운드에서 생성된 클래스 파일에서 발견된 어노테이션들의 집합을 처리합니다. 첫번째 프로세싱 라운드의 인풋은 최초의 인풋입니다. 이 최초의 인풋들은 프로세싱 가상의 0번째 라운드의 아웃풋으로 간주됩니다.”

더 간단한 정의: 프로세싱 라운드는 어노테이션 프로세서의 process() 를 호출합니다. 우리의 팩토리 예제에 적용하면, FactoryProcessor 는 한번 인스턴스화 됩니다. (새로운 프로세서가 매 라운드마다 생성되지는 않습니다.) 하지만 새로운 소스파일이 생겨난다면 process() 는 여러번 호출될 수 있습니다. 이상한 소리처럼 들릴 수 있습니다 그렇지 않나요? 생성된 소스 코드 파일은 @Factory 어노테이션이 달린 클래스들을 포함합니다.

PizzaStore 의 예제를 살펴보면 3번의 라운드가 실행됩니다.

(출처: http://hannesdorfmann.com/annotation-processing/annotationprocessing101)

프로세싱 라운드를 설명하는 또다른 이유가 있습니다. 여러분이 FactoryProcessor 코드를 본다면, 여러분은 데이터를 수집하고 Map<String, FactoryGroupedClasses> factoryClasses 필드에 그것들을 저장합니다. 첫번째 라운드에서 우리는 MagheritaPizza, CalzonePizza, Tiramisu 를 발견하고 MealFactory.java 를 생성합니다. 두번째 라운드에서 MealFactory 를 인풋으로 삼게됩니다. 여기에는 @Factory 어노테이션이 업기 때문에 데이터가 수집되지 않습니다. 에러를 유발하지도 않습니다. 그러나 com.hannesdorfmann.annotationprocessing101.factory.MealFactory 을 위한 파일을 재생성하려고 시도합니다.

factoryClasses 를 지우지 못하는 문제가 있습니다. 두번째 라운드에서보면 첫번째 라운드에서 얻은 데이터를 여전히 저장하고 있고 첫번째 라운드와 같은 파일을 생성하려고 합니다. 이것은 에러를 유발할 수 있습니다. 우리의 경우에 오직 첫번째 라운드에서만 @Factory 가 달린 클래스들이 발견되므로 다음과 같이 간다히 고칠 수 있습니다.

이러한 문제를 다루는 다른 방법들(boolean 플래그를 설정한다.)이 존재합니다. 여기서 중요한 것은 어노테이션 프로세싱은 여러 라운드가 실행되고 이미 생성된 소스 파일을 덮어 쓰거나 재생성할지 않아야 합니다.

Separation of processor and annotation

팩토리 프로세서의 깃 저장소 를 살펴보면 두가지 메이븐 모듈로 나뉘어 저장소가 구성되어 있는 것을 볼 수 있습니다. Factory 예제를 사용하는 개발자들에게 그들의 프로젝트에서 단지 어노테이션만 컴파일되기를 원합니다. 그리고 프로세서 모듈은 컴파일할 때만 포함되기를 원합니다. 혼란스럽나요? 하나의 artifact 로 만든다면 Factory 프로젝트를 사용하는 개발자들은 빌드할 때 그들의 프로젝트에 @Factory 어노테이션과 FactoryProcessor 전체 코드를 둘다 포함시켜야합니다. 그들의 컴파일되는 프로젝트에 프로세서 클래스가 없는거는 당연합니다. 안드로이드 개발자라면 메서드 개수가 65k 를 넘으면 안된다고 들어봤을 것입니다. FactoryProcessor 에서 Guava 를 사용하고 어노테이션과 프로세서 코드를 포함하는 하나의 artifact 를 제공한다면, 안드로이드 APK 는 FactoryProcessor 코드 뿐만아니라 Guava 코드도 포함하게 될 것 입니다. Guava 는 대략 20000 개의 메서드를 가지고 있습니다. 그러므로 어노테이션과 프로세서를 분리하는게 좋아보입니다.

Instantiation Of Generated Classes

PizzaStore 예제에서 보다시피 생성된 MealFactory 클래스는 손으로 쓴 것 처럼 평범한 자바 클래스 입니다. 그러므로 직접 인스턴스화 해야합니다.

안드로이드 개발자라면 ButterKnife 어노테이션 프로세서에 익숙할 것입니다. ButterKnife 에서 뷰에 @InjectView 를 답니다. ButterKnifeProcessor 는 MyActivity$$ViewInjector 이라는 클래스를 생성합니다. 하지만 ButterKnife 에서 직접 new MyActivity$$ViewInjector() 를 호출해서 ButterKnife 인젝터를 인스턴스화할 필요는 없습니다. 대신 Butterknife.inject(activity) 를 사용할 수 있습니다. ButterKnife 는 내부적으로 리플렉션을 이용해서 MyActivity$$ViewInjector() 를 인스턴스화 시킵니다.

근데 리플렉션은 느리지않나요? 맞습니다. 리플렉션은 성능 이슈를 불러올 수 있습니다. 그러나 개발자들이 직접 객체를 인스턴스화 시키지 않기 때문에 개발 속도는 빨라집니다. ButterKnife 는 HashMap 을 사용해서 인스턴스화 된 객체를 캐쉬합니다. 그래서 MyActivity$$ViewInjector 는 한번만 인스턴스화 됩니다. 다음 번에 MyActivity$$ViewInjector 가 필요하다면 HashMap 에서 가져다 사용합니다.

FragmentArgs 는 ButterKnife 와 유사하게 동작합니다. 개발자가 FragmentArgs 를 직접 인스턴스화 시키는 대신 리플렉션을 이용해서 인스턴스화 합니다. FragmentArgs 는 어노테이션 프로세싱동안 특별한 HashMap 과 같은 “lookup” 클래스를 생성합니다. 그래서 전체 FragmentArgs 라이브러리를 실행할때 특별한 HashMap 클래스를 인스턴스화하는 오직 한번만 리플렉션을 사용합니다. Class.forName() 을 이용해서 이 클래스를 인스턴스화 시킬때 프래그먼트 arguments injection 이 동작합니다.

여러분이 만든 어노테이션 프로세서의 사용자들을 위해 리플렉션과 사용성을 비교하는 모든 것은 여러분(어노테이션 프로세서 개발자)한테 달려있습니다.

Conclusion

여러분이 이제는 어노테이션 프로세싱에 대해 깊게 이해하기를 바랍니다. 다시말하지만 어노테이션 프로세싱은 매우 강력한 도구입니다. 그리고 보일러플레이트 코드를 제거하는데 도움을 줍니다. 어노테이션 프로세서를 가지고 여러분이 Factory 예제보다 훨씬 복잡한 일들을 할 수 있다고 말하고 싶습니다. 여러분이 봤다시피 어노테이션 프로세서를 작성할때 일반적으로 2가지 문제를 살펴봤습니다. 첫번째는 다른 클래스들에서 ElementUtils, TypeUtils, Messager 를 사용하길 원한다면 그것들을 파라미터로 넘겨야합니다. 안드로이드의 많은 어노테이션 프로세서들중 하나인 AnnotatedAdapter 에서는 Dagger(의존성 주입)를 이용해서 이 문제를 풀었습니다. 이거는 간단한 프로세서를 만드는데 과한 작업이라고 생각할 수 있습니다. 하지만 실제로 잘작동합니다. 두번째는 Elements 를 위한 “queries” 입니다. 이전에 말했듯이, Elements를 가지고 작업하는것은 XML 이나 HTML 을 파싱하는 것과 같아보입니다. HTML 에서는 jQuery 를 사용할 수 잇습니다. 어노테이션 프로세싱에서도 jQuery 와 유사한 것이 있으면 정말 좋을것 같습니다. 여러분이 아는 어노테이션 프로세싱에서 jQuery 와 같은 라이브러리가 있다면 댓글 달아주시길 바랍니다.

FactoryProcessor 코드의 일부는 함정을 가지고 있으니 주의하시길 바랍니다. (파일을 다시 만들려고 시도하는 것과 같이) 어노테이션 프로세서를 작성할때 제가 설명했던 일반적인 실수들과 같은 함정이 있고 이것이 여러분을 괴롭힐 수 있습니다. FactoryProcessor 에 기반해서 여러분의 어노테이션 프로세서를 작성하려고 한다면 이러한 함정들을 그대로 복사해서 사용하지 마세요. 처음부터 이러한 문제를 피해야만 합니다.

나중에 어노테이션 프로세서 유닛 테스트에 대한 글(Annotation Processing 102)을 작성할 예정입니다. 그치만 다음 블로그 글은 안드로이드 아키텍쳐에 관한 것이 될것입니다.

Update

2015 Germany Droidcon 에서 어노테이션 프로세싱에 대해 발표했습니다. Youtube에서 발표를 보실 수 있습니다.

--

--

Jason Kim

Haechi Labs CEO, Haechi Labs provides Henesis & Haechi Audit.