오늘은 이펙티브 자바 내용 중 빌더에 대해 포스팅하려고 합니다. 빌더 패턴을 통한 객체 생성은 이전 포스팅에서 다뤘던 적이 있어, 이번에는 개념적으로 접근해보려고 합니다.
참고: Lombok에서 @Builder을 자바 코드로 직접 만들어 보자.
객체를 생성할 시 생성자나 정적 팩토리 메서드를 사용합니다.
빌더는 객체에 선택적인 매개변수가 많을 때, 고려해보면 좋습니다.
선택적인 매개변수란 밑의 코드에서 Person의 name, height, weight 필드를 선택해서 객체를 생성할 필요가 있을 때, 선택적으로 매개변수를 사용하는 경우를 의미합니다.
public class Person{
private String name;
private int height;
private int weight;
//....
}
만약, 이 외에도 굉장히 많은 필드들이 Person 객체에 있다고 가정해 보면, new 생성자로 객체를 생성할 경우 굉장히 피곤해질것 같습니다.
1) 개발자는 클라이언트 코드에서 생성자로 써야하는 매개변수의 순서가 헷갈릴수 있습니다. 물론 IDE에서 알려주기도 합니다.
2) 코드를 읽는 것 또한 어려워 질 것입니다. 예를 들어, 자료형도 같고 필드에 담기는 값도 비슷하다면 더욱 헷갈릴 것입니다.
Person person = new Person(name, height, weight, 주소, 성별 ...);
//메서드 체이닝을 통한 객체 생성
//lombok의 @Builder를 사용할 경우 객체에 필수적인 필드를 지정할 수 없습니다.
Person person = Person.builder().height(180).build();
//Builder 객체에 필수적으로 들어가야할 필드(name)를 지정할 수 있습니다.
Pserson person1 = new Builder("name").weight(70).build();
this
를 리턴하지만 상위 클래스에서 this
를 사용하면 리턴 타입을 Builder<T>
로 지정해야 합니다.// this를 사용했을 경우
public Builder<T> addTopping(Topping topping) {
toppings.add(Objects.requireNonNull(topping));
return this;
}
abstract Pizza build();
Pizza#addTopping()
을 사용한 후 build()
메서드를 사용하게 되면 Pizza를 리턴하기 때문에, 형 변환을 해줘야 합니다. self()
라는 추상메서드를 정의하고 하위 타입에서 직접 구현하여 this
를 리턴하게 합니다. public abstract class Pizza {
public enum Topping { HAM, MUSHROOM, ONION, PEPPER, SAUSAGE }
final Set<Topping> toppings;
//(1)
abstract static class Builder<T extends Builder<T>> {
EnumSet<Topping> toppings = EnumSet.noneOf(Topping.class);
//(2)
public T addTopping(Topping topping) {
toppings.add(Objects.requireNonNull(topping));
return self();
}
abstract Pizza build();
//(3)
protected abstract T self();
}
Pizza(Builder<?> builder) {
toppings = builder.toppings.clone(); // 아이템 50 참조
}
}
Pizza#addToping()
을 사용할 수 있습니다.Calzone#self()
를 구현하여 this를 리턴하게 함으로써 Calzone의 Builder를 리턴하기 때문에 (2)번의 Calzone#sauceInside()
를 사용할 수 있게 됩니다.public class Calzone extends Pizza {
private final boolean sauceInside;
//(1)
public static class Builder extends Pizza.Builder<Calzone.Builder> {
private boolean sauceInside = false; // 기본값
//(2)
public Builder sauceInside() {
sauceInside = true;
return this;
}
@Override public Calzone build() {
return new Calzone(this);
}
//(3)
@Override protected Builder self() { return this; }
}
private Calzone(Builder builder) {
super(builder);
sauceInside = builder.sauceInside;
}
@Override public String toString() {
return String.format("%s로 토핑한 칼초네 피자 (소스는 %s에)",
toppings, sauceInside ? "안" : "바깥");
}
}
public class PizzaTest {
public static void main(String[] args) {
Calzone calzone = new Calzone.Builder() //self()를 리턴함으로써 형변환을 하지 않음
.addTopping(HAM)//Pizza의 메서드를 사용
.sauceInside()// Calzone가 가진 메서드를 사용할 수 있다.
.build();
System.out.println(pizza);
System.out.println(calzone);
}
}
self()
라는 추상 메서드를 정의하고 하위 클래스에서 self()
에 this
를 리턴하게 하면 상위 클래스의 빌더에 있는 메서드 뿐만 아니라 본인이 정의한 메서드도 사용할 수 있기 때문에 확정성이 있습니다.빌더 외에도 선택적 매개변수가 많을 때 고려할 수 있는 방안이 있습니다. 빌더만큼 효율적인지는 못한 것 같습니다.
public class NutritionFacts {
private int servingSize;
private int servings;
private int calories;
private int fat;
private int sodium;
private int carbohydrate;
//(1)
public NutritionFacts(final int servingSize, final int servings) {
this(servingSize, servings, 0);
}
//(2)
public NutritionFacts(final int servingSize, final int servings, final int calories) {
this(servingSize, servings, calories, 0, 0, 0);
}
//(3)
public NutritionFacts(final int servingSize, final int servings, final int calories,
final int fat, final int sodium, final int carbohydrate) {
this.servingSize = servingSize;
this.servings = servings;
this.calories = calories;
this.fat = fat;
this.sodium = sodium;
this.carbohydrate = carbohydrate;
}
}
NutritionFacts nf = new NutritionFacts();
nf.setServingSize(1);
nf.setServings(2);
nf.setCalories(3);
nf.setFat(4);
....
reference