728x90
반응형
SMALL

자바로 개발을 할때 이 지루한 코드들을 보거나 작성해 본 경험이 있으신가요?

package cwchoiit;

public class Member {
    private String name;
    private int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}
  • Getter, Setter의 작업은 굉장히 개발자로 하여금 지루한 코드 작성 요소라고 볼 수 있습니다.
  • 물론, 요즘에는 IDE의 도움을 받아 굉장히 편리하게 작성을 해주지만 그럼에도 불구하고 코드가 쓸데없이 방대해지고 길어지는 건 막을 수가 없죠.

 

여기서 우리의 위대한 선배 개발자님들은 이 과정에 지루함을 느꼈습니다. 이 과정이 자동화되면 좋겠다고 말이죠. 그렇게 탄생한 정말 개인적으로 엄청 위대한 라이브러리라고 생각되는 이 `Lombok`. 많이들 사용하시나요? 저는 필수로 사용중입니다. 

 

그래서 다음과 같이 위 코드를 대체할 수가 있죠.

package cwchoiit;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class Member {

    private String name;
    private int age;
}
  • 위 코드와 비교해서 얼마나 깔끔한가요? 쓸데없이 길게 늘어진 Getter, Setter가 없으니 훨씬 보기도 편하고, 여기에 의미있는 메서드나 도메인 주도 개발을 포커스로 개발하시는 분들한테는 더할 나위 없이 좋겠죠.

 

그런데 궁금하지 않으셨나요? 도대체 어떻게 이렇게만 하면 자동으로 위에 코드처럼 Getter, Setter가 만들어지고 사용할 수가 있는지? 전 이 부분이 너무 너무 궁금했습니다. 그래서 공부하고 찾아보게 됐습니다. Lombok의 원리!

 

Lombok의 핵심 키워드는! → '애노테이션 프로세싱'과 'AST 조작'

 

애노테이션 프로세싱

1. 애노테이션 프로세서(Annotation Processor)란?

  • Java의 애노테이션 프로세서는 컴파일 단계에서 애노테이션을 분석하고, 이를 기반으로 추가적인 코드를 생성하거나 컴파일러에 특정 작업을 지시하는 기능을 제공합니다.
  • 이는 Java Compiler API의 일부로, 애노테이션 기반의 코드 생성 및 검증을 지원하는 도구입니다. 
  • 애노테이션 프로세서는 `javax.annotation.processing` 패키지에 정의된 인터페이스와 클래스들을 통해 구현됩니다. 
  • 일반적으로 컴파일 타임에 실행되며, 런타임에 영향을 미치지 않습니다.

2. 애노테이션 프로세서(Annotation Processor)의 주요 역할

  • 애노테이션 처리: 소스 코드에 선언된 애노테이션을 감지하고, 이를 기반으로 처리 작업 수행
  • 소스 코드 생성: @Getter와 같은 애노테이션을 통해 Getter 메서드 자동 생성.
  • 애노테이션 검증: 애노테이션 사용이 올바른지 확인하고, 잘못된 경우 컴파일러 오류를 출력해준다. 예를 들어, 특정 필드에만 적용 가능한 애노테이션을 다른 위치에 사용했는지 검증할 수 있습니다.
  • 리소스 파일 생성: 컴파일 시점에 특정 리소스 파일 생성 (예: `.properties` 파일)

3. 애노테이션 프로세서(Annotation Processor)의 동작 원리

3-1. 컴파일러와의 통합

 

애노테이션 프로세서는 Java 컴파일러(Javac)의 플러그인 형태로 작동합니다.

컴파일러가 코드를 컴파일하면서 애노테이션 프로세서를 호출하여 필요한 작업을 수행합니다.

 

3-2. Annotation Processing API

 

애노테이션 프로세서를 작성하려면 `javax.annotation.processing` 패키지의 API를 사용해야 합니다.

핵심 클래스 및 인터페이스는 다음과 같습니다.

  • Processor 인터페이스
    • 모든 애노테이션 프로세서는 이 인터페이스를 구현해야 합니다. 대부분은 AbstractProcessor 클래스를 확장하여 구현합니다.
  • ProcessingEnvironment 인터페이스
    • 컴파일러와 애노테이션 프로세서 간의 통신을 위한 환경을 제공합니다.
    • 소스 코드 생성 도구(Filer)와 메시지 출력 도구(Messager)를 포함합니다.
  • RoundEnvironment 인터페이스
    • 컴파일러가 처리 중인 애노테이션 및 소스 코드 정보를 제공합니다.

 

 

