본문 바로가기

Java/Java

(JAVA) 생성자에 매개변수가 많다면 빌더를 고려하라

 정적 팩토리 메서드와 생성자의 제약

특정 매개변수만이 필요한데 값을 받는 매개변수에는 원하지 않는 매개변수까지 포함되기 때문에 필요하지 않은 값은 0 혹은 " " 로 지정해야 하는 불편함이 따른다.


점층적 생성자 패턴의 단점

public class NutritionFacts {
    private  final int servingSize;
    private final  int serving;
    private final int calories;
    private final int fat;
    private final int sodium;
    private final int carbohydrate;


    public NutritionFacts(int servingSize, int serving) {
        this(servingSize, serving,0);
    }
    public NutritionFacts(int servingSize, int serving, int calories) {
        this(servingSize,serving,calories,0);
    }

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

    public int getServingSize() {
        return servingSize;
    }

    public NutritionFacts(int servingSize, int serving, int calories, int fat, int sodium, int carbohydrate) {
        this.servingSize = servingSize;
        this.serving = serving;
        this.calories = calories;
        this.fat = fat;
        this.sodium = sodium;
        this.carbohydrate = carbohydrate;
    }
}

이러한 클래스가 있다고 생각해보자.

해당 클래스의 인스턴스를 생성하려면 원하는 매개변수가 있는 생성자중 가장 짧은 것을 선택하기 마련이다.

가장 짧은 값을 선택하더라도 매개변수에 원하지 않은 값이 있을때는 0의 값을 넣어준다.

NutritionFacts nutritionFacts = new NutritionFacts(4,2,0,3);

 

이때 매개변수가 적다면 상관이 없겠지만 만약 매개변수가 많다면 클라언트 코드를 작성하고 읽는데 어려움이 따른다.


대안 1. 자바빈스 패턴(JavaBeans Pattern)

방법 : 디폴트 생성자를 선언한 다음 세터(setter) 메서드를 호출하여 원하는 매개변수 값을 지정하는 방법.

 

public class NutritionFactsJavaBeans {
    private  int servingSize;
    private  int serving;
    private  int calories;
    private  int fat;
    private  int sodium;
    private  int carbohydrate;


    public NutritionFactsJavaBeans() {
    }

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

    public void setServing(int serving) {
        this.serving = serving;
    }

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

 

 하지만 이 방법도 문제가 있다.

1. 인스턴스를 하나 생성하려면 다른 메서드들을 여러 개 호출하야 한다.

객체가 완전히 생성되기 전까지 일관성이 무너진 상태에 놓이게 된다.

2. 런타임 시에 버그가 발생한다면 문제가 발생한 코드는 물리적으로 멀리 떨어져 있으므로 디버깅이 어려워진다.

3. 변수에 final을 사용할 수 없기에 불변 클래스를 만들 수 없다.

4. freeze 메서드를 이용해 스레드 안전성을 보장할 수도 있지만 해당 메서드를 확실히 호출했는지를 컴파일러가 검증할 방법이 없어서 런타임 오류에 취약한 편이라 실전에서 잘 쓰이지 않는다.


대안 2.  빌더 패턴

필요한 객체를 직접 만드는 대신, 필수 매개변수 만으로 생성자를 호출해 빌더 객체를 얻는다.

그리고 세터 메서드를 이용해 원하는 매개변수 값들을 설정한다.

public class NutritionFactsWithBuilder {
    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 NutritionFactsWithBuilder build() {
            return new NutritionFactsWithBuilder(this);
        }
    }

    public NutritionFactsWithBuilder(Builder builder) {
        this.servingSize = builder.servingSize;
        this.servings = builder.servings;
        this.calories = builder.calories;
        this.fat = builder.fat;
        this.sodium = builder.sodium;
        this.carbohydrate = builder.carbohydrate;
    }
}

 

호출

 NutritionFactsWithBuilder cocaCola = new NutritionFactsWithBuilder.Builder(240,8)
                                         .calories(100).build();

 

코드가 처음 봤을 때는 잘 와 닿지 않는다.

  • NutritionFactsWithBuilder 클래스에 필수적으로 입력해야 할 값은 servingSize, servings이다.이를 final로 선언해 두고 
  • 내부 클래스(Builder)에서도 똑같이 final로 선언한 다음 생성자를 이용해서 값을 주입받는다.
  • 필수 값이 아닌 요소들은 Builder를 리턴 값으로 하는 메서드들을 각각 생성한다.
  • 이때 최종적인 인스턴스를 리턴하는 build() 메서드를 생성하는 점에 주의하자!
  • 마지막으로 Builder를 매개변수로 받는 NutritionFactsWithBuilder 생성자를 만들어 내부 클래스에서 생성되었던 값들을 외부 클래스에 선언하였던 final 변수에 주입한다.

 

계층적으로 설계된 클래스와 잘 어울리는 빌더 패턴

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

 

public class MyPizza extends Pizza{
    public enum  Size {SMALL, MEDIUM, LARGE}
    public 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
        MyPizza build() {
            return new MyPizza(this);
        }

        @Override
        protected Builder self() {
            return this;
        }
    }
    private MyPizza(Builder builder) {
        super(builder);
        size = builder.size;
    }
}

 

호출

MyPizza myPizza =  new MyPizza.Builder(SMALL)
                                .addTopping(SAUSAGE).addTopping(ONION).build();

 

빌터패턴은 장점만이 있을까?

빌더 패턴은 상당히 유연하다.

빌더 하나로 여러 객체를 순회하면서 만들 수 있고 빌더에 넘기는 매개 변수에 따라 각자 다른 객체를 생성할 수 있다.

 

단,

객체를 만들려면 그에 앞서 빌더부터 생성하는 것이 우선순위이다.

성능에 민감한 상황에서는 빌더가 민감한 문제가 될 수 있다. (빌더가 생성 비용이 크다는 말은 아니다.)

원하는 매개변수가 4개 정도는 돼야 그 값어치를 할 수 있다.

 

하지만 API는 변화하기 때문에 이전에 만들었던 매개변수가 추가되는 것이 대부분 일어나는 현상이다.

그러니 애초에 빌터패턴으로 만들어버리는 것이 좋은 방법이라고 할 수 있겠다.


References

이펙티브 자바 Effective Java 3/E
국내도서
저자 : 조슈아 블로크(Joshua Bloch) / 이복연(개앞맵시)역
출판 : 인사이트 2018.11.01
상세보기