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

박상준·2024년 5월 18일
0

이펙티브 자바

목록 보기
2/16

점층적 매개변수 패턴

  • 정적 팩토리와 생성자에는 동일한 제약이 있다고 한다.
  • 둘다 선택적 매개변수가 많은 경우 적절하게 대응하기가 어렵다고 한다.
  • 예를 들어..
    • 영양정보를 표현하는 클래스가 있는 경우

    • 영양정보에는 여러가지 필수 사항 + 여러가지 선택 사항이 존재한다.

      public class NutritionFacts {
          private final int servingSize; // (ml, 1회 제공량) 필수
          private final int servings; // (회, 총 제공량) 필수
          private final int calories; // (1회 제공량당) 선택
          private final int fat; // (g/1회 제공량) 선택
          private final int sodium; // (mg/1회 제공량) 선택
          private final int carbohydrate; // (g/1회 제공량) 선택
      
          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;
          }
      }
    • 해당 방식을 점층적 생성자 패턴(telescoping constructor pattern) 이라고 한다.

    • 필수매개와 선택 매개를 몇개 받는 생성자, 선택을 모두 받는 생성자까지 늘려가는 방식이라고 한다.

    • 해당 클래스의 인스턴를 만들려면

      NutritionFacts cocaCola = new NutritionFacts(240, 8, 100, 0, 35, 27);
      • 과 같이 필요한 매개변수를 모두 포함한 생성자 중에 가장 짧은 것을 골라 호출해야함.
      • 매개변수가 6개 뿐이라 나빠 보이진 않지만… 매개변수가 20개정도가 된다면 엄청나게 혼란스러울 것이 뻔하다.
      • 매개변수의 순서를 일일이 찾아가야하는데, 이는 잘못된 값을 입력할 확률이 매우 높아진다.

또 다른 대안: 자바 빈즈 패턴

설명

  • 매개변수가 없는 기본 생성자로 객체를 생성한 후, 각 필드에 대해 세터로 메서드를 호출하여 값을 설정
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 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);

장점

  • 가독성
    • 코드가 길어지지만, 인스턴스를 만들기 쉽고 이전보다 코드가 읽기 쉬운 코드가 됨
  • 유연성
    • 빈 객체를 생성하고, 원하는 필드만 설정할 수 있다.

단점

  • 일관성 문제
    • 객체를 하나 만들기 위해서 메서드를 여러개 호출해야함.
    • 객체가 완전하게 생성되기 전에 일관성이 무너진 상태가 된다
  • 불변성 문제
    • 일관성을 유지하기 위한 장치가 없음.
    • 객체가 불변이 불가능
  • 스레드 안전성 문제
    • 스레드 안전성 문제 때문에 추가 작업이 필요하다

      public class Main {
          public static void main(String[] args) {
              NutritionFacts facts = new NutritionFacts();
      
              Thread t1 = new Thread(() -> {
                  facts.setServingSize(240);
                  facts.setServings(8);
              });
      
              Thread t2 = new Thread(() -> {
                  facts.setCalories(100);
                  facts.setSodium(35);
              });
      
              t1.start();
              t2.start();
          }
      }
    • 같은 소스에서 다른 스레드에서 set을 동시적으로 수행하는 경우 무슨 값이 객체에 저장될지는 알 수가 없다.

  • 디버깅이 어렵다
    • 일관성이 없는 객체 - 디버깅이 어려움.. 일일이 로직을 따라다녀야함

보완

  • 객체 생성이 끝난 후 수동으로 freezing() 이 가능하다고 한다.
  • 다루기 어려워서 거의 안쓰인다고함.

빌더 패턴(Builder 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;

    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;

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

---

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

장점

  • 안정성
    • 점층적 생성자 패턴처럼 모든 필드 매개변수를 설정해야함.
    • 객체 일관성을 유지
  • 가독성
    • 자바빈즈 패턴처럼 각 필드를 명시적으로 설정가능
  • 유연함
    • 필요에 따라 선택 매개변수를 설정가능
  • 불변성
    • 생성된 객체는 Setter 나 상태변경 메서드를 후속적으로 수행하는게 아닌 이상 불변이다.

플루언트 API || 메서드 체이닝

  • 빌더 패턴의 세터메서드
    public Builder carbohydrate(int val) {
    	carbohydrate = val;
    	return this;
    }
    • 빌더 들은 자기 자신을 반환한다.
    • 메서드 호출을 연쇄적으로 할 수 있다.

계층적 클래스와의 결합

  • 빌더 패턴은 계층적으로 설계된 클래스와 함께 사용하기 좋다.
    • 추상 클래스는 추상 빌더
    • 구체 클래스는 구체 빌더를 가진다.

피자 클래스

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

        protected abstract T self();
    }

    Pizza(Builder<?> builder) {
        toppings = builder.toppings.clone();
    }
}
  • 위는 Pizza 클래스라는 파자의 기본적인 구조를 정의하는 추상 클래스이다.
  • 다양한 종류의 피자를 표현할 수 있는 계층 구조라고 할 수 있다.

NyPizza ( 서브 클래스 )

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

Calzone ( 서브 클래스 )

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;
    }
}
  • 여기서 주의깊게 볼 부분은..
    • Pizza 클래스는 추상 클래스인데, Builder 내부 클래스를 가진다.
    • Builder 는 제네릭 타입을 사용해 타입 안정성을 보장하고 서브 클래스의 빌더와 연결된다.
      abstract static class Builder<T extends Builder<T>> {
    • self() 의 경우 하위 클래스의 빌더가 상위 클래스의 빌더 메서드를 호출한 뒤에도, 연쇄적으로 자신의 메서드를 호출가능하도록 함.
      public T addTopping(Topping topping) {
          toppings.add(Objects.requireNonNull(topping));
          return self(); <- 요 부분을 보면 addTopping 은 상위 클래스의 메서드이지만, 재정의된 self() 를 통해 하위 클래스타입으로 되돌려 받을 수 있음.
      }

빌더 패턴의 단점

  • 빌더 생성 비용
    • 객체를 만들기 전부터 생성해야하기에,, 성능에 민감한 상황에서는 문제가 된다고는 한다.
    • 솔직히 얼마나 문제가 될진 공감이 안감
  • 코드가 좀 장황한 편이다
    • 매개변수가 많으면 쓸데없이 코드가 겁나 길어진다.

최종 정리

  1. 생성자나 정적 팩토리가 처리할 매개변수가 많은 경우, 빌더 패턴이 낫다.
  2. 빌더 패턴은 상당히 간결하다.
  3. 빌더 패턴은 자바빈즈 패턴보다 훨씬 안전하다.
profile
이전 블로그 : https://oth3410.tistory.com/

0개의 댓글