바이트코드 조작

Lombok은 바이트코드를 직접 변경하는 것은 아니고, 컴파일러의 AST(Abstract Syntax Tree)를 조작하여 결과적으로 수정된 바이트코드를 생성합니다. 이게 무슨 말일까요?

 

Java에서 AST(Abstract Syntax Tree)는 컴파일러가 소스 코드를 분석할 때 생성하는 구조화된 트리 형태의 데이터 구조를 말합니다.

이 트리는 소스 코드의 문법 요소(클래스, 메서드, 변수 등)를 계층적으로 표현하며, 컴파일러는 이 AST를 기반으로 바이트코드(.class 파일)를 생성합니다. 

 

그러니까, Lombok이 "AST를 조작한다"는 말은 컴파일러가 생성한 이 구문 트리에 접근하여 수정하거나 요소를 추가한다는 의미겠죠. 예를 들면, @Getter 애노테이션이 붙은 필드에 대해 Getter 메서드 노드를 트리에 삽입하겠고 그렇게 조작된 AST를 컴파일러가 바이트코드(.class 파일)로 변환하면서 실제로 동작하는 코드가 만들어집니다.

 

말로만 얘기해서는 AST에 대해 이해하기가 조금 난해합니다. 다음 코드를 보시죠!

 

Java 코드

public class Member {
    private String name;

    public String getName() {
        return name;
    }
}

AST

- Class: Member
  - Field: name (Type: String, Modifier: private)
  - Method: getName (Type: String, Modifier: public)
    - Return: name
  • 이게 바로 AST입니다. 컴파일러는 이 AST를 사용하여 소스 코드의 문법 오류를 검증하고 바이트코드를 생성해 냅니다.

Lombok의 동작 방식: AST 조작

그러니까 엄밀히 따져서 Lombok은 바이트코드를 조작하는 것이 아니라, AST를 조작한다고 보면 되겠죠. Lombok은 애노테이션 프로세서로 동작하며, 컴파일러가 AST를 생성하는 과정에서 해당 트리를 수정합니다. 이를 통해 추가적인 코드를 자동으로 삽입하거나 수정합니다. 

 

그러니까, Lombok@Getter와 같은 애노테이션을 감지하고, 이를 기반으로 AST에서 필드에 대응하는 Getter 메서드 노드를 삽입합니다. 최종적으로 조작된 AST는 컴파일러가 다시 바이트코드로 변환하구요.

 

예를 들어볼까요? 다음 소스 코드를 보시죠.

package cwchoiit;

public class Member {
    @Getter
    private String name;
}
  • 컴파일러가 AST를 생성합니다. Member 클래스와 name 필드를 포함하는 트리 구조를 생성하겠죠. 바로 아래와 같이요.
- Class: Member
  - Field: name (Type: String, Modifier: private)
  • 그 후 Lombok 애노테이션 프로세서가 AST를 조작합니다. 
  • Lombok@Getter 애노테이션이 붙은 필드를 감지합니다. 그리고 그 필드에 대한 GettergetName() 메서드의 노드를 AST에 삽입합니다. 그래서 아래와 같이 조작된 AST가 만들어집니다!
- Class: Member
  - Field: name (Type: String, Modifier: private)
  - Method: getName (Type: String, Modifier: public)
    - Return: name
  • 이제 컴파일러가 다시 이 AST를 통해 바이트코드를 생성해 냅니다. 
  • 그래서, 결과 바이트코드는 getName() 메서드를 포함하게 되죠.

 

정리를 하자면

Lombok의 동작원리를 살펴보았습니다. 핵심 키워드는 [애노테이션 프로세싱]과 [AST 조작]이라고 할 수 있는데요. 이 과정을 통해 지루한 반복 코드를 깔끔하게 애노테이션으로 해결할 수가 있게 됐습니다. 물론, Lombok에 관련된 논란 거리도 있습니다만, 전 Lombok이 좋네요. 

 

 

나도 한번 만들어볼까?

이해를 해봤으니, 직접 다뤄볼까요? 간단하게라도 직접 만들어보기까지 한다면 조금 더 이 과정이 이해될 것 같습니다.

`chyonibok` 이라는 이름의 프로젝트를 하나 만들고 다음과 같이 코드를 작성했습니다.

package cwchoiit;

