본문 바로가기

Dev Book Review/Effective Java 3판

[Effective Java] Item2. 생성자에 매개변수가 많다면 빌더를 고려하라

아이템 1에서 다루었던 정적 팩터리와, 생성자는 선택적 매개변수가 많을 때 적절히 대응하기 어렵다. 따라서 아래와 같은 대안이 존재한다.

1. 점층적 생성자 패턴 (telescoping constructor pattern)

필수 매개변수만 받는 생성자

필수 매개변수와 선택 매개변수 1개를 받는 생성자

필수 매개변수와 선택 매개변수 2개를 받는 생성자

...

이런 식으로 선택 매개변수의 수에 따른 생성자를 늘려가는 방식이다. 해당 클래스의 인스턴스를 만들기 위해서는, 원하는 인스턴스를 모두 포함한 생성자 중에서 가장 짧은 생성자를 호출하면 된다.

 

하지만 이런 생성자를 쓰다 보면 필요 없는 매개변수를 넘겨야 하는 경우도 발생하는데 이럴 땐 0과 같은 기본 값을 넘기게 된다. 이런 식으로 작성하다보면 코드를 작성하기도, 읽기도 어렵다는 단점이 있다.

2. 자바빈즈 패턴 (JavaBeans pattern)

매개변수가 없는 생성자로 객체를 만든 후, setter를 호출해 원하는 매개변수의 값을 설정하는 방식이다.

NutritionFacts cocaCola = new NutritionFacts();
cocaCola.setServingSize(240);
cocaCola.setServings(8);
cocaCola.setCalories(100);
cocaCola.setSodium(35);
cocaCola.setCarbohydrate(27);

가독성은 뛰어나지만, 객체 1개를 만들기 위해 N개의 setter 메서드를 호출해야 한다.

 

또한 객체가 완성되기 전까지 일관성이 무너진 상태에 놓이게 된다. 이게 무슨 말이냐면! 만약 cocaCola.setCalories(100); 메서드까지 실행된 상태에서, 다른 스레드에서 사용하게 된다면 정확한 객체를 받아볼 수 없다는 뜻이다. 이럴 경우 디버깅이 어려워진다. 게다가 스레드 간에 공유 가능한 상태에 있기 때문에 클래스를 불변으로 만들 수 없다. 원한다면 locking과 같은 작업을 추가로 해야 한다.

 

이러한 문제를 해결하기 위해서 객체를 freezing 할 수 있지만, freeze 메서드를 확실히 호출했는지 컴파일러가 보증할 수 없기 때문에 런타임 오류에 취약하다.

3. 빌더 패턴 (Builder pattern)

점층적 생성자 패턴의 안정성과, 자바빈즈 패턴의 가독성을 가진다.

 

만들고자 하는 객체를 직접 만들지 않고, 필수 매개변수를 가지고 생성자(또는 정적 팩터리)를 호출해 Builder 객체를 얻는다. 그리고 Builder 객체가 제공하는 일종의 setter 메서드를 통해 선택 매개변수를 설정한다. 마지막으로 매개변수가 없는 build() 메서드를 호출해 우리가 만들고자 하는 객체를 생성한다. (보통은 불변 객체)

 

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 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 build() {
            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;
    }

    public static void main(String[] args) {
        NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
                .calories(100).sodium(35).carbohydrate(27).build();
    }
}

setter 메서드들은 자기 자신(Builder)을 반환하기 때문에 연쇄적으로 메서드를 호출할 수 있다.

=> 플루언트 API (fluent API) 또는 메서드 연쇄 (method chaining)

 

매개변수의 유효성 확인을 하기 위해서는, Builder의 생성자 또는 setter 메서드에서 할 수 있다. 또한 여러 매개변수가 혼합된 불변식(invariant)은, build() 메서드가 호출하는 생성자에서 검사할 수 있다.

불변식(invariant): 프로그램이 실행되는 동안, 혹은 정해진 기간 동안 반드시 만족해야 하는 조건

빌더 패턴은 계층적으로 설계된 클래스와 함께 사용하기 좋다.

추상 빌더를 가지고 있는 추상 클래스를 만들고, 추상 클래스를 상속 받은 하위 클래스에서는 각 하위 클래스용 빌더를 가진다. (이때 하위 클래스용 빌더도 추상 빌더를 상속 받는다)

public abstract class Pizza {

    public enum Topping {
        HAM, MUSHROOM, ONION, PEEPER, 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(); // `Convariant 리턴 타입`을 위한 준비작업

        protected abstract T self(); // `self-type` 개념을 사용해서 메소드 체이닝이 가능케 함
    }

    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 sauseInside = false;

        public Builder sauceInde() {
            sauseInside = 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.sauseInside;
    }

}
NyPizza nyPizza = new NyPizza.Builder(SMALL)
    .addTopping(Pizza.Topping.SAUSAGE)
    .addTopping(Pizza.Topping.ONION)
    .build();

Calzone calzone = new Calzone.Builder()
    .addTopping(Pizza.Topping.HAM)
    .sauceInde()
    .build();
재귀적 타입 한정: 자기 자신이 들어간 표현식을 통해 타입 매개변수의 허용범위를 한정한다.
공변 반환 타이핑 (covariant return typing): 하위 클래스의 메서드가 상위 클래스의 메서드가 정의한 반환 타입이 아닌, 하위 타입을 반환하는 기능
시뮬레이트한 셀프 타입 (simulated self-type): 추상메서드 self를 더해 하위클래스에서 형변환 없이 메서드 연쇄를 지원한다.

빌더를 이용하면 가변인수 매개변수를 여러 개 사용할 수 있다. ex) addToping()

빌더패턴의 단점

객체를 만들기 전에 먼저 Builder를 만들어야 한다. 따라서 성능이 민감한 상황에서는 문제가 될 수 있다. 또한 점층적 생성자 패턴에 비해 코드가 장황하기 때문에 매개변수가 적어도 4개 이상은 되어야 값어치를 한다. 또는 앞으로 늘어날 가능성이 있을 때 사용하면 좋다.