[JAVA / Design Pattern] 생성자 인자가 많을 때는 Builder 패턴을 적용해보자

정적 팩터리나 생성자는 같은 문제를 갖고 있다.

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

포장 판매되는 음식에 붙어있는 영양 성분표를 나타내는 클래스로 예를 들어보면,

이 성분표에 반드시 포함되어야 하는 항목은 몇 가지 되지 않는다.

총 제공량(serving size), 1회 제공량(servings per container),  1회 제공량당 칼로리(calories per servings) 등이 그런 항목이다.

그러나 선택적인 항목은 무려 20개가 넘는다.  총 지방 함량(total fat), 포화 지방 함량(saturated fat),

트랜스 지방 함량(trans fat)  콜레스테롤 함량(cholesterol) 등이 그런 항목이다.

보통 프로그래머들은 이런 상황에 점층적 생성자 패턴을 적용한다.

필수 인자만 받는 생성자를 하나 정의하고, 선택적 인자를 하나 받는 생성자를 추가하고,

거기에 두 개의 선택적 인자를 받는 생성자를 추가하는 식으로, 생성자들을 쌓아 올리듯 추가하는 것이다.

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

첫번 째 방법은 점층적 생성자 패턴이다.

실제코드가 어떻게 작성되는지 살펴보자.

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
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(servings,servings,0);
    }
    
    public NutritionFacts(int servingSize, int servings,int calories) {
        this(servings,servings,calories,0);
    }
    public NutritionFacts(int servingSize, int servings,int calories,int fat) {
        this(servings,servings,calories,fat,0);
    }
    public NutritionFacts(int servingSize, int servings,int calories,int fat, int sodium) {
        this(servings,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;
    }
        
}
cs

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

1
NutritionFacts cocaCola = new NutritionFacts(240810033527);
cs
그런데 이렇게 하다 보면, 설정할 필요가 없는 필드에도 인자를 전달해야 하는 경우가 생긴다.

위에 코드에서 생성자호출 하는 this 부분 마지막에 0을 적어준것이 그런 사례다.

인자가 6개라서 그다지 흉해 보이지는 않겠지만, 인자 수가 늘어나면 엄청 더러워진다.





두번째의 방법은 getter setter 주는 형식으로 setter를 만들어 필드의 값을 채워주는 

자바빈(JavaBeans) 패턴이다.

자바빈 패턴은 작성해야 코드의 양은 조금 많아질 수 있겠지만, 객체를 생성하기도 쉬우며

읽기도 좋다.

하지만 심각한 단점이 있는데, 1회의 함수 호출로 객체 생성을 끝낼 수 없으므로,

객체 일관성(consistency)이 일시적으로 깨질 수 있다는 것이다.

이 말은 객체를 생성해준 후 setter 작업으로 필드멤버를 변경해주는 작업이 더 들어가기

때문이다. 일관성이 깨진 객체를 사용할 때 생기는 문제는 실제 버그 위치에서 한참 떨어진

곳에서 발생하므로 디버깅 하기도 어렵다.

또한 자바빈 패턴으로 변경 불가능(immutable)클래스를 만들 수 없다는 것이다.

setter 작업으로 인해 필드멤버의 값을 변경 할 수 있어서 그렇다.





점층적 생성자 패턴의 안전성에 자바빈 패턴의 가독성을 결합한 것이 바로 빌더(Builder) 

패턴이다.


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
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 carbohydrate = 0;
        private int sodium = 0;
        
        public Builder(int servingSize, int servings) {
            // TODO Auto-generated constructor stub
            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 carbohydrate(int val) {
            carbohydrate = val;
            return this;
        }
        
        public Builder sodium(int val) {
            sodium = 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;        
    }
}
cs


1
NutritionFacts cocaCola = new NutritionFacts.Builder(2405).fat(5).calories(500).build();
cs
이렇게 사용한다.

calories 부터 sodium 함수는 자기자신을 리턴하므로 계속해서 .함수이름(값) 만

호출함으로써 멤버를 초기화 해줄수 있다.

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

생성자와 마찬가지로, 빌더 패턴을 사용하면 인자에 불변식(invariant)을 적용할 수 있다.

build 메서드 안에서 해당 불변식이 위반되었는지 검사할 수 있는 것이다.

빌더 객체에서 실제 객체로 인자가 복사된 다음에 불변식들을 검사 할 수 있다는 것,

그리고 그 불변식을 빌더 객체의 필드가 아니라 실제 객체의 필드를 두고 검사할 수 있다는 

것 은 중요하다.

불변식을 위반한 경우, build 메서드는 IllegilStateException을 던져야 한다.

이 예외 객체를 살펴보면 어떤 불변식을 위반 했는지도 알아낼 수 있어야 한다.

요약하자면, 빌더 패턴은 인자가 많은 생성자나 정적 팩터리가 필요한 클래스를 설계할 때,

특히 대부분의 인자가 선택적 인자인 상황에 유용하다.

클라이언트 코드 가독성은 전통적인 점층적 생성자 패턴을 따를 때보다 훨씬 좋아질 것이며,

그 결과물은 자바빈을 사용할 때보다 훨씬 안전할 것이다.






스택오버플로우에 DTO에 빌더 패턴을 사용해야되는지 의견을 나누는 글이 있다.

https://stackoverflow.com/questions/31818287/should-i-use-builder-pattern-in-dto






댓글

이 블로그의 인기 게시물

Filter url 제외시키기

[Spring,Java] Validator 구현하기

[Spring] Mock framework에 대하여