우리가 평소에 자주 사용하는 자바빈즈 패턴은 객체 하나를 만들 때 메서드를 여러개 호출해야 되고 완전히 생성되기 전까지 일관성이 무너진 상태로 볼 수 있는데 자바빈즈 패턴이 뭐냐고?
우리가 vo, dto 만들 때 쓰던 바로 그것 이름은 익숙하겠지?
public class AccInfo {
private int cusNo = -1;
private int bnkCd = -1;
private int accNo = 0;
private int balance = 0;
public AccInfo() { }
public void setCusNo(int val) { cusNo = val;}
public void setBnkCd(int val) { bnkCd = val;}
public void setAccNo(int val) { accNo = val;}
public void setBalance(int val) { balance = val;}
}
AccInfo accinfo = new AccInfo();
accinfo.setCusNo(1);
accinfo.setBnkCd(101);
accinfo.setAccNo(333020175201293);
accinfo.setBalance(720300500);
이런 방식으로 안 써온 사람이라면 요즘 사람,, 젊은이,,,
아무튼 안전성과 가독성을 겸비한 빌더 패턴은 필요한 객체를 직접 만들고 필수 매개변수만으로도 생성자를 호출해서 빌더 객체를 얻는다. 빌더는 생성할 클래스 안에 정적 멤버 클래스로 만들어둔다.
public class AccInfo {
private final int cusNo;
private final int bnkCd;
private final int accNo;
private final int balance;
public static class Builder {
//필수
private final int cusNo;
private final int bnkCd;
private final int accNo;
//선택 - 기본값으로 초기화
private int balance = 0;
public Builder(int cusNo, int bnkCd, int accNo){
this.cusNo = cusNo;
this.bnkCd = bnkCd;
this.accNo = accNo;
}
public Builder balance(int val) {
balance = val;
return this;
}
public AccInfo build() {
return new AccInfo(this);
}
}
private AccInfo(Builder builder){
cusNo = builder.cusNo;
bnkCd = builder.bnkCd;
accNo = builder.accNo;
balance = builder.balance;
}
}
AccInfo 클래스는 불변이 되었고 모든 매개변수의 기본값을 한곳에 모아두었다. 빌더의 세터 메서드는 빌더 자신을 반환해서 연쇄적으로 호출이 가능하다. 이런 방식을 플루언트 API, 메서드 연쇄라고 하는데 사용법은 다음과 같다
AccInfo accinfo = new AccInfo.Builder(1,101,333020175201293)
.balance(720300500).build();
잘못된 매개변수를 빌더의 생성자와 메서드에서 검사하고 어떤 매개변수가 잘못됐는지 자세히 알려주는 메세지와 함께 IllegalArgumentException을 던지자!
빌더패턴은 계층적으로 설계된 클래스와 함께 사용하기 좋다. 추상 클래스는 추상 빌더, 구체 클래스는 구체 빌더
public abstract class Pizza {
public enum Topping { HAM, MUSHROOM, ONION, PEPPER, SAUSAGE }
final Set toppings;
abstract static class Builder<T extends Builder> {
EnumSet toppings = EnumSet.noneOf(Topping.class);
public T addTopping(Topping topping) {
toppings.add(Objects.requireNonNull(topping));
return selft();
}
abstract Pizza build();
//하위 클래스에서 오버라이딩해서 this를 반환하도록
protected abstract T self();
}
Pizza(Builder<?> builder) {
toppings = builder.toppings.clone();
}
}
Pizza.Builder는 추상메서드 self를 더해 하위 클래스에서 형변환하지 않고 메서드를 얀쇄 지원할 수 있다. self타입이 없는 경우는 시뮬레이트한 셀프타입이라고 한다.
public class NyPizza extends Pizza{
public enum Size{ SMALL, MEDIUM, LARGE }
private final Size size;
public static class Builder extends Pizza.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 sauceInse() {
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;
}
}
하위 클래스의 빌더가 정의한 메서드는 해당 구체 클래스를 반환하도록 선언한다
NyPizza.Builder는 NyPizza를 반환, Calzone.Builder는 Calzone를 반환한다는 것!!
하위클래스의 메서드가 상위 클래스에서 정의한 반환 타입이 아니라 그 하위 타입을 반환하는 기능을 공변반환 타이핑이라고 한다. 이 기능을 활용해서 클라이언트가 형변환에 신경쓰지 않고 빌더를 사용할 수 있다.
NyPizza pizza = new NyPizza.Builder(SMALL)
.addTopping(SAUSAGE).addTopping(ONION).build();
Calzone calzone = new Calzone.Builder()
.addTopping(HAM).sauceInside().build();
생성자로는 사용할 수 없는 점이다. 빌더를 사용하면 가변인수 매개변수를 여러 개 사용할 수 있다. 위에 처럼 말이다.
빌더 패턴은 여러 객체를 순회하면서 만들 수 있고
빌더에 넘기는 매개변수에 따라 다른 객체를 만들 수도 있다. 객체마다 부여되는 시리얼넘버 같은 특정 필드는 빌더가 알아서 채우게 할 수도 있다.
객체를 만들려면 빌더부터 만들어야한다. 성능에 민감한 상황에서는 이슈가 될 수 있다.