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

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

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

정적 팩터리 메서드와 생성자는 선택적 매개변수가 많을 경우 적절히 대응하기 힘들다는 단점이 존재한다. 이를 해결하기 위한 방안을 알아보자.

  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;
	}
}
  1. 자바 빈즈 패턴
public class NutritionFacts {

    private final int serving = -1;         // 필수, 기본값 없음
    private final int servings = -1;        // 필수, 기본값 없음
    private final int calories = 0;         // 선택, 기본값 있음
    private final int fat = 0;              // 선택, 기본값 있음
    private final int sodium = 0;           // 선택, 기본값 있음
    private final int carbohydrate = 0;     // 선택, 기본값 있음

    public NutritionFacts() {
    }

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

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

자바 빈즈 패턴이란 기본 생성자로 객체를 만든 후, Setter 메서드를 호출해 원하는 매개변수의 값을 결정하는 방식이다. 인스턴스를 만들기 쉽고, 사용법도 매우 간단하지만 이 방식은 객체가 완전히 생성되기 전까지는 일관성이 무너진 상태에 놓인다는 매우 심각한 단점을 갖고 있다.

 일관성(Consistency)이란 객체의 상태가 프로그램 실행 도중 항상 유효한 상태를 유지해야 한다는 원칙을 말한다. 즉, 객체 내부 상태가 항상 유효한 상태로 일관되게 유지되어야 한다는 뜻이다.

이를 해결하기 위해 생성이 끝난 객체를 수동으로 얼리고’freezing’, 얼리기 전에는 사용할 수 없도록 하는 기법을 있다. 하지만 이 방법은 다루기 어려워 실전에서는 거의 쓰이지 않는다.

  1. 빌더 패턴

점층적 생성자 패턴의 안전성과 자바 빈즈 패턴의 가독성을 갖춘 패턴이다.

  1. 클라이언트는 필요한 객체를 직접 만드는 대신, 필수 매개변수만으로 생성자(혹은 정적 팩터리 메서드)를 호출해 빌더 객체를 얻는다.
  2. 빌더 객체가 제공해주는 일종의 세터 메서드를 통해 원하는 선택 매개변수를 설정한다.
  3. 마지막으로 매개변수가 없는 build() 메서드를 호출하여 우리가 필요한 객체(주로 불변객체)를 얻는다.
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 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 }

        private NutritionFacts(Builder builder) {
            servingSize = builder.servingSize;
            servings = builder.servings;
            calories = builder.calories;
            fat = builder.fat;
            sodium = builder.sodium;
            carbohydrate = builder.carbohydrate;
        }
}
  • 빌더는 생성할 클래스 안에 정적 멤버 클래스로 만들어둔다.
  • 빌더의 세터는 자기 자신을 반환하기 때문에 연쇄적으로 호출이 가능한데 이를 메서드 체이닝이라 한다.

단점

  • 객체를 만들기에 앞서 빌더부터 만들어야 한다. (실무에서는 Lombok의 @Builder 어노테이션을 사용하면 실무에서는 이런 단점을 극복할 수 있다. 하지만 필수 값을 만들 수 없다.)
  • 생성 비용이 크지는 않지만 성능에 민감한 상황에서는 영향을 끼칠 수 있다.
  • 코드가 장황해서 매개변수가 4개 이상은 돼야 값어치를 한다는 단점도 존재한다.