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

mhyun, Park·2022년 3월 21일
0
post-thumbnail

정적 팩토리 메소드(Item 1)와 기본 public 생성자에는 똑같은 제약이 있다.
그것은, 선택적 매개변수가 많을 때 적절히 대응하기 어렵다는 것 이다.

따라서, 매개변수가 다양하거나 많을 때 우리가 즐겨 사용하는 방법은 아래와 같다.

1. 점층적 생성자 패턴

생성자를
필수 매개변수 1개만 받는 생성자,
필수 매개변수 1개와 선택 매개변수 1개를 받는 생성자,
필수 매개변수 1개와 선택 매개변수 2개를 받는 생성자 등에 형태로 매개변수 개수만큼 생성자를 늘리는 방식

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

위 클래스의 인스턴스를 만들려면 원하는 매개변수를 모두 포함한 생성자 중에 알맞은 생성자를 호출하면 된다
하지만, 선택적 변수가 하나 더 늘어난다면..? 생성자 조합에 따라 걷잡을 수 없이 커지게 된다...

2. 자바 빈즈 패턴

생성자로 객체를 만든 후 setter method를 호출해 원하는 매개변수의 값을 설정하는 방식

public class Person {

	private String name;
    private Integer age = 0;
    private Integer birth = 0;
    
    public void setName(String name){
    	this.name = name;
    }
    
    public void setName(Inteager age){
    	this.age = age;
    }
    
    public void setName(Inteager birth){
    	this.birth = birth;
    }

}
Person person = new Person();

person.setName("Amugae"); 
person.setAge(100); 
person.setBirth(123456);

자바 빈즈 패턴을 통해 읽기 쉽게 인스턴스를 만들 수 있지만,
객체 하나를 만들려면 메서드를 여러개 호출해야 하고, 객체가 완전히 생성되기 전까지는 일광성이 무너진 상태에 놓이게 된다.
즉, 불변 객체를 만들 수 없기 때문에 런타임에 문제되는 코드가 물리적으로 멀리 떨어져 있을 가능성이 높게 된다.

따라서, 앞서 설명한 점층적 생성자 패턴의 안정성과, 자바 빈즈 패턴의 가독성을 겸비한
빌더 패턴 이라는 대안을 설명한다.

3. Builder Pattern

Client는 필요한 객체를 직접 만드는 대신 필수 매개변수만으로 생성자를 호출해 빌더 객체를 얻는다.
그 후 빌더 객체가 제공하는 일종의 setter method들로 원하는 선택 매개변수들을 설정하고, 마지막으로 매개변수가 없는 build 메서드를 통해 우리에게 필요한 객체를 얻는다.

아래와 같이 클래스를 정의할 수 있다.

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 calories){
            this.calories = calories;
            return this;
        }

        public Builder fat(int fat){
            this.fat = fat;
            return this;
        }

        public Builder sodium(int sodium){
            this.sodium = sodium;
            return this;
        }

        public Builder carbohydrate(int carbohydrate){
            this.carbohydrate = carbohydrate;
            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();	// 객체 생성

이렇게, Builder 패턴을 사용하여 우리가 사용할 객체를 불변 객체로 생성하여 운용시킬 수 있다.
이러한 빌더 패턴 코드는 쓰기 쉽고, 읽기도 쉽다. 그리고 이는 계층적으로 설계된 클래스와 함께 쓰기에 좋다.

4. 계층적으로 설계된 곳에서 Builder Pattern 활용

아래 예제와 같은 계층적으로 설계된 곳에서도 Builder 패턴을 활용할 수 있다.

public abstract class Pizza{
    public enum Topping { HAM, MUSHROOM, ONION, PEPPER, SAUSAGE }
    final Set<Topping> toppings;

	// Pizza.Builder 클래스는 재귀적 타입 한정을 이용하는 제네릭 타입이다. 
    // 여기에 추상 메서드인 self를 추가하여 하위 클래스에서는 형변환하지 않고도 메서드 연쇄를 지원할 수 있다.
    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();
        }

		// 각 하위 클래스의 빌더가 정의한 build 메서드는 해당하는 구체 하위 클래스를 반환하도록 선언한다.
        abstract Pizza build();

        protected abstract T self();
    }

    Pizza(Builder<?> builder){
        toppings = builder.toppings.clone();
    }
}
public class NyPizza extends Pizza{
    public enum Size {SMALL, MEDIUM, LARGE }
    private final Size size;

	// super의 builder도 상속받아
    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;
    }
}
NyPizza nyPizza = new NyPizza.Builder(NyPizza.Size.SMALL)
                             .addTopping(Pizza.Topping.SAUSAGE)
                             .addTopping(Pizza.Topping.ONION).build();

이러한 빌더 패턴은 가변인수 매개변수를 여러개 사용할 수 있다는 이점이 있는데, 이는 생성자로는 누릴 수 없는 사소한 이점 중 하나이다.
또한 빌더 하나로 여러 객체를 순회하면서 만들 수 있고, 빌더에 넘기는 매개변수에 따라 다른 객체를 만들 수도 있다.

그러나 단점으로는 객체를 만들기 위해서 Builder부터 만들어야 한다는 것이다.
Builder 생성 비용이 크지는 않지만 성능에 민감한 상황에서는 문제가 될 수 있다.

Item 2 정리

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

profile
Android Framework Developer

0개의 댓글