Post

Item 2 - 생성자에 매개변수가 많다면 빌더를 고려하라

Item2 - 생성자에 매개변수가 많다면 빌더를 고려하라

정적 팩터리 메서드와 생성자는 선택적 매개변수가 많을 때 적절히 대응하기 어렵다. 🤦‍♀️

이러한 제약의 대안으로 프로그래머들은 다음과 같은 방법을 사용했다.

  1. 점층적 생성자 패턴
  2. 자바 빈즈 패턴
  3. 빌더 패턴

(1) 점층적 생성자 패턴 (확장의 어려움)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class NutritionFacts {
    private final int servingSize;  // (mL, 1회 제공량)     필수
    private final int servings;     // (회, 총 n회 제공량)  필수
    private final int calories;     // (1회 제공량당)       선택
    private final int fat;          // (g/1회 제공량)       선택
    private final int sodium;       // (mg/1회 제공량)      선택
    private final int carbohydrate; // (g/1회 제공량)       선택

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

    public static void main(String[] args) {
        NutritionFacts cocaCola =
            new NutritionFacts(240, 8, 100, 0, 35, 27);
    }

}
  • 필수 매개변수만 받는 생성자 / 필수 매개변수 + 선택 매개변수 1개를 받는 생성자 / 필수 매개변수 + 선택 매개변수를 2개 받는 생성자 / … 형태로 선택 매개변수를 전부 다 받는 생성자까지 점층적으로 늘려가는 방식

단점 👎

  • 매개변수의 개수가 많아지면 클라이언트 코드를 작성하거나 읽기 어렵다.
  • 클라이언트가 실수로 매개변수의 순서를 바꿔 건네도 컴파일러는 알아채지 못하고, 결국 런타임에 엉뚱한 동작을 하게 된다. → 찾기 어려운 버그 유발!
  • 확장이 어렵다.

(2) 자바 빈즈 패턴

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class NutritionFacts {    
    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 NutritionFacts() { }
    // setter 메서드들
    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 setCarbohydrate(int val) { carbohydrate = val; }

    public static void main(String[] args) {
        NutritionFacts cocaCola = new NutritionFacts();
        cocaCola.setServingSize(240);
        cocaCola.setServings(8);
        cocaCola.setCalories(100);
        cocaCola.setSodium(35);
        cocaCola.setCarbohydrate(27);
    }
}
  • 매개변수가 없는 생성자로 객체를 만든 후에 setter 메서드로 값을 설정한다.
  • 점층적 생성자 패턴에 비해 인스턴스를 만들기 쉽고, 가독성이 좋아졌다.

단점 👎

  • 하나의 객체를 만들기 위해 여러 개의 메서드가 호출된다.
  • 객체가 완전히 생성되기 전까지는 일관성이 무너진 상태에 놓이게 된다.
  • 일관성이 무너지면서 클래스를 불변으로 만들 수 없게 된다.

일관성이 무너지면 런타임 시 디버깅에 어려움을 겪을 수 있고 🤦‍♀️, 불변 클래스로 만들 수 없게 되면서 Thread safe하지 않게 된다.

(3) 빌더 패턴

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
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;
    }
}
  • 점층적 생성자 패턴의 안정성과 자바 빈즈 패턴의 가독성을 겸비한 것이 빌더 패턴이다.
  • 필수 매개 변수만으로 생성자를 호출하여 빌더 객체를 얻은 후에 빌더 객체가 제공하는 setter 메서드들로 원하는 매개변수들을 설정한다. 이후 매개변수가 없는 build 메서드를 호출해 필요한 (보통은 불변인) 객체를 얻는다.
  • 빌더의 setter 메서드는 빌더를 반환하기 때문에 연쇄적 호출이 가능하다. (fluent API)
1
2
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
    .calories(100).sodium(35).carbohydrate(27).build();

필수 매개변수를 넣고, 원하는 매개변수를 선택적으로 넣어준 후 bulid()를 통해 객체를 얻고 있다.

이 코드는 읽기 쉽게 이루어져 명확한 정보 전달을 하고 있다.

빌더 패턴의 활용방법

  • 빌더 패턴은 계층적으로 설계된 클래스와 함께 쓰기에 좋다.
  • 각 계층의 클래스에 관련 빌더를 멤버로 정의하고 추상 클래스는 추상 빌더, 구체 클래스는 구체 빌더를 가진다.
  • 추상 클래스(Pizza)의 Builder 클래스는 재귀적 타입 한정을 이용하는 제네릭 타입으로, 하위 클래스에서는 형변환하지 않고도 메서드 연쇄가 가능하다.
  • 각 하위 클래스의 빌더가 정의한 build()는 해당 구체 하위 클래스를 반환한다. (공변 반환 타이핑)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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();

        // 하위 클래스는 이 메서드를 재정의(overriding)하여
        // this를 반환하도록 해야 한다.
        protected abstract T self();
    }
    
    Pizza(Builder<?> builder) {
        toppings = builder.toppings.clone();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 공변 반환 타이핑
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;
    }
}
1
2
3
NyPizza pizza = new NyPizza.Builder(SMALL)
    .addTopping(SUASAGE).addTopping(ONION)
    .build();

핵심정리 💡

  • 생성자나 정적 팩터리가 처리해야할 매개변수가 많다면 빌더 패턴을 선택하는게 낫다.
  • 매개변수 중 다수가 필수가 아니거나 같은 타입이면 특히 더 낫다.

참고 🕶️

자바빈 스펙

https://download.oracle.com/otndocs/jcp/7224-javabeans-1.01-fr-spec-oth-JSpec

RuntimeException

https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/RuntimeException.html

https://docs.oracle.com/javase/tutorial/essential/exceptions/runtime.html

This post is licensed under CC BY 4.0 by the author.