빌더는 선택적 매개변수가 만을 때의 생성자와 자바빈즈 패턴의 단점을 보완한다.
생성자의 경우 점층적 생성자 패턴을 사용할 수 있지만 그러면 클라이언트 코드가 지저분해진다는 단점이 있다.
자바 빈즈 패턴의 경우 객체 하나를 만들려면 메서드(setter
)를 여러 개 호출해야 하고, 객체가 완전히 생성되기 전까지는 일관성이 무너진 상태에 놓이게 된다. 그리고 클래스를 불변으로 만들 수 없다. (setter
로 객체 값 변경 가능하기 때문에)
이러한 단점을 보완하고자 빌더를 사용해야 한다고 책에서 말하고 있다.
public class NutritionFacts {
private final int servingSize;
private final int servings;
private final int calories;
private final int fat;
private final int sodium;
private final int carbohydrate;
public static void main(String[] args) {
NutritionFacts cocaCola = new Builder(240, 8)
.calories(100)
.sodium(35)
.carbohydrate(27).build();
}
public static class Builder {
// 필수 매개변수
private final int servingSize;
private final int servings;
// 선택 매개변수 - 기본값으로 초기화한다.
private int calories = 0;
private int fat = 0;
private int sodium = 0;
private int carbohydrate = 0;
public Builder(int servingSize, int servings) {
this.servingSize = servingSize;
this.servings = servings;
}
public Builder calories(int val)
{ calories = val; return this; }
public Builder fat(int val)
{ fat = val; return this; }
public Builder sodium(int val)
{ sodium = val; return this; }
public Builder carbohydrate(int val)
{ carbohydrate = val; return this; }
public NutritionFacts build() {
return new NutritionFacts(this);
}
}
private NutritionFacts(Builder builder) {
servingSize = builder.servingSize;
servings = builder.servings;
calories = builder.calories;
fat = builder.fat;
sodium = builder.sodium;
carbohydrate = builder.carbohydrate;
}
}
이 코드는 책에 나오는 빌더 패턴의 예시다.
클래스 안에 정적 클래스 Builder
를 만들고 속성을 받아서 Builder 객체
를 반환하는 메서드들을 만들면,
NutritionFacts nutritionFacts = new NutritionFacts.Builder(10, 10)
.carbohydrate(1)
.calories(10)
.build();
이렇게 메서드 체이닝으로 객체를 만들 수 있다.
빌더의 장점
1. 불필요한 생성자를 만들지 않고 객체를 만들 수 있다.
2. 데이터 순서에 상관 없이 객체를 만들 수 있다.
3. 사용자가 봤을 때 명시적이고 이해할 수 있다.
19p.빌더 패턴은 계층적으로 설계된 클래스와 함께 쓰기가 좋다.
처음에 이 말이 이해가 잘 안갔는데
Pizza
라는 추상 클래스에 추상 빌더가 있고, NyPizza
라는 구현클래스에 구체 빌더가 있다고 하자.
public abstract class Pizza {
public enum Topping { HAM, MUSHROOM, ONION, PEPPER, SAUSAGE }
final Set<Topping> toppings;
abstract static class Builder<T extends Builder<T>> {
EnumSet<Topping> toppings = EnumSet.noneOf(Topping.class);
public T addTopping(Topping topping) {
toppings.add(Objects.requireNonNull(topping));
return self();
}
abstract Pizza build();
// 하위 클래스는 이 메서드를 재정의(overriding)하여
// "this"를 반환하도록 해야 한다.
protected abstract T self();
}
Pizza(Builder<?> builder) {
toppings = builder.toppings.clone(); // 아이템 50 참조
}
}
여기서 중요한 건, addTopping
메서드는 Pizza
가 아니라 Pizza를 상속받는 하위 타입 객체
를 반환한다는 것이다.
그렇게 되면 addTopping
이라는 메서드를 상속받아 사용하면서, NyPizza
만 가지고 있는 메서드들도 사용할 수 있게 된다.
NyPizza pizza = new NyPizza.Builder(SMALL)
.addTopping(SAUSAGE)
.addTopping(ONION).build();
NyPizza
의 오버라이드 된 메서드를 타입변환 없이 그냥 사용하면 된다.
이렇게 하면 Pizza
의 Builder를 상속 받아 쓰면서 NyPizza
객체를 쓸 수 있는 것이다.
이런 글을 읽다보면,,, 아 생성자랑 자바빈즈는 쓰지 말고 무조건 빌더를 사용해야겠다~!
라는 생각이 들기 마련이다.
하지만 빌더 패턴에도 단점이 있다.
빌더패턴으로 바꾸면 코드 양이 증가하고 난이도가 상승한다.
그래서 선택적 인자, 필수적 인자가 공존하는 경우에, 생성자가 너무 많고 복잡하며 불변객체로 만들고 싶을 때 사용해야 한다.
위 단점들을 보완하기 위해 요즘 롬복에서 제공하는 @Builder
어노테이션을 많이 쓴다.
@Builder
public class NutritionFacts {
private final int servingSize;
private final int servings;
private final int calories;
private final int fat;
private final int sodium;
private final int carbohydrate;
public static void main(String[] args) {
NutritionFacts build = new NutritionFactsBuilder()
.carbohydrate(1)
.build();
}
}
@Builder
를 사용하면 코드가 정말 깔끔해진다.
근데 여기에도 단점이 있다 !
@Builder
어노테이션을 사용하면 (package 레벨) 모든 필드를 파라미터로 받는 생성자
가 만들어진다.
참고로 @NoArgsConstructor
나 @AllArgsConstructor
등으로 내가 직접 생성자를 만들어주었다면 모든 필드를 파라미터로 받는 생성자
가 생기지 않는다.
그렇다면 객체를 new
로 생성할 수 있게 된다.
NutritionFacts2 nutritionFacts2 = new NutritionFacts2(1, 1, 1, 1, 1, 1);
생성자로 객체를 만들지 않으려고 빌더 패턴을 도입해놓았지만
클라이언트 입장에서는 이를 알지 못하고 new
로 객체를 생성할 가능성이 생긴다.
해결방법
:@AllArgsConstructor(access = AccessLevel.PRIVATE)
로 외부에서 접근 못하게 막으면 된다.
책에 나오는 NutritionFacts
예제에서는 필수 값을 지정할 수 있었다.
NutritionFacts nutritionFacts = new NutritionFacts.Builder(10, 10)
.carbohydrate(1)
.calories(10)
.build();
근데 @Builder
를 사용하면 모든 필드를 선택적으로 주입받게 된다.
이를 원하지 않는다면 @Builder
를 쓰지말고 빌더 패턴을 직접 구현해서 써야 한다.
사실 @Builder
애노테이션을 class
레벨에 사용하는 것은 추천하지 않는다.
@Builder
를 Class에 적용시키면 생성자의 접근 레벨이 default
이기 때문에, 동일 패키지 내에서 해당 생성자를 호출 할 수 있는 문제 발생@AllArgsConstructor
와 같은 효과id
값이 데이터베이스 PK
생성전략에 의존하고 있다고 가정한다면 객체를 생성할 때 id
값을 넘겨받지 않아야 한다.class
레벨에 적용하면 객체생성에 제한을 두기가 어렵습니다.따라서 private
생성자를 구현해서 @Builder
를 지정하자.