본문 바로가기

IT/자바

[Effective Java] Item 2 생성자에 매개변수가 많다면 빌더를 고려하라

반응형

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

정적 팩토리와 public 생성자에는 선택적 매개변수가 많은 경우 대응하기 어렵다는 문제점이 있다.

 

정적 팩토리나 생성자는 3가지 패턴으로 정의할 수 있다. 

1) 점층적 생성자 패턴

2) 자바 빈즈 패턴

3) 빌더 패턴

 

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

클라이언트는 아래처럼 원하지 않는 매개변수까지 포함해야 된다.

NutritionFacts cocaCola = new NutritionFacts(240,8,100,0,35,27);

즉 정의하기 싫은 fat 값을 0으로 넣어야 하는 이슈가 있으며 이는 매개변수가 많아질수록 코드를 작성하기 어렵게 한다.

또한 매개변수 순서가 잘못된 경우에도 컴파일러가 알아차리기 어렵다.

 

2) 자바빈즈 패턴

= 매개변수가 없는 생성자로 객체를 만든 후 Setter를 통해 원하는 매개변수를 설정하는 방식

package jpabook.jpashop.service;

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

클라이언트 한개의 객체를 만들기 위해 여러 메서드를 호출해야 하고, 객체가 완전히 생성되지 전까지는 일관성이 무너진 상태에 놓이게 된다.

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

일관성이 깨진 객체는 오류가 발생하는 부분이 실체 이슈인 부분과 멀리 떨어져있기에 디버깅이 쉽지 않다.

또한 해당 객체를 불변으로 만들 수 없고, 상황에 따라 스레드 안정성도 고려해야 된다.

 

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 NutriFactsBuilder {
      // 필수 매개변수
      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 NutriFactsBuilder(int servingSize, int servings) {
         this.servingSize = servingSize;
         this.servings = servings;
      }

      public NutriFactsBuilder calories(int val) { calories = val; return this; }
      public NutriFactsBuilder fat(int val) { fat = val; return this; }
      public NutriFactsBuilder sodium(int val) { sodium = val; return this; }
      public NutriFactsBuilder carbohydrate(int val) { carbohydrate = val; return this; }
      public NutritionFacts build() { return new NutritionFacts(this);  }
   }

   private NutritionFacts(NutriFactsBuilder builder) {
      servingSize = builder.servingSize;
      servings = builder.servings;
      calories = builder.calories;
      fat = builder.fat;
      sodium = builder.sodium;
      carbohydrate = builder.carbohydrate;
   }
}

클라이언트는 쓰기 쉽고, 읽기도 쉬운 코드를 작성할 수 있다.

유효성 검사가 필요한 경우 빌더의 생성자와 메서드에서 입력 매개변수를 검사할 수 있다.

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

또한 빌더 패턴은 계층적으로 설계된 클래스와 함께 쓰기에 좋다.

 

@Builder 어노테이션을 통해 빌더 패턴을 사용할 수도 있다.

@Builder
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; //선택
    
}
NutritionFacts cocaCola = NutritionFacts.builder().servingSize(240).servings(8).calories(100).sodium(35).carbohydrate(27).build();

빌더 패턴은 계층적으로 설계된 클래스와 함께 쓰기 좋다. 각 계층의 클래스에 관련 빌더를 멤버로 정의하자. 추상 클래스는 추상 빌더를, 구체 클래스는 구체 빌더를 갖게 한다. 다음은 피자의 다양한종류를 표현하는 계층 구조르의 루트에 놓인 추상 클래스이다.

 

Pizza.Builder 클래스는 재귀적 타입 한정을 이용하는 제네릭 타입이다. 여기에 추상 메서드인 self를 더해 하위 클래스에서는 형변환없이 메서드 연쇄를 지원한다.

import java.util.EnumSet;
import java.util.Objects;
import java.util.Set;

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

뉴욕 피자 클래스의 경우 크기를 매개변수로 필요로한다.

import java.util.Objects;

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;
    }
}
NyPizza pizza = new NyPizza.Builder(SMALL).addTopping(SAUSAGE).addTopping(ONION).build();
Calzone calzone = new Calzone.Builder().addTopping(HAM).sauceInside().build();

빌더를 통해 가변인수를 받을 수도 있다. (위 코드처럼 여러번 호출하도록 하고 호출 때 넘겨진 매개변수들을 하나의 필드로 모을 수 있다.

 

단점

1) 빌더 생성 비용이 크지는 않지만 성능에 민감한 상황에서는 문제가 될 수 있다

2) 정층적 생성자 패턴보다는 코드가 장황해서 매개변수가 4개 이상은 되어야 값어치를 한다.

 

정리

생성자나 정적 팩터리가 처리해야 한 매개변수가 많다면 빌더 패턴을 선택하는게 더 낫다.

반응형