생성자에 매개변수가 많다면 빌더를 고려하라.
정적 팩터리와 생성자에게 똑같은 제약이 하나 존재하는데, 선택적 매개변수가 많을 때 적절히 대응하기 어렵다는 점이다. 과거 프래그래머들은 점층적 생성자 패턴(telescoping constructor pattern)을 즐겨 사용했다고 한다. 이 클래스의 인스턴스를 만들려면 원하는 매개변수를 모두 포함한 생성자 중 가장 짧은 것을 골라 호출하면 된다.
점층적 생성자 패턴도 쓸 수는 있지만, 매개변수 개수가 많아지면 클라이언트 코드를 작성하거나 읽기 어렵다.
점층적 생성자 패턴
public class Person {
private final String name;
private final int age;
private final String address;
private final String phone;
public Person(String name, int age) {
this(name, age, null);
}
public Person(String name, int age, String address) {
this(name, age, address, null);
}
public Person(String name, int age, String address, String phone) {
this.name = name;
this.age = age;
this.address = address;
this.phone = phone;
}
public static void main(String[] args) {
Person01 person = new Person01("Jisu", 20, "서울특별시 강동구", "010-0011-1100");
}
}
자바빈즈 패턴
또 다른 방법이 존재한다. 이것은 자바빈즈 패턴(JavaBeans pattern)이라고 한다. 매개변수가 없는 생성자로 객체를 만든 후, Setter 메서드들을 호출해 원하는 매개변수의 값을 설정하는 방식이다. 자바빈즈는 심각한 단점을 지니고 있다. 객체 하나를 만들려면 메서드를 여러 개 호출해야 하고, 객체가 완전히 생성도기 전까지 일관성이 무너진 상태에 놓이게 된다.
점층적 생성사 패턴에서는 매개변수들이 유효한지를 생성자에서만 확인하면 일관성을 유지할 수 있었는데, 그 장치가 완전히 사라진 것이다. 이처럼 일관성이 무너지는 문제 때문에 자바빈즈 패턴에서는 클래스를 불편으로 만들 수 없으며 스레드 안전성을 얻으려면 프로그래머가 추가 작업을 해줘야만 한다.
public class Person {
private String name = null;
private int age = 0;
private String address = null;
private String phone = null;
public Person() {}
// Setter Method
public void setName(String val) { name = val; }
public void setAge(int val) { age = val; }
public void setAddress(String val) { address = val; }
public void setPhone(String val) { phone = val; }
public static void main(String[] args) {
Person person = new Person();
person.setName("Jisu");
person.setAge(30);
person.setPhone("010-0011-1100");
person.setAddress("서울특별시 강동구");
}
}
빌더 패턴
점층적 생성자 패턴과 자바빈즈 패턴의 장점만 취한 패턴이다. 클라이언트는 필요한 객체를 직접 만드는 대신, 필수 매개변수만으로 생성자(혹은 정적 팩터리)를 호출해 빌더 객체를 얻는다. 그런 다음 빌더 객체가 제공하는 일정의 세터 메서드들로 원하는 선택 매개변수들을 설정한다. 마지막으로 매개변수가 없는 build 메서드를 호출해 우리가 필요한 객체를 얻는다.
public class Person {
private final String name;
private final int age;
private final String address;
private final String phone;
private Person(Builder builder) {
this.name = builder.name;;
this.age = builder.age;
this.address = builder.address;
this.phone = builder.phone;
}
public static class Builder {
// 필수 매개변수
private final String name;
private final int age;
// 선택 매개변수
private String address = null;
private String phone = null;
// 필수 매개변수를 받기 위한 생성자
public Builder (String name, int age) {
this.name = name;
this.age = age;
}
public Builder setAddress(String address) {
this.address = address;
return this;
}
public Builder setPhone(String phone) {
this.phone = phone;
return this;
}
public Person build() {
return new Person(this);
}
}
public static void main(String[] args) {
Person person = new Builder("Jisu", 30)
.setAddress("서울특별시 강동구")
.setPhone("010-0011-1100")
.build();
System.out.println(person.name + " " + person.age + " " + person.address + " " + person.phone);
}
}
계층적으로 설계된 클래스에 빌더 패턴 적용하기
Pizza
import java.util.EnumSet;
import java.util.Objects;
import java.util.Set;
public abstract class Pizza {
final Set<Topping> toppings;
Pizza(Builder<?> builder) {
toppings = builder.toppings.clone();
}
public enum Topping {HAM, MUSHROOM, ONION, PEPPER, SAUSAGE}
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();
}
}
NyPizza
import java.util.Objects;
public class NyPizza extends Pizza {
private final Size size;
public NyPizza(Builder builder) {
super(builder);
this.size = builder.size;
}
public enum Size {SMALL, MEDIUM, LARGE}
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;
}
}
}
public class Calzone extends Pizza {
private final boolean sauceInside;
public static class Builder extends Pizza.Builder<Builder> {
private boolean sauceInside = false;
public Builder sauceInside() {
this.sauceInside = true;
return this;
}
@Override
public Calzone build() {
return new Calzone(this);
}
@Override
protected Builder self() {
return this;
}
}
Calzone(Builder builder) {
super(builder);
this.sauceInside = builder.sauceInside;
}
}
각 하위 클래스의 빌더가 정의한 build 메서드는 해당하는 구체 하위 클래스를 반환하도록 선언한다. NyPizza.Builder는 NyPizza를 반환하고, Calzone.Builder는 Calzone을 반환한다는 뜻이다. 하위 클래스의 메서드가 상위 클래스의 메서드가 정의한 반환 타입이 아닌, 그 하위 타입을 반환하는 기능을 공변 반환 타이핑(convariant return typing)이라고 한다.
Builder에 시뮬레이트한 셀프 타입 관용구인 self() 추상 메서드를 넣어 자식 클래스에서 형반환 하지 않고도 메서드 체이닝을 할 수 있도록 지원했다.
public class Main {
public static void main(String[] args) {
NyPizza pizza = new NyPizza.Builder(Size.SMALL).build();
Calzone calzone = new Calzone.Builder()
.addTopping(Topping.HAM).build();
}
}