0. 이 글을 쓰게 된 이유
남의 코드들을 보면서 빌더 패턴을 처음 접했다. 많은 개발자들이 이 패턴을 쓰는 이유와 제대로 알고 쓰고 싶어서 책을 읽고 공부한 내용을 정리해본다.
빌더 패턴에 대해서 공부를 해보니 평소에 사용하는 빌더 패턴(Effective Java에서 설명하고 있는)과 GoF의 빌더 패턴의 목적이 조금 다르다는 것을 알게되었다.
이 글에서는 Effective Java에서 설명하는 빌더 패턴의 목적 위주로 설명한다.
Effective Java 아이템 2.생성자에 매개변수가 많다면 빌더를 고려하라
이펙티브 자바에서 말하는 빌더 패턴은 객체 생성을 가독성 있고 유연하기 하기 위한 목적이다.
이 책에서 설명하는 방식처럼 점층적 생성자 패턴(telescoping constructor pattern)과 자바빈 패턴(JavaBeans pattern)을 알아보고 왜 빌더 패턴을 사용하는지 알아보자.
점층적 생성자 패턴을 만드는 방법은 다음과 같다.
- 필수 매개변수마 받는 생성자를 만든다.
- 1개의 선택 매개변수를 받는 생성자를 받는다.
- 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;
}
}
new NutritionFacts(240, 8, 100, 0, 35, 27);
이번에는 매개변수가 많을 때 활용 가능한 자바 빈즈 패턴이다.
자바 빈즈 패턴을 만드는 방법은 다음과 같다.
- 매개변수가 없는 생성자를 만든다.
- 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 servingSize) {
this.servingSize = servingSize;
}
public void setServings(int servings) {
this.servings = servings;
}
public void setCalories(int calories) {
this.calories = calories;
}
public void setFat(int fat) {
this.fat = fat;
}
public void setSodium(int sodium) {
this.sodium = sodium;
}
public void setCarbohydrate(int carbohydrate) {
this.carbohydrate = carbohydrate;
}
}
NutritionFacts nutritionFacts = new NutritionFacts();
nutritionFacts.setServingSize(240);
nutritionFacts.setServings(8);
nutritionFacts.setCalories(100);
nutritionFacts.setSodium(35);
nutritionFacts.setCarbohydrate(27);
위의 모든 문제들을 모두 해결하고 가독성을 겸비한 패턴이 빌더 패턴이다.
public class NutritionFacts {
private int servingSize; // 필수 매개변수
private int servings; // 필수 매개변수
private int calories;
private int fat;
private int sodium;
private 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;
public Builder(int servingSize, int servings) {
this.servingSize = servingSize;
this.servings = servings;
}
public Builder calories(int calories) {
this.calories = calories;
return this;
}
public Builder fat(int fat) {
this.fat = fat;
return this;
}
public Builder sodium(int sodium) {
this.sodium = sodium;
return this;
}
public NutritionFacts build() {
return new NutritionFacts(this);
}
}
}
다음과 같이 호출하면 된다.
NutritionFacts cocaCola = new NutritionFacts
.Builder(240, 8) // 필수값 입력
.calories(100)
.sodium(35)
.carbohydrate(27)
.build();
Effective java에서 빌더 패턴은 계층적으로 설계된 클래스와 함께 쓰기에 좋다라고 한다. Effective java에 나온 예시를 바탕으로 무슨 뜻인지 이해해보자.
다음은 피자의 다양한 종류를 표현하는 계층구조의 루트에 놓인 추상 클래스이다.
public abstract class Pizza {
public enum Topping {
HAM, MUSHROOM, ONION, PEPPER, SAUSAGE
}
final Set<Topping> toppings;
Pizza(Builder<?> builder) {
toppings = builder.toppings.clone();
}
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();
// 하위 클래스는 이 메서드를 overriding 하여 "this"를 반환하도록 해야한다.
protected abstract T self();
}
}
Pizza.Builder 클래스는 재귀적 타입 한정을 이용하는 제너릭 타입이다. 여기에 추상 메서드인 self를 더해 하위 클래스에서 형변환하지 않고도 메서드 연쇄를 지원할 수 있다. self 타입이 없는 자바를 위한 이 우회 방법을 시뮬레이트한 셀프 타입(simulated self-type) 관용구라 한다.
다음은 일반적인 뉴욕 피자이다. 뉴욕 피자는 size 매개변수를 필수로 받는다.
public class NyPizza extends Pizza {
public enum Size {SMALL, MEDIUM, LARGE;}
private final Size size;
public NyPizza(Builder builder) {
super(builder);
size = builder.size;
}
public static class Builder extends Pizza.Builder<Builder> {
private final Size size;
public Builder(Size size) {
this.size = Objects.requireNonNull(size);
}
@Override
NyPizza build() {
return new NyPizza(this);
}
@Override
protected Builder self() {
return this;
}
}
}
이러한 경우에는 다음과 같은 방식으로 생성할 수 있다.
NyPizza nypizza = new NyPizza
.Builder(MEDIUM)
.addTopping(HAM)
.addTopping(ONION)
.build();
다음은 칼초네 피자이다. 소스를 안에 넣을지 선택(sauceInside)하는 매개변수를 필수인자로 받는다.
public class Calzone extends Pizza {
private final boolean sauceInside;
private Calzone(Builder builder) {
super(builder);
sauceInside = builder.sauceInside;
}
public static class Builder extends Pizza.Builder<Builder> {
private boolean sauceInside = false; // default
public Builder sauceInside() {
sauceInside = true;
return this;
}
@Override
Calzone build() {
return new Calzone(this);
}
@Override
protected Builder self() {
return this;
}
}
}
이러한 경우 다음과 같이 생성할 수 있다.
Calzone calzone = new Builder()
.sauceInside()
.addTopping(ONION)
.build();
NyPizza.Builder는 NyPizza를 반환하고, Calzone.Builder는 Calzone를 반환한다. 하위 클래스의 메서드가 상위 클래스의 메서드가 정의한 반환 타입이 아닌, 그 하위 타입을 반환하는 기능을 공변 반환 타입(concariant return typing)이라 한다. 이 기능을 이용하면 클라이언트가 형변환에 신경 쓰지 않고도 빌더를 사용할 수 있다.
생성자로는 누릴 수 없는 사소한 이점으로, 빌더를 이용하면 가변인수(varargs) 매개변수를 여러 개 사용할 수 있다. 각각을 적절한 메서드로 나눠 선언하면 된다. 아니면 메서드를 여러 번 호출하도록 하고 각 호출 때 넘겨진 매개변수들을 하나의 필드로 모을 수도 있다.(위 Pizza.Builder에 addTopping이 예이다.)
빌더 패턴은 상당히 유연하다. 빌더 하나로 여러 객체를 순회하면서 만들 수 있고, 빌더에 넘기는 매개변수에 따라 다른 객체를 만들 수 있다. 객체마다 부여되는 일련번호와 같은 특정 필드느 빌더가 알아서 채우도록 할 수도 있다.
정리하자면, 생성자나 정적 팩터리가 처리해야 할 매개변수가 많다면 빌더 패턴을 선택하는 게 더 낫다.
매개변수 중 다수가 필수가 아니거나 같은 타입이면 특히 더 그렇다. 빌더는 점층적 생성자보다 클라이언트 코드를 읽고 쓰기가 훨씬 간결하고, 자바빈즈보다 훨씬 안전하다.