[Lombok] @Builder.Default

Hyeonsu Bang·2021년 12월 2일
7

Java-Lib

목록 보기
1/1
post-thumbnail

@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<>로 초기화를 했는데 왜 이런걸까?


doc을 살펴보면 빌더는 아래와 같이 코드를 만들어준다.
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=[])

두 방식 모두 원하는 대로 초기화가 되었다.



TL; DR 🙄



  • @Builder를 클래스 레벨에서 쓰면 모든 필드로 생성자를 생성하는 빌더 생성. 이 때 모든 필드 생성자가 선언되어 있어야 함. 이 생성자 없이 다른 생성자 오버로딩 쓰면 컴파일 에러 발생.

  • 결과적으로는 NoArgsConstructor + AllArgsConstructor + Builder 콤보를 쓰면 됨

  • @Builder.Default는 빌더로 인스턴스 생성 시 초기화할 값을 정할 수 있음. 빌더 패턴을 쓰는데 필드에 객체 타입이 있다면 꼭 써주자.



profile
chop chop. mish mash. 재밌게 개발하고 있습니다.

2개의 댓글

comment-user-thumbnail
2022년 4월 27일

좋은 글 감사합니다!

답글 달기
comment-user-thumbnail
2023년 5월 16일

도움이 됐습니다 감사합니다!

답글 달기