Post

[Java] Lombok

[Java] Lombok

📌 Lombok이란?

Lombok 이란 어노테이션 기반으로 자바 코드를 자동으로 생성해주는 라이브러리이다. 이를 통해 코드가 간결해지며 가독성을 향상시킬 수 있다. 또한 반복적으로 작성되는 보일러플레이트 코드가 감소하게 된다.

이는 개발자가 핵심 비즈니스 로직에 더 집중할 수 있도록 하는 효과를 가져오며, 유지보수가 쉬워진다.


실제로 Lombok을 사용하기 전과 후의 차이점을 살펴보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Person {
    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 메서드의 수가 증가하게 된다. 반복적인 코드의 작성이 증가한다는 뜻이다.

1
2
3
4
5
6
7
8
9
10
import lombok.Getter;
import lombok.Setter;

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

Lombok을 사용하게 되면 어떻게 코드가 간결해질까? @Getter, @Setter 어노테이션을 추가하면 컴파일 시점에 자동으로 getter/setter 메서드가 생성된다.

📌 설정 방법

  • Gradle
1
2
3
4
5
6
dependencies {
    implementation 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.projectlombok:lombok'
    testAnnotationProcessor 'org.projectlombok:lombok'
}
  • Maven
1
2
3
4
5
6
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.16</version>
    <scope>provided</scope>
</dependency>

📌 동작 원리

코드에 어노테이션을 붙이게 되면 컴파일 시점에 해당 어노테이션을 감지하여 필요한 코드를 자동으로 생성한다. javac(자바 컴파일러)에는 Annotation Processing 단계가 존재한다. 이 단계에서 Lombok의 Annotation Processor 가 동작하여 코드를 파싱하여 만든 AST(Abstract Syntax Tree) 를 동적으로 수정한다. 수정된 AST를 기반으로 javac가 최종 바이트코드(.class)를 생성한다.

📌 주요 Lombok 어노테이션

@Getter/Setter

클래스의 필드에 대한 getter/setter 메서드를 자동으로 생성한다.

1
2
3
4
5
6
7
8
// 클래스 전체에 적용
@Getter
@Setter
public class Member {
    private String id;
    private String name;
}

@AllArgsConstructor

클래스의 모든 필드를 파라미터로 받는 생성자를 자동으로 생성한다.

1
2
3
4
5
6
7
8
9
10
11
12
@AllArgsConstructor
public class Employee {
    private String name;
    private int salary;
}

// 생성되는 생성자
public Employee(String name, int salary) {
    this.name = name;
    this.salary = salary;
}

@NoArgsConstructor

파라미터가 없는 기본 생성자를 자동으로 생성한다.

1
2
3
4
5
6
7
8
9
10
@NoArgsConstructor
public class Employee {
    private String name;
    private int salary;
}

// 생성되는 생성자
public Employee() {
}

@RequiredArgsConstuctor

클래스 내에서 초기화되지 않은 final 필드와 @NonNull 이 붙은 필드만을 파라미터로 받는 생성자를 자동으로 생성한다.

1
2
3
4
5
6
@RequiredArgsConstructor
public class UserService {
    private final UserRepository userRepository;
    private String name;
}

위 코드에서 userRepository 만 생성자의 파라미터로 포함된다.

@ToString

클래스의 모든 필드를 기반으로 toString() 메서드를 자동으로 생성한다. toString()은 객체를 읽을 수 있는 문자열로 변환하는 메서드이다.

exclude 속성을 사용하여 특정 필드를 toString() 결과에서 제외할 수 있으며, 또는 @Tostring.Exclude 어노테이션을 직접 붙여 제외할 수 있다.

@ToString(callSuper = true) 를 사용하면 부모 클래스의 toString() 결과도 함께 포함할 수 있다. 그러나 객체 간 순환 참조가 있을 경우 무한 루프가 발생할 수 있다.

@EqualsAndHashCode

클래스에 equals()hashCode() 메서드를 자동으로 생성한다. equals()는 두 객체가 논리적으로 동등한지 비교하며, hashCode()는 객체를 해시 기반 자료구조에서 사용할 때 객체의 해시값을 리턴한다.

두 객체를 비교할 때, 먼저 hashcode를 비교한다. hashcode가 다르다면 두 객체는 명백하게 다르나, 같다면 equals()를 통해 두 객체가 같은지 비교한다.

@Data

@Data 어노테이션을 사용하면 @Getter, @Setter, @ToString, @EqualsAndHashCode, @RequiredArgsConstructor 어노테이션을 한 번에 적용할 수 있다. 그러나 각각의 어노테이션에 대한 세부적인 옵션을 적용할 수 없으므로 실무에서 잘 사용하지 않는 편이다.

@Builder

객체를 생성하기 위한 빌더 패턴을 자동으로 구현해준다. 빌더를 통해 객체를 생성할 때 각 필드의 값을 메서드 체이닝 방식으로 지정할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
@Builder
public class Person {
    private final String name;
    private final int age;
}

Person person = Person.builder()
    .name("Alice")
    .age(25)
    .build();

@Builder 에 대해 더 자세한 내용은 여기를 참고한다.

📌 주의사항

  • 무분별한 Setter, Data, AllArgsConstructor 등과 같은 어노테이션 사용을 지양하자.
    • @Data 는 불필요한 setter의 노출, 순환 참조, 예상치 못한 동작과 같은 문제가 발생할 수 있다.
    • @Setter 는 객체의 불변성을 해칠 수 있다.
    • @AllArgsConstructor필드 순서가 바뀌거나 필드가 추가 또는 삭제될 때 객체 생성 오류가 발생할 수 있다. 따라서 명확하게 생성자를 작성하는 것이 좋다.

  • 순환 참조가 발생하는 상황을 조심하자. 양방향 연관관계가 있는 두 엔티티에 @ToString 어노테이션을 사용하면 StackOverflowError 가 발생할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Entity
@Getter
@ToString
public class Team {
    @Id
    @GeneratedValue
    private Long id;
    private String name;

    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();
}

@Entity
@Getter
@ToString
public class Member {
    @Id
    @GeneratedValue
    private Long id;
    private String username;
    
    @ManyToOne
    @JoinColumn(name = "team_id")
    private Team team;
}

team 객체에서 toString 메서드를 호출하게 되면, 내부적으로 members 리스트를 출력한다. 이 과정에서 member 객체에서 toString 메서드를 호출하고, 내부적으로 team 을 출력한다. 이 과정이 무한히 반복되면서 에러가 발생하게 된다.

1
2
3
4
5
6
@Entity
@Getter
@ToString(exclude = "members")
public class Team {
    // ...
}

순환 참조 문제를 해결하기 위해 exclude 옵션에 순환 참조의 원인이 되는 필드를 제외한다.

1
2
3
4
5
6
7
8
9
10
11
@Entity
@Getter
@ToString
public class Team {
    // ...
    
    @ToString.Exclude
    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();
}

또는 @ToString.Exclude 를 통해 해당 필드를 toString 메서드에서 제외한다.

1
2
3
4
5
6
7
@Entity
@Getter
@ToString(of = {"id", "name"})
public class Team {
    // ...
}

exclude 옵션과 반대로 of 옵션을 사용하면 toString 메서드에 포함할 필드면 명시적으로 지정할 수 있다.


  • @Builder@NoArgsConstructor 또는 직접 만든 생성자를 함께 사용하면 컴파일 에러가 발생할 수 있다. 이는 Lombok의 동작 방식과 연관이 있는데, 개발자가 명시적으로 생성자 관련 어노테이션을 추가하면 @Builder 를 위한 모든 필드 생성자를 자동으로 만들지 않기 때문이다.

즉, @Builder 만 단독으로 사용하는 경우 모든 필드 생성자를 생성하지만, @NoArgsConstructor 와 함께 사용한다면 인자가 없는 기본 생성자만 존재하게 된다. 따라서 @Builder 는 필요한 생성자를 찾지 못하고, 컴파일 에러가 발생하는 것이다.

모든 필드 생성자를 명시적으로 추가하면 문제를 해결할 수 있다.


  • 사용할 어노테이션만 명시하고, 필요한 부분에만 명시하자.

📌 참고

https://lucas-owner.tistory.com/26#google_vignette

https://mangkyu.tistory.com/78

https://jake-seo-dev.tistory.com/70

This post is licensed under CC BY 4.0 by the author.