제리 devlog

[이팩티브 자바]생성자에 매개변수가 많다면 빌더를 고려하라 본문

Java

[이팩티브 자바]생성자에 매개변수가 많다면 빌더를 고려하라

제리 . 2020. 9. 21. 22:50
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;      //(g/1회 제공량)     선택
  private final int carbohydrate;

}

식품 영양정보를 나타내는 class이다. 필드는 총 6가지가 있다.

1. 점층적 생성자 패턴

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;      //(g/1회 제공량)     선택
  private final int carbohydrate;

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

점층적 생성자 패턴은 필수 매개변수만 받는 생성자, 필수 + 1개 선택 매개변수만 받는 생성자 , 필수 + 2개 선택 매개변수만 받는 생성자로 점차 늘려가는 패턴을 말한다. 이 방식에는 몇가지 단점이 있다.

첫번째로, 매개 변수가 많아질수록 사용자가 혼란스러워진다.

NutritionFacts nutritionFacts = new NutritionFacts(1, 2, 3, 4, 5)

위와같은 형태로 사용자가 인스턴스를 생성할때 매개 변수자리가 바뀌어도 실수를 인지하기 쉽지않다.

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

예를들어, 이런 형태의 생성자를 사용하려고할때, servingSize와 servings의 순서가 헷갈려 값이 잘못 입력될 수 있다. 이런 경우, 오류가 발생하지않기때문에 문제를 찾기 쉽지않다.

두번째로, 사용자가 선택 매개 변수의 값을 지정해야한다.

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

/*
servingSize = 1
servings = 2
fat = 3
*/

NutritionFacts nutritionFacts = new NutritionFacts(1, 2, ?, 3)

필수 매개 변수인 servingSize와 servings이외에 fat의 값을 지정해 인스턴스를 생성하고자 할때, 이 3가지의 매개 변수로 만들어진 생성자가 없다. 모든 경우의 수에 대해서 생성자를 만들 순 없기때문에 이런 상황에서는 사용자가 선택 매개 변수인 calories값을 지정해야한다.

2. 자바빈즈 패턴

public class NutritionFacts {

  private final int servingSize = -1; //(ml, 1회 제공량)   필수
  private final int servings = -1;    //(회, 총 n회 제공량) 필수
  private final int calories = 0;    //(1회 제공량당)      선택
  private final int fat = 0;         //(g/1회 제공량)     선택
  private final int sodium = 0;      //(g/1회 제공량)     선택
  private final int carbohydrate = 0;

  public NutritionFacts() { }

  public void setServingSize(int servingSize) {servingSize = servingSize; }

  public void setServings(int servings) {servings = servings; }

  public void setCalories(int calories) { calories = calories; }

  public void setFat(int fat) {fat = fat; }

  public void setSodium(int sodium) {sodium = sodium; }

  public void setCarbohydrate(int carbohydrate) {carbohydrate = carbohydrate; }

}
NutritionFacts cocaCola = new NutritionFacts();
cocaCola.setServingSize(240);
cocaCola.setServings(8);
cocaCola.setCalories(100);
cocaCola.setSodium(35);
cocaCola.setCarbohydrate(27);

점층적 생성자 패턴의 단점이 자바빈즈 패턴에서는 보이지 않는다. 사용자는 원하는 매개변수면 setter함수를 사용해서 초기화할 수 있다. 하지만 자바빈즈 패턴은 객체가 완전히 생성되기까지 여러번의 메서드를 호출해야하며 일관성이 무너진 상태로 놓인다. 필수 매개 변수인 servingSize, servings가 설정되지 않은채 인스턴스가 존재할 수도 있고 setter함수로 인해 cocaCola객체의 필드 값이 변할 가능성이 있어 객체의 불변성이 깨진다.

3. 빌더 패턴

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;      //(g/1회 제공량)     선택
  private final int carbohydrate;

  public static class Builder {

    private final int servingSize; //(ml, 1회 제공량)   필수
    private final int servings;    //(회, 총 n회 제공량) 필수
    private int calories = 0;    //(1회 제공량당)      선택
    private int fat = 0;         //(g/1회 제공량)     선택
    private int sodium = 0;      //(g/1회 제공량)     선택
    private int carbohydrate = 0;

    public Builder(int servingSize, int servings) {
      this.servingSize = servingSize;
      this.servings = servings;
    }

    public Builder calories(int val) {
      this.calories = val;
      return this;
    }

    public Builder fat(int val) {
      this.fat = val;
      return this;
    }

    public Builder sodium(int val) {
      this.sodium = val;
      return this;
    }

    public Builder carbohydrate(int val) {
      this.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;
  }

}
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8).calories(100).sodium(35).build();

NutritionFacts안에 static Builder클래스가 있다. 기본생성자가 없기때문에 NutrtitionFacts 인스턴스를 생성하기위해 builder를 사용해야한다. Builder 클래스 내부에서 필수 매개 변수인 servingSize와 servings을 생성자로 받고 선택 매개변수를 설정 할 수 있으며 각 메서드에서는 Builder를 반환하므로서 메서드 체이닝이 가능하다. 이 방식을 통해 사용자가 매개 변수의 순서를 헷갈려 잘못 입력되는 문제를 방지할 수 있으며 객체의 불변성을 유지할 수 있다. 또, 한가지 장점으로 build 메서드에서 받은 값이 유효한지 확인하여 IllegalAccessException을 던질 수 있다.

빌더 패턴은 계층적 구조에서도 활용 될 수 있다.

public abstract class Pizza {

    public enum Topping {
        HAM, MUSHROOM, ONION, PEEPER, 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(); 

        protected abstract T self();
    }

    Pizza(Builder<?> builder) {
        toppings = builder.toppings.clone();
    }

}

위와 같은 abstrac Pizza클래스가 있다고 할 때 builder패턴이 활용되는 것을 확인해보자.

Pizzar를 상속받는 클래스들은 빌더를 활용해서 addTopping메서드를 사용할 수 있다. 여기서 return 값이 self()로이고 제네릭 타입으로 리턴된다. 제네릭 타입으로 리턴이 되기 때문에 addTopping은 Pizza를 상속받는 클래스를 return할 수 있고 메서드 체이닝이 가능해진다.

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;
    }
}
NyPizza nyPizza = new NyPizza.Builder(SMALL)
    .addTopping(Pizza.Topping.SAUSAGE)
    .addTopping(Pizza.Topping.ONION)
    .build();

NyPizza클래스에서 build메서드를 오버라이딩하여 Pizza -> NyPizza로 캐스팅하지 않아도 된다. 또한 addTopping은 self()메서드를 통해 NyPizza를 반환한다. 따라서 메서드 체이닝이 가능해진다. 이처럼 계층적 구조에 빌더 패턴을 활용할 수 있다.

매개변수가 적은 클래스에대해 빌더 패턴을 사용하면 오히려 복잡해질 수 있으나 추후 확장 가능성이 있다면 빌더 패턴을 도입하는 것이 좋은 선택지가 될 수 있다. lombok에서는 @Builder 어노테이션을 지원한다. 빌더 클래스를 작성하지 않고도 간단한 빌더 패턴을 사용할 수 있다.

Comments