본문 바로가기

Effective Java/객체 생성과 파괴

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

들어가기전 

이번 포스팅은 책에 나와있는 예제를 보고만 넘어 가기에는 빌더패턴을 왜 ? 꼭? 써야 되는지 정확하게 이해가 되지 않았기 때문에 조금 더 깊게 생각해볼 필요가 있을 것 같다. 빌더를 고려하라는 취지는 충분히 이해 했지만 만약에 실무에서 빌더를 이용해 설계를 해야된다면 ?? 지금은 절대로 설계가 불가능 하기 때문이다.  학습 목표는 빌더 패턴을 실무에 적용할 수 있을만한 필요성을 느끼고 상황에 맞게 잘 적용할 수 있도록 코드를 작성하며 느껴보기!!

 

 

학습 요약

  • 빌터패턴(Builder pattern)을 사용해야 되는이유
  • 점층적 생성자 패턴 (telescoping construcor pattern) 과 자바빈즈 패턴 (JavaBeans pattern ) 
  • 빌터패턴 적용실습
  • 계층적으로 설계된 클래스와 잘 어울리는 빌더 패턴 적용실습 ( 소스 분석중 뇌정지옴 이 부분만 따로 포스팅 할 계획임

 

빌더패턴(Builder pattern)을 사용해야 되는이유

- 책의 내용에는 생성자의 매개변수가 늘어나면 개발자는 점층적 생성자 패턴자바빈즈 패턴 을 사용하는데 둘다 단점이 있기 때문에 이 둘의 장점만을 모아서 만든 빌더패턴을 사용하라고 하는데... 말로만 들어서는 뇌정지가 올것 같다. 

백문이 불여일타!  코드로 알아보자

 

우선 책에 나오는 예제를 먼저 보자

 점층적 생성자 패턴

(telescoping construcor pattern) 과 자바빈즈 패턴 (JavaBeans pattern ) 

 

점층적 생성자 패턴 

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;

// 1단계
    public NutritionFacts(int servingSize, int servings) {
        this(servingSize, servings, 0);
    }

// 2단계
    public NutritionFacts(int servingSize, int servings, int calories) {
        this(servingSize, servings, calories, 0);
    }

// 3단계
    public NutritionFacts(int servingSize, int servings, int calories, int fat) {
        this(servingSize, servings, calories,  fat, 0);
    }

// 4단계
    public NutritionFacts(int servingSize, int servings, int calories, int fat, int sodium) {
        this(servingSize, servings, calories,  fat,  sodium, 0);
    }

// 5단계
    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 클래스를 이용해서 콜라 객체를 만들었고 콜라에 들가는 식품 정보를 넣어주고있다.
    public static void main(String[] args) {
        NutritionFacts cocaCola = new NutritionFacts(240810003527);
    }
}
cs

 

장점 : 생성자 오버로드를 이용해서 2개부터 6개까지 필요에 따라서 생성 할 수 있다. 사용자 원하는 매개변수가 모두 포함된 생성자를 호출하면 된다.

 

단점 : 전달하는 파라미터가 늘어날 수록 점점 더 복잡하고 수 많은 생성자를 오버로드 하게 된다. 그리고 설계한 사람이 여러가지 업무적인 부분을 잘 판단해서 순서대로 파라미터를 나열했다고 하더라도 추후에 외부의 요인으로 파라미터 사용 순서라 바뀌게된다면 우리는 최소한의 파라미터를 사용하는 생성자가 아니고 최악의 경우 파라미터가 가장 많은 생성자를 호출하면서 처음과 맨 끝의 값만을 초기화 할 수도 있다. 또한 파라미터마다 모두 같은 이름이기 때문에 생성자를 호출할 때 가독성이 줄어들고 사용자를 고민을 하면서 사용하게 될 것이다.

 

그렇다면 이번에는 가독성이 좋고 파라미터가 늘어나도 상관 없도록 만들수 있는 패턴을 적용해 보자

 

자바빈즈 패턴

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() {}
 
    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;
    }
 
 
    public static void main(String[] args) {
        NutritionFacts cocaCola = new NutritionFacts();
        cocaCola.setServingSize(240);
        cocaCola.setServings(8);
        cocaCola.setCalories(100);
        cocaCola.setFat(0);
        cocaCola.setSodium(35);
        cocaCola.setCarbohydrate(27);
    }
}
cs

 