public class Member {
    @AutoGetter
    private String name;
}
  • 지금 저 @AutoGetter 애노테이션은 아무런 동작도 하지 않습니다. 심지어 컴파일 오류가 납니다. 이렇게 만든 Member라는 클래스가 @AutoGetter 애노테이션이 달린 필드를 보고 해당 필드의 Getter 메서드를 자동으로 만들어주는 그 작업을 한번 해보겠습니다.

 

그러기 위해 프로젝트를 하나 더 만들 생각입니다. 라이브러리 역할을 하는. 그래서 새로 만든 프로젝트의 이름을 `chyoniboklib`라고 하고 새로 만들었습니다. 그리고 다음과 같이 애노테이션을 하나 생성했습니다.

package cwchoiit;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.SOURCE)
public @interface AutoGetter {
}
  • RetentionPolicySOURCE일까요? 이 애노테이션이 런타임이나 바이트코드에 필요할까요? 아닙니다. 컴파일 시에 해당 애노테이션을 찾아 후처리를 하고 AST 조작을 하면 끝나기 때문에 런타임이나 바이트코드에 필요하지가 않습니다. 따라서 CLASS, RUNTIME이 아닌 SOURCE로 지정했습니다.

 

애노테이션 프로세서 구현

자, 이제 애노테이션 프로세서를 구현해야 합니다. 아까 위에서도 얘기했지만, Processor를 직접 구현해도 된다만, 이미 여러 기능이 있는 AbstractProcessor를 구현하는게 일반적입니다. 따라서 저도 그렇게 구현해보도록 하겠습니다.

package cwchoiit;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.element.TypeElement;
import java.util.Set;

public class AutoGetterProcessor extends AbstractProcessor {

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        return false;
    }
}
  • 반드시 구현해야 할 메서드는 `process` 딱 하나입니다. 
  • 그 다음 두가지 애노테이션을 붙여주겠습니다. 아래와 같이요.
package cwchoiit;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.TypeElement;
import java.util.Set;

@SupportedAnnotationTypes("cwchoiit.AutoGetter")
@SupportedSourceVersion(SourceVersion.RELEASE_11)
public class AutoGetterProcessor extends AbstractProcessor {...}
  • 이 애노테이션 프로세서가 지원하는 애노테이션이 어떤 애노테이션인지를 알려주는 @SupportedAnnotationTypes 입니다. 당연히 위에서 만든 @AutoGetter입니다.
  • 그 다음은 지원하는 버전입니다. 최소 해당 버전 이상은 되어야 한다는 이야기고 여기서는 11버전을 작성했습니다. 참고로 이 애노테이션을 사용 안하면 AbstractProcessor가 가지고 있는 getSupportedSourceVersion()을 사용합니다.

 

자 그럼, 이제 애노테이션 프로세서를 직접 만들어 보겠습니다. 우선 구현해야 하는 메서드가 `process()`라고 했죠? 이건 뭐하는 앨까요?

이 메서드는 만약 `true`를 리턴하면, 여기서 이 애노테이션을 처리를 다 했다고 판단하고 다음 애노테이션 프로세서에게 넘기지 않습니다. 그러니까 만약, @AutoGetter를 처리하는 애노테이션 프로세서가 딱 이 하나라면 `true`를 리턴하면 되겠죠? 반면에, 또 다른 애노테이션 프로세서가 있고 그 프로세서 역시 @AutoGetter를 처리하는 또다른 애노테이션 프로세서라면 `false`를 리턴해서 두 프로세서 모두 통과하도록 해줘야 합니다. 그래서 저는 `true`를 리턴하도록 하겠습니다. 어차피 이거 하나만 만들거니까요.

package cwchoiit;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic;
import java.util.Set;

@SupportedAnnotationTypes("cwchoiit.AutoGetter")
@SupportedSourceVersion(SourceVersion.RELEASE_11)
public class AutoGetterProcessor extends AbstractProcessor {

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(AutoGetter.class);
        for (Element element : elements) {
            if (element.getKind() != ElementKind.FIELD) {
                processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "@AutoGetter 애노테이션은 필드에만 사용할 수 있습니다. 현재 사용 위치:" + element);
            } else {
                processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "@AutoGetter 처리 대상 필드: " + element.getSimpleName());
            }
        }

        return true;
    }
}
  • 우선, 처음으로 할 일은 이 애노테이션의 검증입니다. 위에서 애노테이션 프로세서가 하는 역할 중에 애노테이션의 검증도 있다고 했죠? 적절한 곳에 애노테이션이 위치했는지 파악을 해야 합니다. 따라서 먼저 ElementKind.FIELD가 아닌 경우 컴파일 오류를 뱉어내도록 합니다. 
  • 일단 이 상태로만 마치고 애노테이션 프로세서를 등록해볼까요? 하나씩 하나씩 나아가면 좋으니까요.

