[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