기록공간

[Effective Java] Item02. 생성자의 매개변수가 많다면 빌더를 고려하라 본문

Java/Effective Java

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

입코딩 2021. 12. 21. 07:56
반응형

정적 팩터리 메서드 혹은 생성자에는 서로 공통적인 제약이 하나 있다.

선택적 매개변수가 많을 경우 적절히 대응이 어렵다는 것이다. 

ex) 영양정보는 20개가 넘어가지만, 대다수의 값이 0인 경우

 

이를 해결하기 위해 여러 대안이 있는데, 한번 살펴보도록 하자.

 

점층적 생성자 패턴 (Telescoping Constructor Pattern)

매개변수를 점층적으로 받을 수 있게 생성자를 구성하는 것이다. 

 

  • 필수 매개변수만 받는 생성자
  • 필수 매개변수
  • 선택 매개변수 1개 받는 생성자
  • 선택 매개변수 2개 받는 생성자
  • ...
public class NutritionFacts {
    private final int servingSize;      // 1회 제공량       필수
    private final int servings;         // 총 n회 제공량     필수
    private final int calories;         // 1회 제공량당      선택
    private final int fat;              // 1회 제공량(g)    선택
    private final int sodium;           // 1회 제공량(mg)   선택
    private final int carbohydrate;     // 1회 제공량(g)    선택

    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;
    }
}

호출은 다음과 같이 해준다.

NutritionFacts cocaCola = new NutritionFacts(240, 8, 100, 0, 35, 27);

점층적 생성자 패턴은 쓰는것 자체에는 아무런 문제가 없다.

하지만 매개변수의 갯수가 많아지면 클라이언트 코드를 작성하거나, 읽기가 어려워진다.

순서가 조금만 바뀌어도 런타임 에러가 발생할 수 있으며, 버그 생성 가능성도 내제되어 있다.

 

자바 빈즈 패턴 (JavaBeans Pattern) 

자바 빈즈 패턴은 매개변수가 없는 생성자를 만든 후, 세터(setter) 메서드들을 호출하여 원하는 매개변수의 값을 설정하는 방식이다.

public class NutritionFacts2 {
    private int servingSize   = -1;      // 필수
    private int servings      = -1;      // 필수
    private int calories      = 0;   
    private int fat           = 0;
    private int sodium        = 0;
    private int carbohydrate  = 0;

    public NutritionFacts2() { }
    
    public void setServingSize(int val)         { servingSize = val; }
    public void setServings(int val)            { servings = val; }
    public void setCalories(int val)            { calories = val; }
    public void setFat(int val)                 { fat = val; }
    public void setSodium(int val)              { sodium = val; }
    public void carbohydrate(int val)           { carbohydrate = val; }
}

호출은 다음과 같이 해준다.

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

이 패턴은 심각한 단점을 가지고 있다.

객체를 하나 만드려면 메서드를 여러 번 호출해야 한다.

그리고 호출을 통해 객체가 완전히 생성되기 전까지는 일관성이 무너진 상태로 존재한다.

이는 버그나 런타임 문제를 디버깅 하는데에 어려움으로 작용할 수 있다.

 

빌더 패턴 (Builder Pattern)

빌더 패턴은 처음에 살펴봤던 점층적 생성자 패턴의 안전성과 자바 빈즈 패턴의 가독성을 융합한 것이다.

이를 명명된 선택적 매개변수(named optional parameters)라고 한다.

다음 세가지 규칙을 지켜 만들어 진다.

 

  • 필요한 객체를 직접 만들기
  • 필수 매개변수만으로 생성자 호출
  • 세터 메서드를 호출하여 선택 매개변수들을 설정
public class NutritionFacts3 {
    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 NutritionFacts3 build() {
            return new NutritionFacts3(this);
        }
    }
    
    private NutritionFacts3(Builder builder) {
        servingSize     = builder.servingSize;
        servings        = builder.servings;
        calories        = builder.calories;
        fat             = builder.fat;
        sodium          = builder.sodium;
        carbohydrate    = builder.carbohydrate;
    }
}

NutritionFacts3 클래스는 불변이며, 모든 매개변수의 기본값들은 한 곳에 모아둔다.

그리고 빌더 내부 클래스를 만들어 매개변수들을 통제하도록 한다. 필수 매개변수들은 빌더 생성자의 인자로 받는다.

호출은 다음과 같이 해준다.

NutritionFacts3 cocaCola = new NutritionFacts3.Builder(240, 8)
                               .calories(100).sodium(35).carbohydrate(27).build();

빌더의 세터 메서드들은 빌더 자신을 반환하기 때문에 연쇄적으로 호출 할 수 있다. 이를 메서드 체이닝 혹은 플루언트 API라고 부른다. 

 

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

이 경우 각 계층이 클래스에 관련 빌더를 멤버로 정의해야 한다.

 

피자의 다양한 종류를 표현하는 계층구조의 루트 추상 클래스는 다음과 같다.

 

- Pizza.java

import java.util.Objects;
import java.util.EnumSet;
import java.util.Set;

public abstract class Pizza {
    public enum Topping { HAM, MUSHROOM, ONION, PEPPER, SAUSAGE };
    final Set<Topping> toppings;
    
    // Pizza.Builder 클래스는 재귀적 타입  한정을  이용하는  제네릭 타입이다.
    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();

        // 하위 클래스는 이 메서드를 재정의하여
        // this 를 반환하도록 해야 한다.
        // 이를 통해 형변환하지 않고도 Method Chaning을 지원할 수 있다.
        // 이러한 우회 방법을 시뮬레이트한 셀프 타입(simulated self-type) 관용구라 한다.
        protected abstract T self();
    }

    Pizza(Builder<?> builder) {
        toppings = builder.toppings.clone();
    }
}

 

- NyPizza.java

import java.util.Objects;

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;

        // size를 필수로 받음
        public Builder(Size size) {
            this.size = Objects.requireNonNull(size);
        }

        // 해당 구체의 하위 클래스를 반환한다.
        // NyPizza 반환
        @Override public NyPizza build() {
            return new NyPizza(this);
        }

        @Override protected Builder self() { return this; }
    }

    private NyPizza(Builder builder) {
        super(builder);
        size = builder.size;
    }
}

 

- Calzone.java

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;
        }

        // 해당 구체의 하위 클래스를 반환한다.
        // Calzone 반환
        @Override public Calzone build() {
            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();

빌더 패턴은 상당히 유연하다.

빌더 객체 하나로 여러 객체를 순회하면서 만들 수 있고, 빌더에 넘기는 매개변수에 따라 다른 객체를 만들수도 있다.

특정 필드는 알아서 채우도록 할 수도 있다.

 

단점

빌더 패턴을 쓰려면 빌더 클래스부터 만들어줘야 한다.

빌더 클래스의 생성 비용은 크지 않지만 성능이 민감한 쪽에서는 문제가 될 수 있다.

또한 코드가 장황하기 때문에 매개변수 갯수가 일정(4개정도) 이상은 되어야 의미가 있다.

API는 시간이 지날수록 매개변수가 많아지는 경향이 있기 때문에 빌더패턴으로 시작하는 편이 나을 때가 많다.

 

정리

생성자나 정적 펙터리 메서드가 처리해야 할 매개변수가 많은 경우 빌더 패턴을 선택하는 게 더 낫다.

특히 매개변수 중 대다수가 필수가 아니거나, 같은 타입이면 더더욱 그렇다.

빌더는 점층적 생성자 패턴보다 코드를 읽고 쓰기가 훨씬 간결하며, 자바 빈즈 패턴보다 더 안전하다.

 

반응형
Comments