Effective Java | #2. 생성자에 매개변수가 많다면 빌더를 고려하라

보람·2022년 5월 8일
0

Effective-Java

목록 보기
3/25

식품 포장의 영양정보를 표현하는 클래스를 작성한다고 하면 지방, 트랜스지방, 포화지방, 나트륨, 콜레스테롤...등등등 20개가 넘는 선택 항목으로 이뤄진다.
이런 것을 아래와 같은 패턴을 사용한다면..

점층적 생성자 패턴

  • 필요한 매개변수별로 생성자를 추가
  • 매개변수 선언이 private final
  • 매개변수가 많아질수록 클라이언트 코드를 작성하거나 읽기 어려움

자바빈즈 패턴

  • 존재하는 성분별로 세터함수 작성
  • 매개변수 선언이 private
  • 객체 하나를 만들려면 메서드를 여러개 호출해야 함
    • cocaCola.setCalories(100); - 요런것을 10성분이면 10줄 작성해야함😩
  • 객체가 완전히 생성되기 전까지는 일관성이 무너진 상태에 놓이게 된다.
    • 점층적 생성자 패턴에서는 매개변수들이 유효한지를 생성자에서만 확인하면 됐지만 여기서는 그 장치가 완전히 사라진 것

다행히 우리는 점층적 생성자 패턴의 안전성과 자바 빈즈 패턴의 가독성을 겸비한 빌더 패턴을 쓸 수 있어용🌝

빌더 패턴

//빌더 패턴을 사용한 NutritionFacts 클래스 
public class NutritionFacts {
    //private final -> 불변 클래스를 만들기 위함이쥬?
    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 { //빌더 클래스입니다 
        // 필수 매개변수 //아래 두값은 빌더 생성자 호출시 한번만 set하기 때문에 private final
        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;
        }

        //필요시에만 함수 호출하여 정보 넣기
        //빌더 set 함수들은 빌더 본인을 반환하기 때문에 연쇄적으로 호출이 가능하다 calories().fat() 요런식으루!
        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 함수 딱 호출하면 NutritionFacts 클래스 private 생성자 호출가능!
        public NutritionFacts build() {
            return new NutritionFacts(this);
        }
    }

    private NutritionFacts(Builder builder) { 
    //요것이 실제로 우리에게 필요한 객체를 만들수 있는 생성자
    //왼쪽값이 빌더 클래스를 감싸고 있는 NutritionFacts의 매개변수임 
        servingSize  = builder.servingSize;
        servings     = builder.servings;
        calories     = builder.calories;
        fat          = builder.fat;
        sodium       = builder.sodium;
        carbohydrate = builder.carbohydrate;
    }
}
  • 클라이언트는 필수 매개변수로 생성자 호출하여 객체 생성후 필요한 매개변수만 연쇄적으로 설정한 후 build함수를 호출하면 실제 우리에게 필요한 (보통은 불변)객체를 얻을 수 있다.
    • NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8).calories(100).sodium(35).carbohydrate(27).build();
      • 요런식으로 물 흐르듯이 연결된다는 뜻으로 fluent API or method chaining 이라고 함
  • 위 예제에는 유효성 검사 코드는 없지만 build 메서드가 호출하는 생성자에서 여러 매개변수에 걸친 불변식을 검사하는 것이 좋음
    • 프로그램에서의 불변식이라 하면 정해진 기간 동안 반드시 만족해야 하는 조건을 의미한다.
    • 변경을 허용해도 주어진 조건 내에서만 허용한다는 의미

(세가지 방식 모두 클래스 길이가 길긴 매한가지지만 클라이언트에서 연쇄적으로 값을 설정할 수 있는 빌더 패턴을 사용하는 것이 사용성에서는 훨씬 좋아 보인다)

위 예제는 성분 하나만을 위해서 작성된 빌더 패턴이지만 사실 빌더 패턴은

계층적으로 설계된 클래스와 함께 쓰면 좋아요

계층적이라 하면 여러개의 종류의 하위 클래스가 존재하는 추상 클래스같은 것이쥬