장점 : 앞서 작성한 점층적 생성장패턴의 단점이 더이상 보이지 않는다. 객체를 생성할 때 매개변수를 고려하지 않고 쉽게 만들 수 있고 사용하고 싶은 값만을 초기화 할 수 있어서 가독성에도 매우 좋아 보인다.

 

단점 : 객체 하나를 만드는데 메소드를 여러번 호출해야된다. 또한 원하는 값이 모두 set되기 전에는 일관성이 무너진 상태에 놓이게 된다. 또한 클래스를 불변 으로 만들 수 없기 때문에 켑슐화가 깨지게 된다.(조금 복잡하지만 freeze라고 해서 생성이 완료된 객체를 얼리는 작업을 하는 메소드를 만들어서 변하지 않도록 할 수 있지만 이또한 복잡한 작업이고 혹시라도 생성을 마치고 나서 freeze를 하지 않는다면 이또한 찾기 어려운 버그가 될 것으로 예측된다.)

 

 

빌터패턴 적용실습

 

자 이제 우리가 공부하려고 하는 빌터 패턴이 나올 차례다. 책에서는 위에서 설명한 패턴의 장점만을 가지고 만든어 놓은 패턴 이라고 한다. 

 

빌더 패턴

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;
 
    private NutritionFacts(Builder builder) {
        servingSize = builder.servingSize;
        servings = builder.servings;
        calories = builder.calories;
        fat = builder.fat;
        sodium = builder.sodium;
        carbohydrate = builder.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;
 
        // 필수 매개변수만을 담은 Builder 생성자
        public Builder(int servingSize, int servings) {
            this.servingSize = servingSize;
            this.servings = servings;
        }
 
        // 선택한 메소드의 값을 set 하고 Builder 자신을 return 해주므로 메소드체이닝을 사용할 수 있다.
        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;
        }
        
        // build() 호출로 최종 불변 객체를 얻는다.
        public NutritionFacts build() {
            return new NutritionFacts(this);
        }
    }
 
    public static void main(String[] args) {

        NutritionFacts cocaCola = new NutritionFacts.Builder(2408)
.calories(100)
.sodium(35)
.carbohydrate(30)
.build();
    }
}
cs

장점 : 매개변수의 증가에 따른 가독성의 복잡성이 증가하지 않고 필수 값을 제외한 값은 필요에 따라서 메소드 체이닝을 이용해 언제든 유연하게 값을 설정할 수 있다. 

빌더 패턴을 실무에 적용해서 만들고 싶어졌다.   내가 체이닝을 사용했던 적이 있는데 Spring Security 에서 configure메소드를 Override해서 화면마다 권한을 주고 필요에 따라서 로그인페이지 등 여러가지를 유연하게 사용할 수 있었다.

다만 내부적으로 빌더 패턴을 사용하는 것인지는 모르겠지만 체이닝을 사용하므로 편리했던 기억이 난다.

 

계층적으로 설계된 클래스와 잘 어울리는 빌더 패턴 적용실습

public abstract class Pizza{
    public enum Topping { HAM, MUSHROOM, ONION, PEPPER, SAUSAGE }
    final Set<Topping> toppings;
 
    
    abstract static class Builder<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를 반환하도록 해야 한다.
       protected abstract T self();
    }
 
    Pizza(Builder<?> builder) {
       toppings = builder.toppings.clone();
    }
 }
 
 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;
    }
 }
 
 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;
       }
 
       @Override public Calzone build() {
          return new Calzone(this);
       }
 
       @Override protected Builder self() { return this; }
    }
 
    private Calzone(Builder builder) {
       super(builder);
       sauceInside = builder.sauceInside;
    }
 
 
    public static void main(String[] args) {
      NYPizza pizza = new NYPizza.Builder(SMALL)
              .addTopping(SAUSAGE)
              .addTopping(ONION)
              .build();
 
      Calzone calzone = new Calzone.Builder()
              .addTopping(HAM)
              .sauceInside()
              .build();
  }
 
 }
cs