•
정적 팩토리와 생성자는 똑같은 제약을 하나 가지고 있다. 선택적 매개변수가 많을때 적절히 대응하기가 힘들다는 것이 바로 그것이다.
•
먼저 점층적 생성자 패턴에 대해서 알아보자.
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.cabohydrate = carbohydrate;
}
}
Java
복사
◦
이 클래스의 인스턴스를 만들려면 원하는 매개변수를 모두 포함한 생성자 중 가장 짧은 것을 골라 호출하면 된다.
◦
보통 이런 생성자는 사용자가 설정하길 원치 않는 매개변수까지 포함하기 쉬우나 어쩔 수 없이 그런 매개변수도 값을 설정해주어야 한다.
◦
점층적 생성자 패턴도 사용이 어렵지는 않지만 매개변수가 만약 더 늘어난다면 클라이언트 코드를 작성하거나 읽기 어렵게 된다.
▪
코드를 읽을 때 각 값의 의미가 무엇인지 파악하기가 힘들어진다.
▪
매개변수가 몇 개인지도 주의해서 세어보아야하게 된다.
•
두 번째 대안으로는 자바빈즈 패턴이 존재한다.
◦
매개변수가 없는 생성자로 객체를 만든 후, setter 메소드를 호출해 원하는 매개변수의 값만 설정하는 방식이다.
public class NutritionFacts {
private int servingSize;
private int servings;
private int calories;
private int fat;
private int sodium;
private int carbohydrate;
public NutritionFacts() {}
public void setServingSize(int val);
public void setServings(int val);
public void setCalories(int val);
public void setFat(int val);
public void setSodium(int val);
public void setCarbohydrate(int val);
}
Java
복사
◦
점층적 생성자 패턴의 단점들이 더는 보이지 않는다. 코드가 길어지긴 했지만 인스턴스를 만들기 쉽고 읽기도 더 쉬워졌다.
◦
불행히도 자바빈즈 패턴은 심각한 단점을 지니고 있다.
◦
자바빈즈 패턴에서는 객체 하나를 만들려면 메소드를 여러개 호출해야 하고 객체가 완전히 생성되기 전까지는 일관성이 무너진 상태에 놓이게 된다.
▪
점층적 생성자 패턴에서는 매개변수들이 유효한지를 생성자에서만 확인하면 되었으나 그 장치가 완전히 사라진 것이다.
▪
일관성이 깨진 객체가 만들어지면 버그가 존재하는 코드와 그 버그 때문에 런타임 상에서 문제가 발생하는 코드가 물리적으로 멀리 떨어져있으므로 디버깅이 쉽지 않다.
◦
이처럼 일관성이 무너지는 문제 때문에 자바빈브 패턴에서는 객체를 불변(final)으로 만들 수 없으며 스레드 안전성을 확보하려면 프로그래머가 병렬 프로그래밍 관련 처리를 해주어야 한다.
•
다행히 우리에게는 세 번째 대안이 있다. 바로 빌더 패턴이다.
◦
클라이언트는 필요한 객체를 만드는 대신 필수 매개변수만으로 생성자를 호출해 빌더 객체를 얻는다.
◦
그런 다음 빌더 객체가 제공하는 일종의 세터 메소드들로 원하는 선택 매개변수들을 설정한다.
◦
마지막으로 매개변수가 없는 build 메소드를 호출해 우리에게 필요한 객체를 얻는다.
◦
빌더는 생성할 클래스 안에 정적 멤버 클래스로 만들어두는 것이 보통이다.
•
빌더의 동작방식 - 점층적 생성자 패턴과 자바빈즈 패턴의 장점만 취했다.
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;
private NutritionFacts(Builder builder) {
servingSize = builder.servingSize;
servings = builder.servings;
calories = builder.calories;
fat = builder.fat;
sodium = builder.sodium;
carbohydrate = builder.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;
// 필수 매개변수만을 담은 Builder 생성자
public Builder(int servingSize, int servings) {
this.servingSize = servingSize;
this.servings = servings;
}
// 선택 매개변수의 setter, Builder 자신을 반환해 연쇄적으로 호출 가능
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;
}
// build() 호출로 최종 불변 객체를 얻는다.
public NutritionFacts build() {
return new NutritionFacts(this);
}
}
}
Java
복사
◦
NutritionFacts 클래스는 불변이며 모든 매개변수의 기본값들을 한곳에 모아두었다.
◦
Builder 클래스의 set 메소드들은 빌더 클래스 자신을 반환하기 때문에 연쇄적으로 호출할 수 있다.
▪
이처럼 메소드 호출이 흐르듯 연결된다는 뜻으로 플루언트 API 혹은 메소드 체이닝이라고 한다.
•
빌더 패턴은 사용하기 쉽고 무엇보다도 가독성이 좋다. 이는 파이썬과 스칼라에 있는 명명된 선택적 매개변수를 흉내낸 것이다.
•
빌더 패턴은 계층적으로 설계된 클래스와 함께 사용하기 좋은데, 각 계층의 클래스에 관련 빌더를 멤버로 정의하자.
◦
추상 클래스는 추상 빌더를 구체 클래스는 구체 빌더를 가지게 한다.
◦
다음 예시를 확인해보자.
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();
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 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;
}
}
Java
복사
◦
추상 클래스인 Pizza의 내부 Builder 클래스는 재귀적 한정 타입을 이용하는 제네릭 타입이다.
▪
abstract static class Builder<T extends Builder<T>> 이 부분을 조금 더 상세하게 알아보자..
▪
일단 추상 클래스 Builder는 제네릭을 이용해 자기 자신을 반환하는 메소드인 self()의 반환형 타입으로 규정하고 있다.
▪
여기서 재귀적 한정 타입을 이용해 해당 제네릭의 타입을 자기 자신을 상속한 경우로 한정하였다.
▪
이렇게 함으로써 Builder 클래스의 서브 클래스는 Builder 클래스 자신의 타입을 사용할 수 있게 된다.
▪
일반적으로 타입 안정성과 확장성을 위해 이 기법을 사용한다.
◦
각 하위 클래스의 빌더가 정의한 build 메소드는 해당하는 구체 클래스를 반환하도록 선언되었다.
▪
하위 클래스의 메소드가 상위 클래스에서 정의된 반환 타입이 아닌 그 하위 타입을 반환하는 기능을 공변 반환 타이핑이라 한다.
▪
이 기능을 이용하면 이 Builder를 사용하는 메소드에선 형변환을 신경쓰지 않고도 빌더를 사용할 수 있다.
•
외에도 빌더를 사용하면 가변인수 매개변수를 여러개 사용할 수 있다는 장점이 있다.
•
물론 단점도 존재한다.
◦
객체를 만들려면 그에 앞서 빌더 객체부터 만들어야 한다. 성능에 민감한 경우 충분히 이슈가 될 수 있는 영역이다.
◦
점층적 생성자 패턴보다는 코드 라인 수가 많아지므로 가급적 변수가 4개 이상인 경우에 사용할만 하다.
▪
하지만 API는 기본적으로 시간이 지남에 따라 매개변수가 많아질 가능성이 항상 존재한다는 걸 명심하자.
▪
생성자나 정적 팩토리로 만들어두었다가 매개변수가 많아질때 리팩토링해도 되지만 가급적 빌더로 시작하는 편이 나을 때가 많다.
핵심 정리
•
생성자나 정적 팩토리가 처리해야할 매개변수가 많다면 빌더 패턴을 사용하는 것이 낫다.
•
매개변수 중 다수가 필수가 아니거나 같은 타입이면 특히 더 그렇다.
•
빌더는 점층적 생성자 패턴보다 이를 호출하는 메소드 입장에서 가독성이 좋으며 자바빈즈 패턴보다 훨씬 안전하다.