공부기록
ITEM 2 : 생성자에 매개변수가 많다면 빌더를 고려하라 본문
ITEM 2 : 생성자에 매개변수가 많다면 빌더를 고려하라
객체의 생성자 패턴에는 먼저 점층적 생성자 패턴, 자바빈즈 패턴이 있었다.
먼저 점층적 생성자 패턴이다. 생성자를 필수 매개변수를 받는 생성자부터 시작해서 선택 매개변수 1개 ... N개 까지 생성자를 늘려가는 식이다.
class Example1{
private final int a;
private final int b;
private final int c;
public Example(int a){
this(a,0,0);
}
public Example(int a, int b){
this(a,b,0);
}
public Example(int a, int b, int c){
this.a = a;
this.b = b;
this.c = c;
}
}
이 패턴을 통해 객체를 생성하면 다음과 같이 생성할 수 있다.
Example1 ex1 = new Example1(a,b);
Example1 ex1_2 = new Example1(a,b,c);
이 패턴은 필수 매개변수를 포함하여 필요한 인자를 미리 설정하기 때문에 안정성 있다. 문제는 매개변수의 개수가 많아지면 클라이언트 코드를 작성하고 읽기가 어렵다는 것이다. 매개변수의 갯수가 헷갈릴 수도 있고 순서도 헷갈리게 된다.
다음은 자바빈즈 패턴이다. 생성자를 통해 매개변수를 세팅하지 않고, 세터 함수로 세팅하는 방식이다.
class Example2{
private int a=0;
private int b=0;
private int c=0;
public void setA(int a){
this.a = a;
}
public void setB(int b){
this.b = b;
}
public void setC(int c){
this.c = c;
}
}
이 패턴을 통해 객체를 생성하면 다음과 같다.
Example2 ex2 = new Example2();
ex2.setA(a);
ex2.setB(b);
ex2.setC(c);
자바빈즈 패턴을 통해 더 가독성 있는 코드를 생성할 수 있으나, 문제가 있다. 객체 하나를 생성하기 위해 많은 세터메서드를 호출해야하고, 필수 매개변수들이 설정되기 전까지 객체는 일관성을 잃은 상태가 된다. 일관성이 깨졌기 때문에 디버깅과 버그에 의한 비용이 크게 증가하게 된다. 이 때문에 자바빈즈 패턴에서는 클래스를 불변으로 만들 수 없다.
위의 두 패턴의 대안, 즉 가독성과 안전성 둘을 겸비한 패턴은 빌더패턴이다. 빌더 패턴은 아래와 같다.
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 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 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;
}
public NutritionFacts builds(){
return new NutritionFacts(this);
}
private NutritionFacts(Builder builder){
servingSize = builder.servingSize;
servings = builder.servings;
calories = builder.calories;
fat = builder.fat;
sodium = builder.sodium;
carbohydrate = builder.carbohydrate;
}
}
}
객체를 생성하는 코드는 다음과 같다.
NutritionFacts cocaCola = new NutritionFacts.Builder(240,8)
.calories(100).sodium(35).carbohydrate(27).build();
빌더 패턴은 클래스 안에 Builder
라는 내부클래스를 두고, 객체 생성시에 빌더 클래스의 함수를 호출해서 빌더의 필드를 초기화 한 뒤에, 클래스의 생성자에 빌더를 넣어 객체를 생성하는 패턴이다. 빌더 패턴은 named optional parameters를 흉내낸 것이다.
빌더 패턴은 계층적으로 설계된 클래스와 함께 쓰기 좋다. 추상클래스에 추상 빌더를 넣고, 구체 클래스에는 구체 빌더를 구현한다.
다음은 추상 클래스와 추상 빌더, 구체 클래스와 구체 빌더의 예시이다.
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 builde(){
return new Calzone(this);
}
@Override protected Builder self(){ return this; }
}
private Calzone(Builder builder){
super(builder);
sauceInside = builder.sauceInside;
}
}
아래는 위 코드들의 사용 예시이다.
NyPizza pizza = new NyPizza.Builder(SMALL)
.addTopping(SAUSAGE).addTopping(ONION).build();
Calzone calzone = new Calzone.Builder()
.addTopping(HAM).sauceInside().build();
위의 코드로 피자의 토핑하는 부분과, 뉴욕피자의 사이즈부분, 또는 칸조네 피자의 소스를 안에 뿌리는 부분은 철저하게 분리되어 있음을 알 수 있다.
왜 빌더 패턴이 계층적 구조에 유리할까? 점층적 생성자 패턴은 상속 시에 자식 클래스의 생성자에 부모의 것도 넣어줘야 하기 때문에 구현하기 쉽지 않다. 자바빈즈 패턴의 경우에는 앞서 말한 객체의 일관성이 깨지는 문제에 의해 오류에 대처하기 어렵다. 이처럼 빌더 패턴은 매우 유연하다.
빌더 패턴의 단점은 빌더 클래스를 구현해야 하는 문제가 있다. 하지만 이 또한 객체의 크기가 커지면 오히려 빌더 패턴이 유리할 수 있다.
'Programming > JAVA' 카테고리의 다른 글
item1. 생성자 대신 정적 팩터리 메서드를 고려하라 (0) | 2021.12.01 |
---|---|
쓰레드(2) (0) | 2021.10.25 |
쓰레드(1) (0) | 2021.10.25 |
JVM (0) | 2021.05.29 |