4 분 소요

자바에서 코드를 작성할 때 반복적으로 작성해야 되는 코드로 인해 불편함을 느끼신 적 있나요?
저는 gettter, setter 메서드들에서 특히 그런 느낌을 받았습니다.
기능적으로 필요한 코드들이지만 같은 유형의 반복적인 코드와 작성한 클래스 파일의 스크롤도 길어지는 것이 불편했습니다.

Lombok은 이런 불편함을 해소해주는 라이브러리로 이미 많은 개발자들이 사용하고 있습니다.
이번에는 Lombok에 대해 알아보려고 합니다.

불편한 Member 클래스

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

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }
    
    public void setName(String name) {
        this.name = name;
    }
    
    public void setAge(int age) {
        this.age = age;
    }
}

간단한 예제용 Member 클래스입니다.
이름과 나이를 가지고 있으며 각각 getter와 setter를 가지고 있습니다.
만약 여기서 필드가 더 추가된다면 어떨까요?
하나의 필드가 추가될 때마다 2개의 메서드를 추가로 작성해야할 것이며 정작 이 객체가 가진 비즈니스 로직에 집중하지 못하게 됩니다.

public class Member {
    private String name;
    private int age;
    private Something field ... // 100 개가 추가된다면??

    public get... // getter, setter도 각각 100개씩 추가해야 한다!
    public set...

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }
    
    public void setName(String name) {
        this.name = name;
    }
    
    public void setAge(int age) {
        this.age = age;
    }
}

위와 같이 반복되는 코드를 보일러플레이트라고 하는데요.
IDE에서 자동으로 코드를 작성해주는 기능이 제공되기는 하지만 완전한 해결책은 아니라고 생각합니다.
Lombok을 적용하면 어떻게 될까요?

Lombok을 적용한 Member 클래스

@Getter
@Setter
public class Member {
    private String name;
    private int age;
    
    public void introduce() {
        System.out.println("안녕하세요. 저는 " + name + "이며 "+ age + " 입니다.);
    }
}

Lombok은 이를 어노테이션으로 제어해서 컴파일 시점에 자동으로 코드를 추가해줍니다.
Member 클래스가 한 눈에 보기 편해졌으며 스크롤 아래에 숨겨져 있던 로직도 찾을 수 있게 됐네요.

이렇게 편한 Lombok을 어떻게 적용하는지 알아보겠습니다.

Lombok 적용하기

image

먼저 Lombok을 사용하기 위해선 Lombok 플러그인을 추가해야 합니다.

dependencies {
    compileOnly 'org.projectlombok:lombok:1.18.26'
    annotationProcessor 'org.projectlombok:lombok:1.18.26'
}

그리고 build.gradle에 Lombok 의존성을 추가해줍니다.
여기서 annotationProcessor를 추가하지 않으면 컴파일 시점에 Lombok이 동작하지 않아 코드가 추가되지 않습니다.

intellij 설정

또 IntelliJ를 사용한다면 위와 같이 annotation processing을 활성화 시켜줘야 합니다.
여기까지 완료했다면 Lombok을 사용할 준비는 마쳤습니다.

@Getter
@Setter
public class Member {
    private String name;
    private int age;
}

Member 클래스에 Lombok이 제공하는 @Getter와 @Setter를 적용한 모습입니다.
위에서 이미 봤다시피 훨씬 깔끔해진 클래스를 확인할 수 있습니다.

컴파일 결과

컴파일 된 Member.class의 코드를 살펴보면 getter와 setter가 추가된 것을 확인할 수 있습니다.
Lombok이 어떻게 코드를 추가해주는지는 컴파일 과정을 알아봐야합니다.

자바 컴파일 과정

자바 컴파일러는 java 파일을 class 파일로 변환하는 기능을 수행합니다.
이 때 AST(Abstract Syntax Tree)를 생성하고 이를 바탕으로 컴파일을 진행하게 되는데 AST는 추상 구문 트리로 소스 코드의 구조를 추상적으로 표현하는 트리입니다.
IntelliJ에서는 JDT AST 플러그인을 설치하면 AST를 쉽게 확인할 수 있습니다.

public class Main {
    public static String hello() {
        return "Hello, world!";
    }
}

위와 같은 코드를 컴파일 하면 아래와 같은 AST를 생성합니다.

jdt_ast

천천히 확인해보면 클래스의 접근제어자와 인터페이스 여부도 있고, 메서드의 정보와 반환 하는 타입과 무엇을 반환하는지 등이 포함되어 있는 것을 확인할 수 있습니다.
그럼 컴파일러가 컴파일 과정에서 AST를 생성하고 생성된 AST를 기반으로 바이트 코드를 생성하는 것은 알겠는데 Lombok은 어떻게 컴파일 과정에서 코드를 추가해주는걸까요?

Lombok과 어노테이션 프로세서

그 전에 Lombok의 @Getter를 살펴보겠습니다.

@Target({ElementType.FIELD, ElementType.TYPE})
@Retention(RetentionPolicy.SOURCE)
public @interface Getter {
	lombok.AccessLevel value() default lombok.AccessLevel.PUBLIC;
	
