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

Jifrozen·2023년 1월 15일
0

자바 스터디

목록 보기
9/14

1. 점층적 생성자 패턴

생성자 체이닝 방식이라고도 불리는 패턴이다.

기존에 선언한 생성자를 재사용한다.

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 void main(String[] args) {
      NutritionFacts nutritionFacts01 = new NutritionFacts(1, 0);
      NutritionFacts nutritionFacts02 = new NutritionFacts(1, 0, 10);
   }

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

장점

  • 중복 코드를 줄일 수 있다.

단점

  • 확장이 어렵다.
  • IDE 도움이 없다면 각 파라미터에 무슨 값을 집어넣어야하는지 파악하기 어렵다.

2. 자바빈즈 패턴

자바 빈즈는 자바 표준 스택 중 하나로 클래스 필드에 대한 getter, setter 네이밍 패턴을 정의한 표준 스펙 중 하나이다.

기본 생성자로 객체를 생성하고 → setter 메서드를 통해 필드값을 할당한다.

public class NutritionFacts {
    // 기본값이 있다면, 매개변수들을 기본값으로 초기화된다.
    private final int servingSize  = -1;  // 필수, 기본값 없음
    private final int servings     = -1;  // 필수, 기본값 없음
    private final int calories     = 0;
    private final int fat          = 0;
    private final int sodium       = 0;
    private final int carbohydrate = 0;

    public NutritionFact () {}

    // 세터 메서드들
    public void setServingSize(int val) { servingSize = val;}
    public void setServings(int val) { servings = val;}
    public void setCalories(int val) { calories = val;}
    public void setFat(int val) { fat = val;}
    public void setSodium(int val) { sodium = val;}
    public void setCarbohydrate(int val) { carbohydrate = val;}

}
NutritionFacts cocaCola = new NutritionFacts();
// 원하는 매개변수의 값만 설정
cocaCola.setServingSize(240);
cocaCola.setServings(8);
cocaCola.setCalories(100);
cocaCola.setSodium(35);
cocaCola.setCarbohydrate(27);

장점

  • 객체 생성이 간단하고 읽기 쉬운 코드

단점

  • 메서드를 여러개 호출 (setter 메서드)
  • 객체가 생성되기 전까지 일관성이 무너짐 → 불변객체로 만들기 어려움

불변 객체란?
String같은 객체로 한번 값이 정해지면 바뀌지 않는 값이다.
내부 상태가 변경되지 않아 새롭게 할당하지 않기 때문에 성능과 동기화가 간편하다.

3. 빌더 패턴

점층적 생성자 패턴의 안전성과 자바 빈즈 패턴의 가독성을 겸비했다.

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 final int calories;
        private final int fat;
        private final int sodium;
        private final int carbohydrate;

        // 필수 매개변수만 사용
        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 sodium(int val) {
            sodium = val;
            return this;
        }
        public Builder carbohydrate(int val) {
            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;
    }
}

클래스 안에 빌더를 만들고 빌더 안에 클래스와 동일한 필드를 가지도록 만든다.

빌더의 생성자에는 필수로 받아야하는 값을 넣어주고 나머지 세팅을 해야하는 필드들은 일종의 Setter를 만들어 값을 설정하도록 한다.

최종적으로 build() 메서드를 통해 NutritionFacts의 생성자를 호출한다.

NutritionFacts의 생성자는 private 으로 선언했기 때문에 불변 객체이며, builder를 통해서만 생성이 가능하다.

setter와 다르게 public Builder calories(int val) { 선택적 매개변수 메서드는 Builder타입을 리턴하기 때문에 메서드 체이닝이 가능하다.

NutritionFacts cocaCola = new Builder(1, 10).calories(240).sodium(35).build();

Lombok에서 제공하는 @Builder

코드가 복잡하다는 Builder 패턴의 단점을 롬복의 @Builder 를 통해 없앨 수 있다.

@Builder
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;
}
  • 모든 파라미터를 받는 생성자가 생긴다. 이런 경우 외부에서 생성자를 사용할 수 있으므로 외부에서 객체 생성이 가능하다. → Lombok 에서 제공하는 @AllArgsConstructor(access = AccessLevel.PRIVATE)
    설정을 통해 해결할 수 있게 된다.
  • 필수값을 지정해 줄 수 없다. 위 코드의 경우 Builder 생성자에 필수값을 넣어 지정해줬지만 해당 어노테이션 빌더는 불가능하다.

빌더를 계층 구조에서 사용하는 방법

public abstract class Pizza {
   public enum Topping {
      HAM,
      MUSHROOM,
      ONION,
      PEPPER,
      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();

      // 하위 클래스는 이 메서드를 재정의(overriding)하여
      // "this"를 반환하도록 해야 한다.
      protected abstract T self();
   }

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

Pizza라는 추상 클래스가 있다. pizza안에 추상 클래스인 Builder는 자신의 하위 클래스의 타입을 받도록 했다.(재귀적인 타입 제한) self() 메서드에서 상속받은 클래스의 빌더를 리턴하도록 했다.

public class NyPizza extends Pizza{
   public enum Size {
      SMALL,
      MEDIUM,
      LARGE
   }
   private final Size size;

   public static class Builder extends Pizza.Builder<NyPizza.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;
   }
}

pizza 추상 클래스를 상속받아 구현한 NyPizza 클래스이다.

self()는 pizza의 하위클래스를 리턴하기 때문에 뉴욕피자 클래스는 자기 자신을 반환한다. return this

이를 통해 형변환과 같은 별도의 캐스팅없이 builder 사용이 가능하다.

1개의 댓글

comment-user-thumbnail
2023년 1월 16일

확실히 매개변수가 많은 경우에는 Builder 패턴이 편하겠네요! 게시글 잘 봤습니다!!

답글 달기