북극곰의 개발일기

chapter 2 빌더 패턴





posted by purplebeen on Sat Feb 03 2018 21:32:06 GMT+0900 (KST) in JAVA


규칙 2

생성자 인자가 많을 때에는 Builder 패턴 적용을 고려해라.

정적 팩토리나 생성자는 선택전 인자가 많은 상황에서 잘 적응하지 못한다는 문제를 갖고 있다.


package com.purplebeen.chapter2;

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

예를들어 이런 영양표의 값을 가져오는 객체가 있다고 생각해보자.

필수적으로 들어가야하는 요소는 2개이고 나머지 요소들은 선택이다.

이 클래스에 맞는 객체를 생성하기 위해서는 인자 개수에 맞는 생성자를 골라서 호출하면 된다.

그러나 이 경우에는 설정할 필요가 없는 필드의 설정을 위해 인자값을 받는 경우가 생긴다.

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

인자 수가 많을때 점층적 생성자 패턴을 대채할 수 있는 중요한 방법 중 하나는 자바 빈 (JAVA BEAN) 패턴이다.

겟터와 셋터 메소드를 작성한 후 셋터를 호출하여 필수 필드 뿐만 아니라 선택적 필드의 값까지 채우는 방법이다.

그러나 이 방법은 1회의 함수 호출로 객체 생성을 끝낼수 없으므로,

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

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

그래서 대안으로 점층적 패턴의 안전성과 자바 빈 패턴의 가독성을 결합한 빌더(Builder) 패턴 이다.

//자바 빈 패턴 - 일관성 훼손이 가능하고, 항상 변경 가능하다.
public class NutritionFactsWithBuilder {
    //필드는 기본값으로 초기화 (기본값이 있는 경우만)
    private int servingSize = -1;
    private int servings = -1;
    private int calories = 0;
    private int fat = 0;
    private int carbohydrate = 0;
    private int sodium = 0;
    
    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) {
            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 = sodium;
            return this;
        }
    }
    
    private NutritionFactsWithBuilder(Builder builder) {
        servingSize = builder.servingSize;
        servings = builder.servings;
        calories = builder.calories;
        fat = builder.fat;
        sodium = builder.sodium;
        carbohydrate = builder.carbohydrate;
    }
}

Builder 패턴을 이용해서 내부에 static class 형태의 Builder를 만든 다음 빌더에 있는 메소드를 설정해준 후,

객체의 생성자에서 빌더에 있는 값을 가져와서 객체를 생성한다.

메인 메소드에서 이 부분의 객체를 생성하면,

public class Main {

    public static void main(String[] args) {
        NutritionFactsWithBuilder nutrition = new NutritionFactsWithBuilder.Builder(240,8)
                .calories(100).sodium(35) .carbohydrate(27).build();
        System.out.println(nutrition.toString());
    }
}

결과값을 알아보기 위해 객체를 toString() 메소드를 이용하여 String화 하였다.

물론 toString 메소드는 오버라이딩 된 상태이다.

출력시키면,

예상했던데로 입력하지 않은 결과값에 대해서는 0이 나오고, 입력한 결과값은 제대로 값이 들어가서 출력되는 것을 알 수 있다.

빌더 패턴을 이용해서는 불변식 (invariant)을 사용할 수 있다. build() 메소드는 불변식을 위반했는지 알아내어 불변식을 위반한 경우, IllegalStateException을 던져야 한다.

불변객체 : 객체 지향 프로그래밍에 있어서 불변객체(immutable object)는 생성 후 그 상태를 바꿀 수 없는 객체를 말한다.

빌더 패턴의 단점

  • 객체를 생성하려면 우선 빌더 객체를 생성해야 한다.

    → 성능이 중요한 상황에서는 빌더 객체를 만드는 오버헤드가 문제가 될 수도 있다.

  • 빌더 패턴은 점층적 생성자보다 많은 코드를 요구하기 때문에 인자가 충분히 많은 상황 (인자값이 4개 이상) 에서 이용하는 것이 좋다.

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