	AnyAnnotation[] onMethod() default {};
	
	boolean lazy() default false;
    
	@Deprecated
	@Retention(RetentionPolicy.SOURCE)
	@Target({})
	@interface AnyAnnotation {}
}

Lombok의 @Getter 어노테이션을 살펴보면 @Retention이 SOURCE로 되어있는 것을 확인할 수 있습니다.
어노테이션을 소스 코드에만 남기고 컴파일 시점에는 제거되는 것을 의미하는데 컴파일 시점에 AST를 조작하여 새로운 코드를 추가시키기 위함입니다.
이 기능은 위에서 잠깐 언급된 어노테이션 프로세서로 구현할 수 있습니다.

image

GPT의 힘을 빌려봤는데요, 어노테이션 프로세서는 컴파일 시점에 어노테이션을 분석하고 새로운 코드를 생성하는 컴파일러 플러그인이라고 합니다.
또 Lombok만이 아니라 JPA, QueryDSL 등 다양한 라이브러리에서 어노테이션 프로세서를 이용하여 기능을 제공하는 것을 추가로 알게 되었습니다.

자바에서는 어노테이션 프로세서를 어떻게 구현하는지 알아보겠습니다.

Processor

자바에서는 Processor라는 인터페이스를 제공하는데 이 인터페이스를 구현한 클래스를 어노테이션 프로세서라고 합니다.

The interface for an annotation processor. Annotation processing happens in a sequence of rounds. On each round, a processor may be asked to process a subset of the annotations found on the source and class files produced by a prior round. The inputs to the first round of processing are the initial inputs to a run of the tool; these initial inputs can be regarded as the output of a virtual zeroth round of processing. If a processor was asked to process on a given round, it will be asked to process on subsequent rounds, including the last round, even if there are no annotations for it to process. The tool infrastructure may also ask a processor to process files generated implicitly by the tool’s operation. Each implementation of a Processor must provide a public no-argument constructor to be used by tools to instantiate the processor.

Processor의 주석을 보면 어노테이션 프로세서를 위한 인터페이스이며, 여러 라운드를 거쳐서 어노테이션을 처리한다고 합니다.

image

Lombok 또한 Processor 인터페이스를 구현하고 있는 것을 알 수 있는데요.
다만 어노테이션 프로세서는 컴파일 시점에 새로운 파일을 생성하는 데 사용할 수 있으며 Lombok은 일종의 해킹으로 AST를 조작하여 새로운 코드를 기존 파일에 추가한다고 합니다.

class AnnotationProcessorHider {
    public static class AnnotationProcessor extends AbstractProcessor {
        private final AbstractProcessor instance = createWrappedInstance();

        @Override
        public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
            return instance.process(annotations, roundEnv);
        }

        private static AbstractProcessor createWrappedInstance() {
            ClassLoader cl = Main.getShadowClassLoader();
            try {
                Class<?> mc = cl.loadClass("lombok.core.AnnotationProcessor");
                return (AbstractProcessor) mc.getDeclaredConstructor().newInstance();
            } catch (Throwable t) {
                if (t instanceof Error) throw (Error) t;
                if (t instanceof RuntimeException) throw (RuntimeException) t;
                throw new RuntimeException(t);
            }
        }
    }

    @SupportedAnnotationTypes("lombok.*")
    public static class ClaimingProcessor extends AbstractProcessor {
        @Override
        public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
            return true;
        }

        @Override
        public SourceVersion getSupportedSourceVersion() {
            return SourceVersion.latest();
        }
    }
}

Lombok의 AnnotationProcessorHider의 내부 구조를 보면 AnnotationProcessor 클래스와 ClaimingProcessor 클래스가 Processor 인터페이스를 구현하고 있습니다. 여기서 ClaimingProcessor가 @SupportedAnnotationTypes 어노테이션에 lombok.*을 지정해놓았습니다.
Lombok 어노테이션은 lombok 패키지에 위치하고 있기 때문에 이를 지정해놓은 것이며, 모든 Lombok 어노테이션이 여기서 처리됩니다.

어노테이션 프로세서를 커스텀하는 방법과 Lombok 어노테이션을 커스텀하는 방법, Lombok이 제공하는 다른 기능들은 아래 링크를 확인해주세요.

참고
https://projectlombok.org/
https://www.baeldung.com/java-annotation-processing-builder
https://www.baeldung.com/lombok-custom-annotation
https://www.javacodegeeks.com/2015/09/java-annotation-processors.html

댓글남기기