//계층적으로 설계된 클래스와 잘 어울리는 빌더 패턴을 사용한 Pizza 추상 클래스
public abstract class Pizza {
    public enum Topping { HAM, MUSHROOM, ONION, PEPPER, SAUSAGE } //필요한 정보만을 추상 클래스에 작성
    final Set<Topping> toppings;

	//재귀적 타입 한정(item-30)을 이용하는 제네릭 타입
    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"를 반환하도록 해야 한다. ==> 형변환하지 않고도 메서드 연쇄를 지원할 수 있다. 
        //==> self 타입이 없는 자바를 위한 이 우회 방법이 simulated self-type 관용구
        protected abstract T self();
    }
    
    //item-50 참조
    Pizza(Builder<?> builder) {
        toppings = builder.toppings.clone();
    }
}
  • (item-50)의 객체 필드 검사

해당 피자 추상 클래스의 하위클래스에는 size를 필수로 받는 뉴욕 피자와 소스를 안에 넣을지 선택하는 칼초네 피자 두개가 있다.

//size를 필수로 받는 뉴욕 피자
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); }
        
        //구체하위클래스인 NyPizza 반환하도록 선언(공변 반환 타이핑)
        @Override public NyPizza build() {
            return new NyPizza(this);
        }
        //뉴욕피자 본인 반환
        @Override protected Builder self() { return this; }
    }
    //private 생성자 - build함수에서 호출됨
    private NyPizza(Builder builder) {
        super(builder);
        size = builder.size; 
        //왼쪽은 Pizza의 size임
    }

    //toString 오버라이드 하여 원하는 문자열 출력 가능
    @Override public String toString() {
        return toppings + "로 토핑한 뉴욕 피자";
    }
}
  • 뉴욕피자는 사이즈를 필수로 받기 때문에 뉴욕 피자 빌더 생성자에서 사이즈를 파라미터로 받고 있음을 확인 할 수 있다.
  • 사이즈 매개변수는 private final임도 확인 가능하다.
  • NyPizza pizza = new NyPizza.Builder(SMALL).addTopping(SAUSAGE).addTopping(ONION).build();
//소스를 안에 넣을지 선택하는 칼초네 피자
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;
    }

    //toString 오버라이드 하여 원하는 문자열 출력 가능
    @Override public String toString() {
        return String.format("%s로 토핑한 칼초네 피자 (소스는 %s에)",
                toppings, sauceInside ? "안" : "바깥");
    }
}
  • 칼초네피자는 선택적 매개변수 sauceInside를 갖고 있으며 필수 값이 없기 때문에 빌더 생성자는 기본생성자로 대체되어 해당 예제에서는 생성자가 보이지 않는다.
  • sauceInside 매개변수가 private 임도 확인 가능하다.
  • Calzone calzone = new Calzone.Builder().addTopping(HAM).sauceInside().build();

공변 반환 타이핑

  • 위 두 예제처럼 하위 클래스의 메서드가 상위 클래스의 메서드가 정의한 반환 타입이 아닌, 그 하위 타입을 반환하는 기능을 공변 반환 타이핑(convariant return typing)
  • T<A>T<B>의 하위타입이면 T가 공변성질을 가졌다고 한다고 한다.

이런 빌더 패턴은요

  • 가변인수 매개변수를 여러 개 사용 가능
  • 생성 비용이 크지는 않지만 성능에 민감한 상황에서는 문제가 될 수 있음
  • 점층적 생성자 패턴보다는 코드가 장황하여 매개변수가 4개 이상은 되어야 값어치를 한다.

핵심 정리

생성자나 정적 팩터리가 처리해야 할 매개변수가 많다면 빌더 패턴을 선택하는게 더 낫다. 매개변수 중 다수가 필수가 아니거나 같은 타입이면 특히 더 그렇다. 빌더는 점층적 생성자보다 클라이언트 코드를 읽고 쓰기가 훨씬 간결하고, 자바빈즈보다 훨씬 안전하다.

profile
백엔드 개발자

0개의 댓글