애노테이션 프로세서는 어떻게 등록할까요? 원래는 이렇게 해야합니다.

  • main 하위에 `resources` 폴더를 만들고, 해당 폴더 하위에 META-INF/services 폴더를 만들어서 그 안에 `javax.annotation.processing.Processor`라는 파일을 만들어야 합니다.
  • 그리고 이 파일 안에 지금 제가 만든 애노테이션 프로세서의 풀 패키지명을 작성해주면 됩니다. 다음과 같이요.
cwchoiit.AutoGetterProcessor

 

그런 다음에 이 상태에서 빌드를 해볼까요? 다음 명령어를 실행해봅니다.

mvn clean install

 

잘 될까요? 안 됩니다. 다음과 같이 에러를 마주하게 됩니다.

이 에러가 발생하는 이유는, 지금 이 Maven으로 빌드를 하는 과정중에 즉, 소스를 컴파일 하는 시점에 이 프로세서가 동작을 하려고 합니다. 왜냐하면, 제가 프로세서로 등록을 했으니까요. 그런데 지금 최초 빌드이죠? 그럼 이 애노테이션 프로세서가 등록이 안 된 상태겠죠? 그래서 없다고 하는겁니다. 그래서 이 경우에는 어떻게 해야하냐면, 먼저 저 resources에 등록한 애노테이션을 잠깐 주석 처리하고 빌드를 합니다. 그러면 빌드가 끝나면 애노테이션 프로세서가 만들어지겠죠? 그리고 다시 `mvn install`을 하는겁니다.

 

그래서 우선, 애노테이션 프로세서를 등록한 것을 주석 처리 후 빌드를 먼저 합니다. 그럼 다음과 같이 정상적으로 빌드가 끝납니다.

 

그리고 컴파일된 파일들을 보시면, 다음과 같이 `AutoGetterProcessor`가 만들어졌죠?

이 상태에서 `mvn install`을 하면 됩니다. 그런데, 굉장히 불편하죠? 그래서 이런 불편함을 해결해주기 위한 라이브러리가 하나 있습니다. 바로 구글에서 만든 `auto-service`인데요. 한번 사용해보겠습니다. 다음과 같이 의존성을 추가해 줍니다.

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

 

그리고, 아까 만든 애노테이션 프로세서에 가서, 이런 애노테이션 `@AutoService(Processor.class)` 을 추가해줍니다.

@AutoService(Processor.class)
@SupportedAnnotationTypes("cwchoiit.AutoGetter")
@SupportedSourceVersion(SourceVersion.RELEASE_11)
public class AutoGetterProcessor extends AbstractProcessor {...}
  • 이 녀석을 추가하면, 아까 만든 `META-INF/services/javax.annotation.processing.Processor` 이 파일을 컴파일 하고 자동으로 만들어 주고, 그 파일 안에 이 애노테이션 프로세서를 등록해줍니다.

잘 되는지 확인하기 위해, 다시 `mvn clean install`을 해보면 정상적으로 빌드가 됩니다. 근데 눈으로 봐야 믿을 수 있잖아요 우리들은? 그래서 만들어진 `.jar`파일을 `.zip` 파일로 바꾼 다음에 안에 파일을 까볼까요? 

까보면, 이렇게 잘 만들어진 것을 확인할 수 있네요!

 

 

그럼 실제로 이 애노테이션 프로세서가 잘 동작하는지 확인해보겠습니다. 방금 다음 명령어를 입력했죠.

mvn clean install

이렇게 `install`을 하면 로컬 레포지토리에 해당 `.jar` 파일이 추가가 되잖아요? 

 

그 말은 다른 프로젝트에서 이 `.jar`파일을 다운받아 사용할 수 있다는 말입니다. 한번 아까 최초에 만든 `chyonibok`으로 다시 돌아가볼까요? 그 전에 pom.xml 파일에서 이 부분만 복사해서 돌아가죠. 내려받아야 하니까요.

 

그리고, 다시 돌아간 프로젝트에서 저 라이브러리를 추가해봅니다.

 

추가하면, 다음과 같이 외부 라이브러리에 잘 추가가 된 모습을 볼 수 있어요!

 

 

이제 아까 컴파일 오류가 났었던 @AutoGetter를 사용한 Member 클래스로 가볼까요? 이젠 컴파일 오류가 나지 않습니다!

