거꾸로 바라본 세상
반응형

규칙2. 생성자 인자가 많을 때는 Builder 패턴 적용을 고려하라



정적 팩터리나 생성자의 문제

- 선택적 인자가 많은 상황에 잘 적응하지 못하다는 것



프로그래머들은 보통 점층적 생성자 패턴(telescoping constructor pattern)을 적용

- 필수 인자만 받는 생성자를 하나 정의하고, 선택적 인자를 하나 받는 생성자를 추가하고, 거기에 두 개의 선택적 인자를 받는 생성자를 추가하는 식으로 생성자들을 쌓아 올리듯 추가하는 것.

결국 모든 선택적 인자를 다 받는 생성자를 추가하면 정의는 끝나게됨.

ex)

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
//점층적 생성자 패턴 - 더 많은 인자 개수에 잘 적응하지 못한다.
 
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 NutritionFacts(int servingSize, int servings) {
        this(servingSize, servings, 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, sodium) {
        this(servingSize, servings, calories, fat, sodifum, 0);
    }
    public NutritionFacts(int servingSize, int servings, int calories, int fat, sodium, carbohydrate) {
        this.servingSize = servingSize;
        this.servings = servings;
        this.calories = calories;
        this.fat = fat;
        this.sodium = sodium;
        this.carbohydrate = carbohydrate;
    }
}
cs


이 클래스로 객체를 생성할 때는 설정하려는 인자 개수에 맞는 생성자를 골라 호출하면된다.


그런데 이렇게하다보면 설정할 필요가 없는 필드에도 인자를 전달해야하는 경우가 생긴다.


즉, 점층적 생성자 패턴은 잘 동작하지만 인자 수가 늘어나면 클라이언트 코드를 작성하기가 어려워지고, 무엇보다 읽기 어려운 코드가 되고만다.


그래서 인자가 많을 때 적용 가능 한 두 번째 대안은 자바빈(JavaBean)패턴이다.

인자 없는 생성자를 호출하여 객체부터 만든다음, 설정 메서드(setter methods)를 호출하여 필수 필드뿐 아니라 선택적 필드의 값들 까지 채우는 것이다.

ex)

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
//자바빈 패턴 - 일관성 훼손이 가능하고, 항상 변경이 가능하다.
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 servingSize) {
        this.servingSize = servingSize;
    }
    public void setServings(int servings) {
        this.servings = servings;
    }
    public void setCalories(int calories) {
        this.calories = calories;
    }
    public void setFat(int fat) {
        this.fat = fat;
    }
    public void setSodium(int sodium) {
        this.sodium = sodium;
    }
    public void setCarbohydrate(int carbohydrate) {
        this.carbohydrate = carbohydrate;
    }
}
 
cs


이 패턴은 점층적 생성자 패턴에 있던 문제는 없고, 작성해야하는 코드의 양이 좀 많아질 수 있지만 ,객체를 생성하기 쉽고 읽기도 쉽다. 하지만 자바빈의 단점은 1회의 함수 호출로 객체 생성을 끝낼 수 없으므로, 객체 일관성(consitency)이 일시적으로 깨질 수 있다.

그래서 자바빈 패턴으로는 변경 불가능 클래스를 만들 수 없다는 것이다.



* Builder 패턴 : 점증적 생성자 패턴의 안전성에 자바빈 패턴의 가독성을 결합한 대안

- 필요한 객체를 직접 생성하는 대신, 클라이언트는 먼저 필수 인자들을 생성자에(또는 정적 팩터리 메서드에)전부 전달하여 빌더 객체(builder object)를 만든다. 그런 다음 빌더 객체에 정의된 설정 메서드들을 호출하여 선택적 인자를 추가해 나간다. 그리고 마지막으로 아무런 인자 없이 build 메서드를 호출하여 변경 불가능 객체를 만드는 것이다.


