정적 팩터리 메서드나 생성자나 똑같이 넣어도되고, 빼도되는 매개변수가 많을 때 이를 선택적 으로 구현하기 위해 수많은 메서드나 생성자 코드를 작성해야 된다는 제약이 있다.
필수 매개변수만 받는 생성자부터 시작해서 필수 매개변수와 선택 한개, 필수와 선택2개... 이런식으로 늘려가는 방식을 뜻한다.
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 NutritionFacts(int servingSize, int servings) {
this(servingSize, servings, 0);
}
public NutritionFacts(int servingSize, int servings, int calories) {
this(servingSize, servings, calories, 0);
}
public NutritionFacts(int servingSize, int servings, int calories,
int fat) {
this(servingSize, servings, calories, fat, 0);
}
public NutritionFacts(int servingSize, int servings, int calories,
int fat, int sodium) {
this(servingSize, servings, calories, fat, sodium, 0);
}
public NutritionFacts(int servingSize, int servings, int calories,
int fat, int sodium, int carbohydrate) {
this.servingSize = servingSize;
this.servings = servings;
this.calories = calories;
this.fat = fat;
this.sodium = sodium;
this.carbohydrate = carbohydrate;
}
}
이 클래스의 인스턴스를 만들려면 원하는 매개변수를 모두 포함한 생성자 중 가장 짧은 것을 골라 호출면된다.
그리고 이 케이스에서는 매개변수가 겨우 6개라 그리 나빠보이지 않을 수도 있지만, 수가 늘어나면 금세 걷잡을 수 없게 된다.
또 개인적인 생각으로는 필요할때마다 추가해서 사용하는 방식이 있겠지만, 이는 다른 코드를 바꿨는데 이 코드를 바꿔야 된다는 변경에 취약하다는 점때문에 지양하게 되는 부분이다.
자바빈스 패턴은 set 함수를 이용하여 클래스의 패러미터를 바꿀 수 있도록 만드는 방법이다. 점층적 생성자 패턴마냥 코드가 선택 패러미터만큼 조합적으로 늘어나지는 않는 모습을 볼 수 있다.
public class NutritionFacts {
private int servingSize=-1;
private int servings=-1;
private int calories=0;
private int fat=0;
private int sodium=0;
private int carbohydrate=0;
public NutritionFacts() {}
public void setServingSize(int val) {servingSize = val;}
public void setServings(int val) {servings = val;}
public void setCalories(int val) {calories = val;}
public void setFat(int val) {fat = val;}
public void setSodium(int val) {sodium = val;}
public void setCarbohydrate(int val) {carbohydrate = val;}
}
이렇게 짜면 인스턴스를 만들기 쉽고, 클라이언트 코드는 더 읽기 쉬운 코드가 된다.
NutritionFacts cocaCola = new NutritionFacts();
cocaCola.setServingSize(240);
cocaCola.setServings(8);
cocaCola.setCalories(100);
cocaCola.setSodium(35);
cocaCola.setCarbohydrate(27);
하지만 이 패턴에서는 객체를 하나 만드려면 메서드를 여러개 호출해야 하고, 객체가 완전히 생성되기 전까지는 점층적 생성자 패턴과는 달리 일관성이 무너진 상태에 놓인다.
일관성이 깨지게 되면, 버그를 심은 코드와 그 버그 때문에 저 뒤에서 문제가 생기기 때문에 디버깅도 어려워진다는 단점이 있다.
또 자바빈즈 패턴에서는 클래스를 불변으로 만들 수 없으며, 스레드 안정성을 얻으려면 프로그래머가 추가작업을 해줘야만 한다.
스레드 안정성이 뭔 상관?
위에서 소개한 점층적 생성자 패턴의 안정성과 Java beans의 가독성을 겸비한 빌더 패턴이라는 것을 소개할 것이다.
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 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);
}
}
public NutritionFacts(Builder builder) {
servingSize = builder.servingSize;
servings = builder.servings;
calories = builder.calories;
fat = builder.fat;
sodium = builder.sodium;
carbohydrate = builder.carbohydrate;
}
}
이렇듯 빌더의 setter 메서드는 빌더 자신을 호출하기 때문에 연쇄적으로 호출할 수 있는 구조로 되어있다. 이런 방식을 fluent API 혹은 method chaining이라고도 한다.
위의 코드에 따른 클라이언트 코드는 아래와 같게 된다.
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8).calories(100).sodium(35).carbohydrate(27).build();
이 클라이언트 코드는 쓰기도, 읽기도 쉽다. 또한 NutritionFacts
클래스는 불변이기까지 하다.
이런 빌더패턴은 계층적으로 설계된 클래스와 함께 사용하기 좋다고 한다.
이팩티브 자바에서 예시로 들었던 피자의 다양한 종류를 표현한 코드를 살펴보자.
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 add Topping(Topping topping) {
toppings.add(Objects.requireNonNull(topping);
return self();
}
abstract Pizza build();
protected abstract T self();
}
Pizza(Builder<?> builder) {
toppings = builder.toppings.clone();
}
}
위에 코드는 다양한 피자들의 계층 구조에 최상위층에 놓인 추상 클래스이다.
다시 돌아와서 pizza 추상클래스를 상속받는 다양한 피자들을 살펴보자.
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 public NyPizza build() {
return new NyPizza(this);
}
@Override protected Builder self() { return this; }
}
private NyPizza(Builder builder) {
super(builder);
size = builder.size;
}
}
public class Calzone extends Pizza {
private final boolean sauceInside;
public static class Builder extends Pizza.Builder<Builder> {
private boolean sauceInside = false;
public Builder sauceInside() {
sauceInside = true;
return this;
}
@Override public Calzone build() {
return new Calzone(this);
}
@Override protected Builder self() { return this; }
}
private Calzone(Builder builder) {
super(builder);
sauceInside = builder.sauceInside;
}
}
각 하위 클래스의 빌더가 정의한 build 메서드는 자식 클래스를 반환하도록 override 하여 선언한다.
이렇게 하위 클래스의 메서드가 상위 클래스의 메서드가 정의한 반환 타입이 아닌, 그 하위 타입을 반환하는 기능을 covariant return typing 이라고 한다. 이 기능을 사용하면 클라이언트가 형변환에 신경쓰지 않고도 빌더를 사용할 수 있다.(원래는 Pizza.Builder
형으로 return 될것이므로 covariant return typing을 사용하지 않고 Calzone.Builder
만의 메서드나 특징을 사용하고 싶으면 형변환해서 사용해야 한다.)
이러한 계층적 빌더를 사용하는 클라이언트의 코드도 앞선 영양정보 빌더를 사용하는 코드와 다르지 않다.
NyPizza pizza = new NyPizza.Builder(SMALL).addTopping(SAUSAGE).addTopping(ONION).build();
Calzone.Builder().addTopping(HAM).sauceInside().build();
이런식으로 Builder를 사용하면 선택형 매개변수를 여러개 사용할 수 있을 뿐 아니라 적절한 메서드로 나누어 원하는 속성을 넣을 수 있다는 장점을 가지고 있다. 위 클라이언트 코드에서 또 개인적으로 두드러지는 점은 Pizza.Builder
에서 선언했던 addTopping
을 통해 공통 속성인 토핑을 고르고 나머지는 자식클래스에서 사용하는 각자의 빌더 메서드로 값을 세팅한다는 점이었다.
만약 이게 생성자로 구현하려고 했으면
1. 일단 자식클래스 생성자에 설정하기 원하는만큼의 매개변수를 가진 생성자가 있어야 하고
2. 자식클래스 생성자에서 필요한 정보들을 다 넣어주고
3. 자식클래스 생성자 내부적으로도 super
을 통해서 위 클래스의 생성자를 또 따로불러줬어야 했을 것이다.
빌더가 클라이언트단에서도, API 제공자로서도 상당히 간편한 방법인듯 하다.
위에 코드가 좀 헷갈리는 부분이 abstract static class Builder<T extends Builder<T>>
이부분이다. <> 이 꺽쇄를 사용하면 class 의 type 을 지정할 수가 있는건가? 그리고 이게 T extends Builder<T>
라는게 재귀적인 선언인가..? 원래 T 라는 걸 제너릭을 사용할 때 써주는게 문법인가 싶다.
찾아보니까 Java 에서 자주보이는 generics 이라고 한다. 생각해보니까 개인적으로 엄청 자주 쓰이는 자료구조들도 이렇게 되어 있었다.
// List
List<Integer> list = new ArrayList<Integer>();
//Map
Map<String, Integer> map = new HashMap<>();
위에 자바에서 자주쓰던 리스트나 맵 클래스의 변수나 인스턴스를 선언할 때 붙여주는 모습을 확인할 수 있다.
abstract static class Builder<T extends Builder<T>>
이런식으로 정의하고
public static class Builder extends Pizza.Builder<Builder>
이런식으로 상속받은 클래스를 오버라이딩해서 구현한다. 그래서 Builder 객체를 사용하려고 하면 Builder
클래스를 extend 한 타입(자기자신의 타입 || Builder을 구현한 타입
)을 사용하여야 한다는 걸 말하고 있다. 책에서는 이를 재귀적 한정 타입 이라고 하는데, 아이템 30에 나온다고 하니까 그때 살펴보는걸로 하고 좀 미뤄놓자.
아래 피자의 자식클래스 안에 정의된 Builder
클래스는
객체를 생성할때 처리해야 될 매개변수가 많다면 빌더 패턴을 선택해버리는 편이 낫다. 만약 매개변수 중 다수가 필수가 아니거나 같은 타입이면 더더욱.