생성자에 매개변수가 많다면 빌더를 고려하라
매개변수가 많을 때 생성자와 정적 팩터리 메서드 방식의 어려움을 확인해보자.
public class NutritionFacts {
private int servingSize;
private int serving;
private int calories;
private int fat;
public NutritionFacts(int servingSize, int serving) {
this.servingSize = servingSize;
this.serving = serving;
}
public NutritionFacts(int servingSize, int serving, int calories) {
this.servingSize = servingSize;
this.serving = serving;
this.calories = calories;
}
public NutritionFacts(int servingSize, int serving, int calories, int fat) {
this.servingSize = servingSize;
this.serving = serving;
this.calories = calories;
this.fat = fat;
}
...
}
다음과 같이, 매개 변수에 따라 생성자의 개수가 늘어나는 문제가 있다. 이를 해결하기 위해 점층적 생성자 패턴을 고려해볼 수 있다.
점층적 생성자 패턴
점층적 생성자 패턴이란 적은 매개변수를 가진 생성자에서부터 호출을 시작해서 가장 많은 매개변수를 가진 생성자에서 인스턴스를 생성하는 방법이다.
public NutritionFacts(int servingSize, int serving) {
this(servingSize, serving, 0);
}
public NutritionFacts(int servingSize, int serving, int calories) {
this(servingSize, serving, calories, 0);
}
public NutritionFacts(int servingSize, int serving, int calories, int fat) {
this.servingSize = servingSize;
this.serving = serving;
this.calories = calories;
this.fat = fat;
}
하지만 이러한 방법은 매개변수가 많아지면 클라이언트 코드를 작성하거나 읽기 어렵다는 단점이 있다. 매개변수의 순서도 기억해야할 뿐만아니라 필수로 넣어주어야 하는 값을 알기 쉽지 않다. 두 번째 대안으로 자바빈즈 패턴을 고려해볼 수 있다.
자바빈즈 패턴
매개변수가 없는 생성자로 객체를 만든 후 Setter 메서드를 이용하여 맴개변수의 값을 설정하는 방식이다.
public NutritionFacts() {}
public void setServingSize(int servingSize) {this.servingSize = servingSize;}
public void setServing(int serving) {this.serving = serving;}
public void setCalories(int calories) {this.calories = calories;}
public void setFat(int fat) {this.fat = fat;}
public static void main(String[]args){
NutritionFacts nutrition = new NutritionFacts();
nutrition.setServingSize(240);
nutirion.setServing(8);
nutirion.setCalories(100);
}
자바빈즈 패턴에서는 생성자를 호출하고 Setter 메서드를 여러번 호출해야 하는 단점을 가지고 있다. 또 기본 생성자를 사용하기 때문에
생성자에서 할 수 있는 유효성 체크가 불가능해져 일관성이 무너질 가능성이 있다.
또 다른 문제로 클래스를 불변으로 만들 수 없으며 스레드 안정성을 얻기 위해 추가적인 작업이 필요해진다. 이러한 문제를 해결하기 위해 점층적 생성자 패턴의 안정성과 자바빈즈 패턴의 가독성을 합친 빌더 패턴이 등장했다.
빌더 패턴
필수 매개변수만으로 생성자를 호출해 빌더 객체를 얻고, 빌더 객체가 제공하는 일종의 세터 메서드를 이용해 선택 매개변수들을 설정한다.
public class NutiritionBuilder {
// 필수 값
private final int servingSize;
private final int servings;
// 선택 값
private final int calories;
private final int fat;
public static class Builder {
private final int servingSize;
private final int servings;
private int calories = 0;
private int fat = 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 = fat;
return this;
}
public NutiritionBuilder build() {
return new NutiritionBuilder(this);
}
}
private NutiritionBuilder(Builder builder) {
servingSize = builder.servingSize;
servings = builder.servings;
calories = builder.calories;
fat = builder.fat;
}
}
빌더의 세터 메서드들은 빌더 자신을 반환하기 때문에 연쇄적으로 호출할 수 있다. 따라서 클라이언트 코드는 읽고 쓰기에 용이해진다. 또한 필수 매개변수의 입력을 강제할 수 있다.
NutritionBuilder Nutrition = new NutritionBuilder.Builder(240, 8)
.calories(100)
.fat(35)
.build();
빌더 패턴은 계층적으로 설계된 클래스와 함께 쓰기에 좋다.
self() 메서드를 추상 메서드로 선언함으로써 pizza 클래스를 상속한 NyPizza에서는 이 self() 메서드를 구현해야 하며 이렇게 되면
Pizza 인스턴스가 아닌 하위 인스턴스를 리턴할 수 있게 된다.
public 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();
protected abstract T self();
}
Pizza(Builder<?> builder) {
toppings = builder.toppings.clone();
}
}
public class NyPizza extends Pizza {
public enum Size { SMALL, MEDIUM, LARGE }
private final Size size;
public static class Builder extends Pizza.Builder<Builder> {
private final Size size;
public Builder(Size size) {
this.size = Objects.requireNonNull(size);
}
@Override
Pizza build() {
return new NyPizza(this);
}
@Override
protected Builder self() {
return this;
}
}
private NyPizza(Builder builder) {
super(builder);
size = builder.size;
}
}
장점
- 필수 매개변수의 입력을 강제할 수 있다.
- 메서드 체이닝 방법으로 가독성이 좋아진다.
단점
- 코드가 장황해져서 매개변수가 4개 이상 되어야 값어치를 한다.
- 생성 비용이 크지는 않지만 성능에 민감한 상황에서는 문제가 될 수 있다.