ex 1) 아래 코드의 사용 방법

 NutritionFacts builder = new NutritionFacts.Builder(240,9).setFat(3).build();

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
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;
        private int fat;
        private int sodium;
        private int carbohydrate;
        
        public Builder(int servingSize, int servings) {
            this.servingSize = servingSize;
            this.servings = servings;
        }
 
        public int getCalories() {
            return calories;
        }
 
        public Builder setCalories(int calories) {
            this.calories = calories;
            return this;
        }
 
        public int getFat() {
            return fat;
        }
 
        public Builder setFat(int fat) {
            this.fat = fat;
            return this;
        }
 
        public int getSodium() {
            return sodium;
        }
 
        public Builder setSodium(int sodium) {
            this.sodium = sodium;
            return this;
        }
 
        public int getCarbohydrate() {
            return carbohydrate;
        }
 
        public Builder setCarbohydrate(int carbohydrate) {
            this.carbohydrate = carbohydrate;
            return this;
        }
        public NutritionFacts build() {
            return new NutritionFacts(this);
        }
    }
    private NutritionFacts(Builder builder) {
        this.servingSize = builder.servingSize;
        this.servings = builder.servings;
        this.calories = builder.calories;
        this.fat = builder.fat;
        this.sodium = builder.sodium;
        this.carbohydrate = builder.carbohydrate;
    }
 
}
cs


ex2) 아래 코드의 사용방법

NutritionFacts build = NutritionFacts.newBuilder().setCalories(32).setCarbohydrate(3).setFat(4).setSodium(4).build();


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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
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 int servingSize;
        private int servings;
        //선택적인자
        private int calories;
        private int fat;
        private int sodium;
        private int carbohydrate;
        
        private Builder() { }
        
        public int getServingSize() {
            return servingSize;
        }
        
        public int getServings() {
            return servings;
        }
        
        public int getCalories() {
            return calories;
        }
 
        public Builder setCalories(int calories) {
            this.calories = calories;
            return this;
        }
 
        public int getFat() {
            return fat;
        }
 
        public Builder setFat(int fat) {
            this.fat = fat;
            return this;
        }
 
        public int getSodium() {
            return sodium;
        }
 
        public Builder setSodium(int sodium) {
            this.sodium = sodium;
            return this;
        }
 
        public int getCarbohydrate() {
            return carbohydrate;
        }
 
        public Builder setCarbohydrate(int carbohydrate) {
            this.carbohydrate = carbohydrate;
            return this;
        }
        public NutritionFacts build() {
            return new NutritionFacts(this);
        }
    }
    private NutritionFacts(Builder builder) {
        this.servingSize = builder.servingSize;
        this.servings = builder.servings;
        this.calories = builder.calories;
        this.fat = builder.fat;
        this.sodium = builder.sodium;
        this.carbohydrate = builder.carbohydrate;
    }
    public static Builder newBuilder() {
        return new Builder();
    }
}
cs


이 코드는 작성하기도 쉽고, 무엇보다 일기 쉽다.

Ada나 Python 같은 언어는 선택적 인자에 이름을 붙일 수 있도록 허용하는데, 그것과 비슷한 코드를 작성할 수 있기 때문이다.


빌더 패턴을 사용하면 불변식(invariant)을 적용할 수 있다. build 메서드 안에 해당 불변식이 위반되었는지 검사할 수 있는 것이다.



Builder 패턴의 장점

1. 빌더 객체는 여러개의 varargs 인자를 받을 수 있다.

2. 빌더 패턴은 유연하다. 하나의 빌더 객체로 여러 객체를 만들 수 있다. 제네릭 자료형 하나면 어떤 자료형의 객체를 만드는 빌더냐에 관계없이 모든 빌더에 적용할 수 있다.

//자료형이 T인 객체애 대한 빌더

public interface Builder<T> {

public T build();

}



정리

- 생성자와 정적 팩터리로 시작하더라도 인자 개수가 통제할 수 없을 정도로 많아지면 빌더 패턴을 적용하자.

- 계속 인자가 증가할 것이라고 예상되면 처음부터 빌더 패턴을 적용하자.

- 빌더 패턴은 인자가 많은 생성자냐 정적 팩터리가 필요한 클래스를 설계할 때, 특히 대부분의 인자가 선택적 인자인 상황에 유용하다.


반응형
profile

거꾸로 바라본 세상

@란지에。

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!