@Builder
Lombok에서 제공하는 이 어노테이션은 생성자 인자를 메서드 체인을 통해 명시적으로 대입하여 생성자를 호출할 수 있게 빌더 클래스를 생성 해준다. 빌더 클래스와 IDE의 자동 완성 기능을 같이 활용하면 생성자 작성 시 오기입 확률과 인자를 누락할 확률을 획기적으로 낮출 수 있다.
doc을 보면 @Builder
는 생성자, 메서드 또는 클래스 레벨에서 쓰일 수 있다고 설명되어 있다. 또한 클래스 레벨에서 쓰일 경우 기본적으로 전체 멤버를 생성자의 매개값으로 갖는 private
생성자를 만들어 준다. 이 생성자는 @XArgsConstructor
(NoArgs, RequiredArgs) 또는 어떤 생성자도 클래스 내부에 선언하지 않았을 경우에만 생성된다. 반대로 위의 두 조건 중 하나를 했을 경우, 모든 필드를 매개값으로 하는 생성자를 자동으로 선언해서 사용한다. 따라서 이 경우 All Args Constructor가 없으면 컴파일 에러가 발생한다.
정리하면 @Builder
클래스 레벨에서 쓰려면 All args constructor가 있어야 한다. 이 외의 생성자는 컴파일 에러를 일으킨다.
예를 들어 @NoArgsConstructor
를 쓰고 클래스 레벨의 @Builder
를 쓰게 되면 All args constructor 없이 기본 생성자만 선언한 것과 같으므로 컴파일 에러가 뜬다.
그런데 JPA나 json parser와 같은 라이브러리를 쓸 때에는 반드시 클래스에 기본 생성자가 있어야 한다. 이 경우 @NoArgsConstructor
를 쓸 수 밖에 없다. 그러면 클래스 레벨에서 @Builder
를 쓸 수가 없어진다. 방법은 전체 필드를 사용하는 생성자를 직접 선언하고 그 생성자에 @Builder
어노테이션을 쓰든가, 아니면 @NoArgsConstructor
와 @AllArgsConstructor
를 모두 쓰면 된다.
@Builder.Default
Builder 어노테이션은 편리하고 명확하게 객체를 생성할 수 있게 도와준다.
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Pojo {
private String name;
private String nickname;
private List<PojoTwo> pojoTwos = new ArrayList<PojoTwo>();
}
public class PojoApp {
public static void main(String[] args) {
Pojo pojo = Pojo.builder().name("철수").nickname("짱구친구").build();
System.out.println(pojo.toString());
}
}
위의 예시처럼 필드 이름을 명시적으로 넣을 수 있어서 생성자 오버로딩 사용 시 시그니처를 신경쓸 필요가 없어서 좋다. 결과는 다음과 같다.
Pojo(name=철수, nickname=짱구친구, pojoTwos=null)
여기서 빌더 패턴을 통해 인스턴스를 만들 때 특정 필드를 특정 값으로 초기화하고 싶다면 @Builder.Default
를 쓰면 된다.
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Pojo {
@Builder.Default
private String name = "짱구엄마";
private String nickname;
private List<PojoTwo> pojoTwos = new ArrayList<PojoTwo>();
}
public class PojoApp {
public static void main(String[] args) {
Pojo pojo = Pojo.builder().nickname("짱구친구").build();
System.out.println(pojo.toString());
}
}
Pojo(name=짱구엄마, nickname=짱구친구, pojoTwos=null)
이렇게 객체를 원하는 값으로 초기화해서 반환 받을 수 있다. 이번엔 아래 코드의 결과값을 보자.
public class PojoApp {
public static void main(String[] args) {
Pojo pojo = Pojo.builder().nickname("짱구친구").build();
System.out.println(pojo.toString());
Pojo pojo1 = new Pojo();
System.out.println(pojo1.toString());
}
}
Pojo(name=짱구엄마, nickname=짱구친구, pojoTwos=null)
Pojo(name=짱구엄마, nickname=null, pojoTwos=[])
빌더를 통해 만든 객체는 List
필드가 null
로 초기화 되었고, 빌더 없이 기본 생성자로 생성한 pojo1의 List
필드는 정상적으로 empty List로 초기화되었다. 클래스에서는 분명히 new ArrayList<>
로 초기화를 했는데 왜 이런걸까?
class Example<T> {
private T foo;
private final String bar;
private Example(T foo, String bar) {
this.foo = foo;
this.bar = bar;
}
public static <T> ExampleBuilder<T> builder() {
return new ExampleBuilder<T>();
}
public static class ExampleBuilder<T> {
private T foo;
private String bar;
private ExampleBuilder() {}
public ExampleBuilder foo(T foo) {
this.foo = foo;
return this;
}
public ExampleBuilder bar(String bar) {
this.bar = bar;
return this;
}
@java.lang.Override public String toString() {
return "ExampleBuilder(foo = " + foo + ", bar = " + bar + ")";
}
public Example build() {
return new Example(foo, bar);
}
}
}
필드를 사용하는 생성자와 각 필드의 setter 메서드로 구성된 inner 클래스를 하나 만들어서 그 안에서 원본 클래스의 인스턴스를 리턴한다. 여기서 객체 타입의 필드가 있다고 가정하면, 당연히 내부 클래스에서는 이 객체 타입을 초기화하는 코드가 없다. 따라서 null로 초기화 될 것이고, 이 필드를 포함해서 원본 클래스가 만들어지므로 빌더를 통한 객체 생성에서는 객체 타입 필드가 null인 것이다.
이를 해결하기 위해서도 역시 @Builder.Default
를 쓰면 된다.
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Pojo {
@Builder.Default
private String name = "짱구엄마";
private String nickname;
@Builder.Default
private List<PojoTwo> pojoTwos = new ArrayList<PojoTwo>();
}
Pojo(name=짱구엄마, nickname=짱구친구, pojoTwos=[])
Pojo(name=짱구엄마, nickname=null, pojoTwos=[])
두 방식 모두 원하는 대로 초기화가 되었다.
@Builder
를 클래스 레벨에서 쓰면 모든 필드로 생성자를 생성하는 빌더 생성. 이 때 모든 필드 생성자가 선언되어 있어야 함. 이 생성자 없이 다른 생성자 오버로딩 쓰면 컴파일 에러 발생.
결과적으로는 NoArgsConstructor + AllArgsConstructor + Builder 콤보를 쓰면 됨
@Builder.Default
는 빌더로 인스턴스 생성 시 초기화할 값을 정할 수 있음. 빌더 패턴을 쓰는데 필드에 객체 타입이 있다면 꼭 써주자.
좋은 글 감사합니다!