- Published on
Lombok이란?
- Authors
- Name
- 윤종원
Lombok이란?
Lombok
이란 Java의 라이브러리로 반복되는 메소드를 Annotation
을 사용해서 자동으로 작성해주는 라이브러리다. 보통 DTO나 Model, Entity의 경우 여러 속성이 존재하고 이들이 가지는 프로퍼티에 대해서 Getter수 있다.
또한 DTO와 같이 자주 변경되는 클래스의 경우 멤버 변수가 추가되거나 없어질 때마다 Getter, Setter, 생성자 등을 수정해줘야 하는 경우가 발생한다. 이러한 경우에도 Lombok
을 이용하면 단순히 프로퍼티를 추가하고 삭제하는 것만으로도 충분하다.
Lombok
을 이용해서 작성한 코드는 컴파일 과정에서 Annotation
을 이용해서 코드를 생성하고 이런 결과물이 .class
에 담기게 되는 것이다.
귀찮은 과정을 줄여주고 반복되는 코드 작성을 대신 해준다는 점에서 많은 개발자들이 선호하는 라이브러리이지만 호불호가 갈리는 라이브러리이기도 하므로 팀 프로젝트에 도입하는 경우 주의해야 한다.
또한 단순히 Annotation
을 이용해서 코드를 작성해주는 라이브러리이므로 각 API
가 어떤식으로 작동하는지 숙지한 채로 사용하는 것이 좋다. 다른 라이브러리와 충돌이 발생할 수도 있고 내가 원하지 않는 방식으로 작동할 수도 있기 때문이다.
예를 들어 @Data
나 @ToString
의 경우 순환 참조 또는 무한 재귀 호출로 인해 StackOverFlowError
가 발생할 수도 있다. 이는 아래서 자세하게 살펴보겠다.
Lombok 사용법
@Getter, @Setter
필드에 대한 getter
, setter
를 자동으로 생성해주는 Annotation
이다. 만약 필드의 이름이 name
이라면 getName()
과 setName()
을 추가해준다.
예를 들면 아래와 같다.
// Code
class Person{
@Getter
@Setter
private String name;
}
// Compiled
class Person {
private String name;
Person() {
}
public String getName() {
return this.name;
}
public void setName(final String name) {
this.name = name;
}
}
위와 같이 Annotation
이 명시된 필드에 대해 getX()
, setX()
를 추가해주는 것을 알 수 있다.
자동으로 생성되는 getter
와 setter
의 경우 기본은 public
이며 AccessLevel
을 명시한 경우 PUBLIC
, PROTECTED
, PACKAGE
, PRIVATE
등으로도 생성할 수 있다.
// Code
class Person{
@Getter(AccessLevel.PRIVATE)
@Setter(AccessLevel.PROTECTED)
private String name;
}
또한 @Getter
,@Setter
를 클래스에 명시할 수도 있다. 이 경우 모든 non-static
필드에 대해 getter
와 setter
를 추가해준다.
@Getter
@Setter
class Person{
private String name;
private int age;
}
만약 이름이 같고 매개변수의 수가 같은 메소드가 이미 존재한다면 메소드가 생성되지 않는다.
예를 들어 getName(String... names)
가 이미 존재한다면 getName()
메소드는 기술적으로 가능하더라도 생성되지 않는다. 이는 메소드 사용의 혼동을 방지하기 위해서다. 메소드가 생성되지는 않지만 이에 대해 경고 메시지로 알려준다.
또한 열거형 변수에 @Getter
는 사용할 수 있지만 @Setter
는 사용할 수 없다.
@NonNull
메소드나 생성자의 매개변수에 @NonNull
을 사용하면 lombok
이 null check를 해준다.
// code
class Person{
private String name;
private int age;
public Person(@NonNull String name, int age) {
this.name = name;
this.age = age;
}
}
// build
class Person {
private String name;
private int age;
public Person(@NonNull String name, int age) {
if (name == null) {
throw new NullPointerException("name is marked non-null but is null");
} else {
this.name = name;
this.age = age;
}
}
}
@ToString
@ToString
이 붙은 클래스는 lombok
이 toString()
메소드를 생성해준다. 기본적으로는 클래스 이름과 각 필드에 대한 값을 ,
으로 구분해서 출력해준다.
// code
@ToString
class Person{
private String name;
private int age;
}
// build
// 필요없는 부분은 생략
class Person {
private String name;
private int age;
public String toString() {
return "Person(name=" + this.name + ", age=" + this.age + ")";
}
}
includeFieldNames
를 설정하면 각 필드의 이름과 함께 값을 확인할 수 있다. true
가 기본값이다.
// code
@ToString(includeFieldNames = false)
class Person{
private String name;
private int age;
}
// build
class Person {
private String name;
private int age;
public String toString() {
return "Person(" + this.name + ", " + this.age + ")";
}
}
기본적으로 모든 non-static
필드는 toString()
에 포함되지만 원한다면 몇몇 필드는 @ToString.Exclude
를 사용해서 제외할 수 있다. 아니면 @ToString(onlyExplicitlyIncluded = true)
를 사용해서 명시적으로 @ToString.Include
가 붙은 필드만 포함시킬 수도 있다.
// code
@ToString()
class Person{
@ToString.Exclude
public static String type = "human";
private String name;
}
// build
class Person {
public static String type = "human";
private String name;
public String toString() {
return "Person(name=" + this.name + ")";
}
}
callSuper
를 설정하면 슈퍼 클래스의 toString
반환값을 포함할 수도 있다.
다른 메소드의 출력을 toString
에 포함시킬 수도 있다. 다만 매개변수가 없는 인스턴스 메소드(non-static
)만 포함시킬 수 있다. @ToString.Include
를 사용하면 된다.
// code
@ToString()
class Person{
private String name;
@ToString.Include
public String greet(){
return "Hello ";
}
}
// build
class Person {
private String name;
public String greet() {
return "Hello ";
}
public String toString() {
String var10000 = this.name;
return "Person(name=" + var10000 + ", greet=" + this.greet() + ")";
}
}
또한 @ToString.Include(name = "custom name")
를 사용해서 이름을 바꾸거나 @ToString.Include(rank = -1)
를 사용해서 출력 순서를 바꾸는 것도 가능하다. 필드의 기본 rank
는 0
이다. 높은 값을 가질 수록 먼저 출력되며 rank
가 같은 경우 소스 파일에 등장하는 순서대로 출력된다.
// code
@ToString()
class Person{
@ToString.Include(rank=-1, name="Important Field!!!!!")
private String other;
private String school;
private String name;
@ToString.Include(rank=2)
private int age;
}
// build
class Person {
private String other;
private String school;
private String name;
private int age;
public String toString() {
return "Person(age=" + this.age + ", school=" + this.school + ", name=" + this.name +
", Important Field!!!!!=" + this.other + ")";
}
}
만약 매개변수를 받지 않는 toString
메소드가 이미 존재한다면 반환 타입에 관련없이 메소드를 생성하지 않는다. 그대신 경고를 발생시킨다.
배열은 Arrays.deepToString
메소드를 사용해서 출력된다. 따라서 만약 배열이 자신을 포함하는 경우 StackOverFlowError
를 발생시킨다. Arrays.deepToString
메소드는 내부적으로 각 요소의 toString()
을 호출하게 되므로 StackOverFlowError
가 발생하는 것이다.
또한 lombok
는 각 버전 별로 toString()
출력이 같음을 보장하지 않는다. 따라서 toString()
를 파싱하는 등 API
에 의존하는 코드를 짜서는 안된다.
또한 $
로 시작하는 변수는 기본적으로는 제외한다. 포함하려면 @ToString.Include
를 명시해야만 한다.
만약 getter
가 존재하는 경우 필드에 직접 접근하지 않고 getter
를 호출한다. 만약 필드에 직접 접근하도록 하려면 @ToString(doNotUseGetters = true)
를 사용한다.
@EqualsAndHashCode
@EqualsAndHashCode
를 사용하면 lombok
이 equals(Object other)
와 hashCode()
를 만들어준다. 기본적으로 모든 non-static
, non-transient
필드를 사용하지만 @EqualsAndHashCode.Include
와 @EqualsAn로 선택할 수도 있다.
@ToString처럼
@EqualsAndHashCode(onlyExplicitlyIncluded = true)`를 사용하는 것도 가능하다.
만약 다른 클래스를 상속받는 클래스에게 @EqualsAndHashCode
를 사용한다면 동작 방식이 특이하다. 일반적으로, 다른 클래스를 상속받는 클래스에게 자동으로 equals
와 hashCode
를 생성하게 하는 것은 좋은 방의 코드까지 자동으로 생성해 줄 수는 없기 때문이다.
callSuper
를 true
로 설정하면 슈퍼클래스의 equals
와 hashCode
를 사용한다. 모든 equals
구현이 모든 상황을 적절하게 다룰 수 있는 것은 아니지만 lombok
이 만든 equals
는 모든 상황을 적절하게 다룰 수 있도록 해준다.
만약 클래스가 아무런 클래스를 상속받지 않는데 callSuper
를 true
로 설정한다면 컴파일 에러가 발생한다. 또한 클래스를 상속받는데 callSuper
를 true
로 설정하지 않는 경우 경고를 발생시킨다. 슈펴 클래스에 equals
에 사용하는 필드가 없다면 상관없지만 그렇지 않다면 슈퍼 클래스에 존재하는 필드를 비교하지 못하기 때문이다.
warning: Generating equals/hashCode implementation but without a call to superclass, even though this class does not extend java.lang.Object. If this is intentional, add '@EqualsAndHashCode(callSuper=false)' to your type.
실제로 위와 같은 경고를 발생시키며 callSuper
를 false
로 명시적으로 설정하면 경고는 사라진다.
또한 @ToString
과 마찬가지로 StackOverFlowError
를 조심해야 한다. 자기 자신을 포함하는 배열을 가지거나 순환 참조가 존재하는 경우 명시적으로 이를 제외해야만 사용할 수 있다.
또한 @ToString
처럼 doNotUseGetters
를 사용할 수 있으며 $로 시작하는 변수는 포함하지 않는다.
lombok
1.16.22 버전 이전에는 of
와 exclude
를 사용해서 Include / Exclude
를 할 수 있었고 여전히 지원되지만 deprecated
될 예정이므로 사용하지 말자.
@NoArgsConstructor, @RequiredArgsConstructor, @AllArgsConstructor
@NoArgsConstructor
매개변수가 없는 생성자를 생성한다. 만약 불가능 하다면(final
필드 떄문에) 컴파일 에러가 난다. 만약 @NoArgsConstructor(force = true)
를 사용하면 컴파일 에러를 발생시키는 대신 모든 final
필드는 기본값(0, false, null)로 초기화된다.
@RequiredArgsConstructor
초기화되지 않은 모든 final
필드, @NonUll
필드에 대한 생성자를 생성해준다. @NonNull
필드의 경우 null check 구문 또한 생성해준다. 생성자 파라미터의 순서는 필드가 작성된 순서와 같다.
@AllArgsConstructor
모든 필드에 대한 생성자를 만들어준다. 마찬가지로 @NonNull
필드에 대한 null check 구문을 생성해준다.
staticName
@RequiredArgsConstructor(staticName = "of")
와 같이 사용하면 MapEntry.of("name", value)
처럼 static Factory
를 만들어준다.
// code
@AllArgsConstructor
@RequiredArgsConstructor(staticName = "from")
class Person{
final private String name;
private int age;
@NonNull private String school;
}
// build
class Person {
private final String name;
private int age;
@NonNull
private String school;
public Person(final String name, final int age, @NonNull final String school) {
if (school == null) {
throw new NullPointerException("school is marked non-null but is null");
} else {
this.name = name;
this.age = age;
this.school = school;
}
}
private Person(final String name, @NonNull final String school) {
if (school == null) {
throw new NullPointerException("school is marked non-null but is null");
} else {
this.name = name;
this.school = school;
}
}
public static Person from(final String name, @NonNull final String school) {
return new Person(name, school);
}
}
@Data
모든 필드에 대해 @ToString
, @EqualsAndHashCode
, @Getter
를, 모든 non-final
필드에 대해 @Setter
를 설정하고 @RequiredArgsConstructor
를 설정해주는 단축 Annotation
이다.
@Value
@Data
의 불변 클래스 버전이다. 모든 필드를 private / final
로 만들고 setter
는 생성되지 않는다. 클래스 또한 final
로 만든다.
@Data
처럼 toString(), equals(), hashCode()
를 자동으로 생성해주고 각 필드에 대한 getter
와 생성자 또한 만들어 준다.
즉, @Value
는 final @ToString @EqualsAndHashCode @AllArgsConstructor @FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE) @Getter
의 단축형이다.
@With
lombok
0.11.4 버전에서 실험 기능으로 @Wither
가 추가되었으며 1.18.10 버전에서 정식 기능으로 바뀌면서 @With
로 이름이 바뀌었다.
setter
의 불변 버전이다. 필드에 @With
를 명시한 경우 withFieldName(newValue)
형태로 메소드를 추가해준다.
@With
는 객체를 생성하기 위해서 생성자에 의존한다. 만약 적절한 생성자가 존재하지 않는다면 @With
는 컴파일 오류를 발생시킨다.
@Setter
처럼 AccessLevel
을 사용해서 접근 수준을 설정할 수 있으며 기본값은 public
이다.
// code
@AllArgsConstructor
class Person{
private String name;
@With private int age;
}
// build
class Person {
private String name;
private int age;
public Person(final String name, final int age) {
this.name = name;
this.age = age;
}
public Person withAge(final int age) {
return this.age == age ? this : new Person(this.name, age);
}
}
@Builder
빌더를 자동으로 작성해준다. 클래스에 작성하면 모든 필드에 대한 빌더를 만들어준다. 원하는 필드에 대해서만 빌더를 작성하고 싶은 경우 생성자를 작성하고 그 위에 @Builder
를 붙여주면 된다.
// code
@Builder
class Person{
private String name;
private int age;
}
// build
class Person {
private String name;
private int age;
Person(final String name, final int age) {
this.name = name;
this.age = age;
}
public static Person.PersonBuilder builder() {
return new Person.PersonBuilder();
}
public static class PersonBuilder {
private String name;
private int age;
PersonBuilder() {
}
public Person.PersonBuilder name(final String name) {
this.name = name;
return this;
}
public Person.PersonBuilder age(final int age) {
this.age = age;
return this;
}
public Person build() {
return new Person(this.name, this.age);
}
public String toString() {
return "Person.PersonBuilder(name=" + this.name + ", age=" + this.age + ")";
}
}
}
// 아래처럼 사용
Person person = Person.builder()
.name("name")
.age(1)
.build();
@CleanUp
안전하게 close()
를 호출해준다.
// code
class Person {
public static void main(String[] args) throws IOException {
File file;
@Cleanup InputStream in = new FileInputStream(args[0]);
byte[] b = new byte[10000];
while (in.read(b) != -1) {
System.out.println("Read~");
}
}
}
// build
class Person {
public static void main(String[] args) throws IOException {
FileInputStream in = new FileInputStream(args[0]);
try {
byte[] b = new byte[10000];
while(in.read(b) != -1) {
System.out.println("Read~");
}
} finally {
if (Collections.singletonList(in).get(0) != null) {
in.close();
}
}
}
}
결론
위와 같이 lombok
이 어떠한 기능을 제공하고 실제로 어떠한 코드를 생성하는지 살펴봤다. 더 자세한 정보는 여기에서 확인할 수 있다. lombok
은 많은 개발자들이 사용 어떻게 이런 고민을 해결하는지 살펴보고 실제로 사용해보면 좋을 것 같다.