본문 바로가기

책리뷰/이펙티브자바

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

728x90
반응형

정적 팩터리와 생성자는 선택적 매개변수가 많으면 대응하기 어렵다. 

영양 정보를 제공하는 클래스이다. 

점층적 생성자 패턴 - 확장이 어려움


pubic 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 soduim) {
    	this(servingSize, servings, calories, fat, soduim, 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.sodium 	  = sodium;
        this.carbohydrate = carbohydrate;
    }
}

점층적 생성자 패턴을 쓸 수 있지만 매개변수가 많아지면 클라이언트 코드를 작성하거나 읽기 어렵다. 

각 값의 의미가 무엇인지 헷갈리고 매개변수가 몇 개인지도 주의해서 작성해야 한다. 

 

자바빈즈 패턴 - 일관성이 깨지고, 불변으로 만들 수 없다.


매개변수가 없는 생성자로 객체를 만든 후 세터메서드들을 호출해 원하는 값을 설정하는 방식이다. 

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(240);
cocaCola.setServing(8);
cocaCola.setCalories(100);
cocaCola.setSodium(35);
cocaCola.setCarbohydrate(27);

하지만 객체 하나를 만들려면 여러 개의 메서드를 호출해야하고 객체가 완전히 생성되기 전까지 일관성이 무너진 상태에 놓이게 된다. 

생성자 패턴은 매개변수들이 유효한지를 생성자에서만 확인하면 일관성을 유지할 수 있는데 자바빈즈는 어디서든 set할 수 있기 때문에 일관성이 무너진다. 

 

빌더 패턴 - 점층적 생성자 패턴과 자바빈즈 패턴의 장점


클라이언트가 직접 객체를 만드는 대신 필수 매개변수만으로 생성자를 호출해 빌더 객체를 얻는다. 

그 후 세터메서드들을 사용해서 매개변수를 설정한다.

마지막으로 매개변수가 없는 build메서드를 호출해서 객체를 얻는다. 

public class NutritionFacts {
    private int servingSize;
    private int servings;
    private int calories;
    private int fat;
    private int sodium;
    private 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클래스는 불변이고 빌더의 세터 메서드들은 빌더 자신을 반환했기 때문에 연쇄적으로 호출할 수 있다.

(플루언트 API or Method Chaining)

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

위처럼 읽기 쉬운 코드로 사용할 수 있다. 불변성을 보장하려면 매개변수 복사후 객체 필드도 검사해야한다. 

 


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

public abstract class Pizza {

    public enum Topping {
        HAM, MUSHROOM, ONION, PEEPER, SAUSAGE
    }

    final Set<Topping> toppings;

    abstract static class Builder<T extends  Builder<T>> { // 재귀적 타입 한정을 이용하는 제네릭 타입 
    	// 처음 toppings는 비어있는 타입으로 선언
        EnumSet<Topping> toppings = EnumSet.noneOf(Topping.class);

		// addTopping으로 추가할 수 있도록 한다. 
        public T addTopping(Topping topping) {
            toppings.add(Objects.requireNonNull(topping));
            return self();
        }

        abstract Pizza build(); // Convariant 리턴 타입이 서브클래스라는 범위안에서 다양함 

		// 하위클래스는 이 메서드를 재정의하여 this를 반환하도록 해야함 
        protected abstract T self(); // self-type개념을 사용해서 메소드 체이닝이 가능케 함
    }

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

}

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<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를 상속받아서 Size를 추가한 NyPizza클래스다.

Builder 구현시 Pizza.Builder<Builder>를 상속받아서 구현하고 Objects.requireNonNull메서드로 필수파라미터 Size를 세팅한다. 

build()메서드로 NyPizza객체를 생성하면서 자기자신을 리턴한다. 

self()로 NyPizza빌더를 리턴한다.

생성자에 super(builder);를 통해 토핑을 세팅하고 size를 세팅하는 코드다. 

public class Calzone extends Pizza {

    private final boolean sauceInside;

    public static class Builder extends Pizza.Builder<Builder> {
        private boolean sauseInside = false;

        public Builder sauceInde() {
            sauseInside = 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.sauseInside;
    }

}

sourceInside()를 호출하면 sourceInside가 true로 바뀌면서 다시 빌더를 리턴함으로써 메서드체이닝이 가능하게 한다. 

나머지는 위코드와 같음.

 

각각 하위 클래스의 빌더가 정의한 build 메서드는 해당 구체 하위 클래스를 반환한다. 

클라이언트는 형변환헤 신경쓰지 않고 빌더를 사용할 수 있다. 

 

NyPizza nyPizza = new NyPizza.Builder(SMALL)
    .addTopping(Pizza.Topping.SAUSAGE)
    .addTopping(Pizza.Topping.ONION)
    .build();

Calzone calzone = new Calzone.Builder()
    .addTopping(Pizza.Topping.HAM)
    .sauceInde()
    .build();

가변인수 매개변수 여러개 사용이 가능하고. 호출 시 넘겨진 매개변수들을 하나의 필드로 모을 수 있다.(addToping)

 

생성자나 정적 팩터리가 처리해야할 매개변수가 많다면 빌더패턴을 선택하는 게 낫다. 
매개변수 중 다수가 필수가 아니거나 같은 타입일 경우 더욱 그렇다. 
점측정 생성자보다 코드를 읽고 쓰는데 간결하며 자바빈즈보다 안전하다. 
728x90
반응형