Spring Boot

Lombok

iksadnorth 2023. 7. 29. 22:42

👣 개요

Java 개발 중에 단순 노동에 가까운 코드들이 존재한다.
예를 들어, Getter, Setter, Hashcode, toString 등등의 코드들은 번거롭고 반복되는 코드의 예시라고 볼 수 있다.
이러한 코드들은 가독성도 해치고 많은 Cost이 소요될 수 밖에 없다.
이러한 코드를 손쉽게 처리해주는 라이브러리가 Lombok이다.

Lombok은 컴파일 시점에서 바이트코드를 변환하여 원하는 부분을 주입해주는 방식으로 동작한다.
Lombok이 처리되는 과정은 아래와 같다.

  1. 자바 컴파일러는 소스파일을 파싱하여 AST트리를 만듭니다.
  2. Lombok은 AnnotationProcessor에 따라 AST트리를 동적으로 수정하고 새 노드를 추가하고 마지막으로 바이트 코드를 분석 및 생성합니다.
  3. 최종적으로 자바 컴파일러는 Lombok Annotation Processor에 의해 수정된 AST를 기반으로 Byte Code를 생성합니다.

👣 AnnotationProcessor

Annotation Processor란 자바 컴파일러 플러그인의 일종으로, 어노테이션에 대한 코드베이스를 검사, 수정, 생성하는 훅입니다. 컴파일 에러나 컴파일 경고를 만들어내거나, 소스코드(.java)와 바이트코드(.class)를 내보내기도 한다.

즉, Annotation을 사용하려면 AnnotationProcessor가 필요하다.

Annotation Processor의 동작 구조는
1. 어노테이션 프로세서를 사용한다는 것을 자바 컴파일러가 알고 있는 상태에서 컴파일을 수행한다.
2. 어노테이션 프로세서들이 각자의 역할에 맞게 구현되어 있는 상태에서 실행되지 않은 어노테이션 프로세서를 실행한다.
3. 어노테이션 프로세서 내부에서 어노테이션에 대한 처리를 한다.
4. 자바 컴파일러가 모든 어노테이션 프로세서가 실행되었는지 검사하고,
모든 어노테이션 프로세서가 실행되지 않았다면 반복한다.

👣 각 문법

1. @Getter, @Setter

public class Person {
    private String name;
    
    public String getName() {
        return name;
    }
    
    public void setName(String name) {
        this.name = name;
    }
}

2. @ToString

public class Person {
    private String name;
    private int age;
    
    @Override
    public String toString() {
        return "Person [name=" + name + ", age=" + age + "]";
    }
}

3. @EqualsAndHashCode

public class Person {
    private String name;
    private int age;
    
    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null || getClass() != obj.getClass())
            return false;
        Person other = (Person) obj;
        return Objects.equals(name, other.name) && age == other.age;
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
}

4. @NoArgsConstructor, @AllArgsConstructor

public class Person {
    private String name;
    private int age;
    
    public Person() {
        // 기본 생성자
    }
    
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

5. @Data

@Getter, @Setter, @ToString, @EqualsAndHashCode, @RequiredArgsConstructor 모두를 포함한 Annotation.

6. @Builder

public class Person {
    private String name;
    private int age;
    
    private Person(Builder builder) {
        this.name = builder.name;
        this.age = builder.age;
    }
    
    public static class Builder {
        private String name;
        private int age;
        
        public Builder name(String name) {
            this.name = name;
            return this;
        }
        
        public Builder age(int age) {
            this.age = age;
            return this;
        }
        
        public Person build() {
            return new Person(this);
        }
    }
}

7. @Slf4j

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Example {
    private static final Logger log = LoggerFactory.getLogger(Example.class);
}

8. @UtilityClass

유틸리티 클래스란 주로 정적(static) 메서드를 가지고 있고, 객체의 상태를 가지지 않는 클래스를 말한다.
@UtilityClass
를 클래스에 적용하면, 해당 클래스를 더 편리하게 유틸리티 클래스로 만들어 준다.
@UtilityClass를 적용한 클래스는 다음과 같은 특징을 가진다.

  1. 모든 필드가 private static final로 설정된다.
  2. 모든 메서드가 public static으로 설정된다.
  3. 생성자는 private으로 설정되어 객체 인스턴스의 생성을 방지한다.
  4. 클래스는 final로 설정되어 상속이 불가능하다.
public final class MyUtils {
    private static final String arg = "some config value";

    private MyUtils() {}
    private MyUtils(String arg1) {}
    
    public static String concatenate(String str1, String str2) {
        return str1 + str2;
    }

    public static int add(int a, int b) {
        return a + b;
    }
}

9. @RequiredArgsConstructor

해당 클래스의 모든 final 필드나 @NonNull으로 표시된 필드들에 대해 생성자를 생성한다.
이 방법은 생성자 주입을 통해 Bean 객체를 부여할 수 있어서 권장되는 방법이기도 하다.

public class Person {
    private final String name;
    private int age;

    public Person(String name) {
        this.name = name;
    }
}

 

👣 Bean Injection - [@Autowired VS @RequiredArgsConstructor]

Spring 개발팀에서는 생성자 주입을 통한 Bean 주입을 권장하고 있다.
왜냐하면 다음과 같은 이유에서다.

1. 객체 불변성 보장

@Autowired 같은 경우엔, 필드 주입을 받기 위해서는 필드에 final 적용시킬 수 없다.
만약 필드 주입을 final 필드에 사용하면 컴파일 오류가 발생하게 된다.
때문에 생성자 주입을 통한 DI를 권장하고 있다.
@RequiredArgsConstructor 같은 경우엔, final이 부여된 필드에만 생성자를 만들 뿐 아니라
생성자 주입으로 Bean 객체를 전달하므로 객체 불변성 보장을 보장한다.

2. 순환 참조에 대한 안정성

생성자 Annotation을 이용하는 경우, 순환 참조가 발생한다면 컴파일 단계에서 오류 메시지를 띄운다.
때문에 build 과정에서 오류를 찾아낼 수 있어서 더 안전한 방법이다.

'Spring Boot' 카테고리의 다른 글

Spring Boot 전역 예외 처리  (0) 2023.07.30
@Transactional  (0) 2023.07.30
SpEL - Spring Expression Language  (0) 2023.07.29
Spring Boot AOP  (0) 2023.07.29
Spring Boot Bean 등록 방법 5가지  (0) 2023.07.29