그리고 이 상태에서 빌드를 해보시면 아무런 문제가 없을거에요. 왜냐하면 지금 애노테이션이 필드에 잘 붙어있으니까요. 그러나, 이 애노테이션을 클래스 레벨에 붙이면 이제 컴파일 오류를 보실 수 있습니다.

 

다음과 같이 클래스 레벨에 붙여주세요.

빌드를 하니 이런 예쁜 에러가 발생하네요! 😆

 

 

 

이제, 애노테이션 프로세서의 동작은 확인을 해봤습니다. 여기서 끝내면 안되죠?! 이제 실제로 Getter 메서드를 만들어 볼거예요!

Javapoet

이제 메서드를 동적으로 만들어야 합니다. 메서드나 클래스를 동적으로 만들어낼 때 자주 사용되는 라이브러리인 `javapoet`을 사용해볼게요.

package cwchoiit;

import com.google.auto.service.AutoService;
import com.squareup.javapoet.*;

import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.*;
import javax.tools.Diagnostic;
import java.io.IOException;
import java.util.Set;

@AutoService(Processor.class)
@SupportedAnnotationTypes("cwchoiit.AutoGetter")
@SupportedSourceVersion(SourceVersion.RELEASE_11)
public class AutoGetterProcessor extends AbstractProcessor {

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(AutoGetter.class);
        for (Element element : elements) {
            if (element.getKind() != ElementKind.FIELD) {
                processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "@AutoGetter 애노테이션은 필드에만 사용할 수 있습니다. 현재 사용 위치:" + element);
            } else {
                processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "@AutoGetter 처리 대상 필드: " + element.getSimpleName());
            }

            VariableElement variableElement = (VariableElement) element; // 필드로 캐스팅
            TypeElement enclosingClass = (TypeElement) variableElement.getEnclosingElement(); // 부모 클래스
            String fieldName = variableElement.getSimpleName().toString(); // 필드 이름 
            TypeName fieldType = TypeName.get(variableElement.asType()); // 필드 타입

            String className = enclosingClass.getSimpleName().toString(); // 기존 클래스 명 
            String packageName = processingEnv.getElementUtils() // 기존 패키지 명
                    .getPackageOf(enclosingClass)
                    .getQualifiedName()
                    .toString();

            // Getter 메서드 생성
            MethodSpec getterMethod = MethodSpec.methodBuilder("get" + capitalize(fieldName))
                    .addModifiers(Modifier.PUBLIC)
                    .returns(fieldType) // 반환 타입 설정
                    .addStatement("return this.$L", fieldName) // 메서드 본문
                    .build();

            // 새 클래스 생성
            TypeSpec newClass = TypeSpec.classBuilder(className + "AutoGenerated")
                    .addModifiers(Modifier.PUBLIC)
                    .addField(fieldType, fieldName, Modifier.PRIVATE) // 기존 필드 추가
                    .addMethod(getterMethod) // Getter 메서드 추가
                    .build();

            Filer filer = processingEnv.getFiler();

            try {
                JavaFile.builder(packageName, newClass)
                        .build()
                        .writeTo(filer);
            } catch (IOException e) {
                processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Fatal: " + e.getMessage());
            }
        }

        return true;
    }

    private String capitalize(String name) {
        return Character.toUpperCase(name.charAt(0)) + name.substring(1);
    }
}
  • 우선, 해당 필드의 Getter 메서드를 만들어야겠죠?
MethodSpec getterMethod = MethodSpec.methodBuilder("get" + capitalize(fieldName))
                                    .addModifiers(Modifier.PUBLIC)
                                    .returns(fieldType) // 반환 타입 설정
                                    .addStatement("return this.$L", fieldName) // 메서드 본문
                                    .build();
  • 현재 필드의 필드 이름을 가지고 Getter 메서드를 만듭니다. MethodSpecjavapoet에서 제공해주는 메서드를 동적으로 만들어주는 녀석입니다. 이렇게 Getter 메서드를 동적으로 만들 수가 있습니다.

그런데, 사실 Lombok은 AST를 조작한다고 했잖아요? 이 AST를 조작해서 기존 클래스에 메서드를 추가하는 방식을 Lombok은 컴파일러 내부 API를 사용합니다. 그런데 말이죠. 컴파일러 내부 API는 공개 API가 아닙니다. 쉽게 말해 저희같은 일반인들은 가져다가 사용하는게 거의 불가능에 가깝죠. 과거 JDK8 이전에는 `tools.jar` 파일에 컴파일러 내부 API가 포함되어 있었다고 합니다. 그런데 JDK9 이상부터는 모듈로 전환이 되고, 해당 모듈은 기본적으로 공개되지 않습니다. 그래서, 기존 클래스에 추가하는 방식 말고 아예 새로운 클래스를 만들고 그 클래스에 Getter를 추가한 클래스를 만들어 볼게요. 결국 이 과정도 애노테이션 프로세서를 이용하는 것이니까요.

