Effective Java – Consider a builder when faced with many constructor parameters

정적 팩토리와 생성자에는 동일한 제약 조건이 있다. -> 선택적 매개변수가 많을 때 적절히 대응하기 어렵다.

매개 변수가 많아질 경우 사용할 수 있는 세 가지를 고려해 볼 수 있다.

  • 텔레스코핑 생성자 패턴
  • 자바빈즈 패턴
  • 빌더 패턴

(1) Telescoping constructor pattern

필수 매개변수와 선택 매개변수를 갖는 생성자의 형태를 띤다. 아래에 예시를 나타내겠다.

필수 매개변수만 갖는 생성자
필수 매개변수 + 하나의 선택 매개변수 생성자
필수 매개변수 + 두 개의 선택 매개변수 생성자
...
...

위와 같이 필수 매개변수만 갖는 생성자를 생성할 수 있고, n개의 선택 매개변수를 생성하는 생성자를 함께 갖는 경우의 방식이다.

예제) 식품의 영양 정보를 표현하는 클래스를 생각해보자, 식품의 양, 개수, 칼로리, 총 지방, 포화 지방 등 20개 이상의 선택 필드를 가질 수 있다. 몇 가지만 값을 가지고 대부분은 0의 값을 가진다. 생성자를 어떻게 만들까?

public class NutritionFacts{
  private final int servingSize; //required
  private final int servings; //required
  private final int calories; //optional
  private final int fat; //optional
  private final int sodium; //optional
  private final int carbohydrate; //optional

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

위와 같이 오버로딩을 사용하여 나타내는 방식이 점층적 생성자 패턴이라고 한다.

대신 위와 같은 방법을 사용하면 원하지 않은 변수에도 초기값을 설정해야 한다.

오버 로딩 -> 같은 이름이지만 변수의 개수 또는 타입이 달라 여러 개를 정의할 수 있다. 

단점

위처럼 점층적 생성자 패턴도 쓸 수 있지만, 매개변수의 개수가 많아지면 클라이언트 코드 작성이 힘들고 가독성이 떨어진다.

저 생성자 코드를 보면 내가 무엇에게 값을 주는지 알기 어렵고 어떤 파라미터에 값을 입력하는지 주의해서 봐야 한다.

또한, 동일한 타입의 매개변수가 늘어져 있다면 찾기 어려운 버그로 이어질 수 있다. 

클라이언트가 실수해서 다른 생성자를 선택하거나, 알아채지 못할 수 있다.

(2) JavaBeans pattern

매개변수가 없는 생성자로 객체를 만든 후, Setter 메서드를 이용하여 원하는 값을 설정하는 방식

public class NutritionFacts{
  private int servingSize = -1; //required
  private int servings = -1; //required
  private int calories = 0; //optional
  private int fat = 0; //optional
  private int sodium = 0; //optional
  private int carbohydrate = 0; //optional

  public NutritionFacts(){}

  public void setservingSize (int val) { servingSize = val };
  public void setservings (int val) { servings = val };
  public void setcalories (int val) { calories = val };
  public void setfat (int val) { fat = val };
  public void setsodium (int val) { sodium = val };
  public void setcarbohydrate (int val) { carbohydrate = val };

}

위처럼 아까 Telescoping constructor pattern 보다는 생성자 코드가 길지만 보기는 쉽게 변한다는 장점이 있다.

어떻게 매개변수에 setter  메서드가 존재하기에

cocaCola.setServingSize(240); 과 같이 값을 지정해주면 된다.

단점

1. 여러 메소드 호출로 나누어져 인스턴스가 생성되므로, 생성 과정이 이뤄지는 동안 객체가 완전히 생성되기 전까지는 일관성이 무너진 상태에 놓기에 된다.

2. 클래스를 불변으로 만들 수 없어서 프로그래머가 추가 작업을 해주어야 한다.

(3) Builder pattern

-> 점층적 생성자 패턴의 안전성 + 자바 빈즈의 가독성

장점

1. 작성이 쉽고 가독성이 좋다. -> 롬복을 이용하면 더 쉽게 된다.

2. 불변 규칙을 이용할 수 있고, 검사 또한 가능하다. -> IllegalStateException을 통해 예외 처리 가능하다.

단점

1. 어떤 객체를 생성하기 위해 빌더를 만들어야 가능하기에 성능상 문제가 될 수 있다.

2. 매개 변수가 4개 이상이 될 경우 사용하는 것이 좋다. Telescoping 보다 더 긴 코드가 생성될 수 있다.

-> 그래도 Builder pattern을 염두에 두고 코드를 구현해라.

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 = serginsSize;
			this.servings = servings;
		}

		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 NutirionFacts(Builder builder) {
		servingSize = builder.servingSize;
		servings = builder.servings;
		calories = builder.calories;
		fat = builder.fat;
		sodium = builder.fat;
		carbohydrate = builder.carbohydrate;
	}
}

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

정리

생성자나 정적 팩토리가 처리해야 할 매개변수가 많다면 빌더 패턴을 선택하는 게 더 낫다. 매개변수 중 다수가 필수가 아니거나 같은 타입이면 특히 더 그렇다. 빌더는 점층적 생성자보다 클라이언트 코드를 읽고 쓰기가 훨씬 간결하고, 자바 빈즈보다 훨씬 안전하다.

댓글 남기기