정적 팩터리와 생성자는 선택적 매개변수가 많을 때 적절히 대응하기 어렵다는 제약이 있다.
점층적 생성자 패턴 (telescoping constructor pattern)
public class NutritionFacts {
private final int servingSize;
private final int servings;
...
public NutritionFacts(int servingSize, int servings) {
this(servingSize, servings, 0);
}
public NutritionFacts(int servingSize, int servings, int calories) {
this(servingSize, servings, calories, 0);
}
...
}
점층적 생성자 패턴을 사용할 수는 있지만, 매개변수 개수가 많아지면 클라이언트 코드를 작성하거나 읽기 어렵다는 단점이 있다.
자바빈즈 패턴 (JavaBeans pattern)
public class NutritionFacts {
private int servingSize = -1;
private int servings = -1;
...
public NutritionFacts() {}
public void setServingSize(int val) {
servingSize = val;
}
public void setServings(int val) {
servings = val;
}
...
}
setter를 이용해서 점층적 생성자 패턴의 단점을 보완할 수 있다. 하지만 자바빈즈 패턴은 심각한 단점을 지니고 있다. 객체 하나를 만들기 위해서 메서드를 여러 개 호출해야 하고, 객체 완성 전까지는 일관성이 무너진 상태에 놓인다. 이런 문제 때문에 클래스를 불변으로 만들 수도 없고 스레드의 안정성을 얻으려면 freeze 메서드를 호출해줘야 하는데 어려운 방법이라서 잘 쓰이지 않는다고 한다.
이런 단점들을 보완하기 위해서 점층적 생성자 패턴의 안정성과 자바 빈즈 패턴의 가독성을 겸비하고 있는 빌더 패턴을 사용하는 것이 좋다.
빌더 패턴 (Builder pattern)
public class NutritionFacts {
private final int servingSize;
private final int serving;
private final int fat;
...
public static class Builder {
private final int servingSize;
private final int serving;
private int fat = 0; // 선택 매개변수
...
public Builder(int servingSize, int serving) {
this.servingSize = servingSize;
this.serving = serving;
}
public Builder fat(int val) {
fat = val;
return this;
}
...
}
}
NutritionFacts 클래스는 불변이며, 모든 매개변수의 기본값들을 한곳에 모아뒀다. 빌더의 세터 메서드들은 빌더 자신을 반환하기 때문에 연쇄적으로 호출할 수 있다. 이런 방식을 플루언트 API(fluent API) 혹은 메서드 연쇄(method chaining)라 한다. 이런 빌더 패턴은 (파이썬과 스칼라에 있는) 명명된 선택적 매개변수(named optional parameters)를 흉내 낸 것이다.
코드에는 유효성 검사가 생략되어 있는데 실제로는 빌더 클래스 내에서 불변식을 보장할 필요가 있다. 만약 불변식(invariant) 검사에서 잘못된 점을 발견하면 illegalArgumentException을 던지면 된다.
- 불변식은 프로그램이 실행되는 동안, 혹은 정해진 기간 동안 반드시 만족해야 하는 조건을 말한다.
빌더 패턴은 계층적으로 설계된 클래스와 함께 쓰기에 좋다. 각 계층의 클래스에 관련 빌더를 멤버로 정의하자. 추상 클래스는 추상 빌더를, 구체 클래스(concrete class)는 구체 빌더를 갖게 한다.
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();
}
}
추상 메서드인 self는 하위 클래스에서 형변환하지 않고도 메서드 연쇄를 지원할 수 있도록 한다. self 타입이 없는 자바를 위한 이 우회 방법을 시뮬레이트 한 셀프 타입(simulated self-type) 관용구라 한다.
하위 클래스 예시
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;
}
}
하위 클래스의 메서드가 상위 클래스의 메서드가 정의한 반환 타입이 아닌, 그 하위 타입을 반환하는 기능을 공변 반환 타이핑(convariant return typing)이라 한다. 이 기능을 이용하면 클라이언트가 형변환에 신경 쓰지 않고도 빌더를 사용할 수 있다.
이런 빌더 패턴을 사용하면 가변인수(varargs) 매개변수를 여러 개 사용할 수 있다. 각각을 적절한 메서드로 나누어 선언하거나 메서드를 여러 번 호출하도록 하고 각 호출 때 넘겨진 매개변수들을 하나의 필드로 모을 수도 있다. 위에 Pizza 클래스의 addTopping 메서드가 이렇게 구현한 예다.
빌더 패턴은 객체를 만들기 위해서 빌더부터 만들어야 한다는 단점이 있다. 빌더 생성 비용이 크지는 않지만 성능에 민감한 상황에서는 문제가 될 수 있다. 또한 매개변수가 4개 이상은 되어야 값어치를 한다. 그러나 애초에 빌더로 시작하는 편이 나을 때가 많다고 한다.
'책 > 이펙티브 자바' 카테고리의 다른 글
Effective Java equals() 재정의 (0) | 2022.07.21 |
---|---|
Effective Java 아이템 5 : DI를 사용해라 (0) | 2022.03.21 |
Effective Java 아이템 4 : 인스턴스화 막기 (0) | 2022.03.20 |
Effective Java 아이템 3 : private 생성자나 열거 타입으로 싱글턴임을 보증하라 (0) | 2022.03.20 |