// 새 클래스 생성
TypeSpec newClass = TypeSpec.classBuilder(className + "AutoGenerated")
        .addModifiers(Modifier.PUBLIC)
        .addField(fieldType, fieldName, Modifier.PRIVATE) // 기존 필드 추가
        .addMethod(getterMethod) // Getter 메서드 추가
        .build();
  • `javapoet`으로 클래스를 만드려면 TypeSpec을 사용하면 됩니다. 기존 클래스명에 `AutoGenerated`라는 이름을 추가해볼게요.
  • 그리고 여기에 addMethod()로 아까 위에서 만든 메서드를 추가해주면 됩니다.
  • 여기까지만하면 실제로 만든건 아니고 메모리에 클래스와 메서드를 만들어 낸것까지 한거예요. 우리는 메모리에만 둥둥 떠있는게 아니라 실제 클래스로 만들어야겠죠?
Filer filer = processingEnv.getFiler();

try {
    JavaFile.builder(packageName, newClass)
            .build()
            .writeTo(filer);
} catch (IOException e) {
    processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Fatal: " + e.getMessage());
}
  • 그래서, JavaFile로 해당 클래스가 있던 패키지 위치에 새로 만든 클래스를 만들어 줍니다. 

 

이제, 다 끝났습니다. 이 소스를 다시 `install`해서 `chyonibok` 프로젝트에서 사용해볼게요. 다시 설치하면 사용했던 프로젝트에서도 다시 로드를 해줘야해요. 그래서 `chyonibok` 프로젝트에서 다음과 같이 리로드를 해주세요.

 

리로드가 잘 됐으면, 다시 다음 명령어를 실행해볼까요? 다시 `chyonibok` 프로젝트를 컴파일해서 애노테이션 프로세서가 동작해야겠죠?

mvn clean compile

 

컴파일한 후, 만들어진 `target` 폴더에 어떤 클래스가 있는지 보면 놀랄겁니다!

이렇게 `MemberAutoGenerated`라는 클래스가 생겨버렸어요! `chyoniboklib` 프로젝트로 만든 애노테이션 프로세서가 잘 동작해서 이 라이브러리를 가져다가 사용하는 `chyonibok` 프로젝트에서 컴파일 시 해당 애노테이션 프로세서가 동작한거예요! 이 코드 한번 볼까요? Getter가 아주 이쁘게 만들어졌습니다.

 

가져다가 사용하는 것도 당연히 되겠죠? 한번 해볼까요? 

  • 문제없이 잘 가져다가 사용할 수가 있습니다. (참고로, 이게 사용이 안된다면 IDE에서 Enable annotation processing을 체크해줘야 합니다)

 

정리

Lombok의 동작 원리와 실제로 비슷하게나마 Lombok의 구현을 해봤습니다. 사실 Lombok은 새로운 클래스를 만들지는 않습니다. 기존 클래스에 메서드를 붙여버립니다. 그 방식을 AST 조작을 통해서 해내는 것이구요. 이 포스팅에서는 AST 조작 대신 새로운 클래스를 만들어내고 그 클래스에 Getter 메서드를 만들어봤습니다. 저는 처음에 Lombok의 동작 원리를 이해해보니 이렇게까지 깊은 내용이구나.. 싶으면서도 또 다른 세계가 펼쳐진 것 같아서 재밌었습니다. 

 

그럼, AST 조작을 하는건 거의 불가능에 가깝잖아요? 자바 컴파일러 내부 API를 사용하니까요. 그럼 바이트코드 조작을 해서 기존 클래스에 Getter 메서드를 추가하는건 어떨까요?! 재밌을 것 같지 않나요?

728x90
반응형
LIST

'JAVA의 가장 기본이 되는 내용' 카테고리의 다른 글

[Java 8] CompletableFuture  (0) 2024.11.30
[Java 8] Optional  (0) 2024.11.27
[Java 8] Stream API  (0) 2024.11.27
[Java 8] 함수형 인터페이스와 람다 표현식  (0) 2024.11.25
애노테이션  (6) 2024.10.21

+ Recent posts