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

2022. 5. 13. 13:46JAVA/Effective Java

정적 팩토리와 생성자가 가지고 있는 제약

  • 선택적인 매개변수가 많을 때 적절히 대응하기가 어려움
  • 예를 들어 식품 포장의 영양정보가 20개정도가 된다면 필수적인 정보 몇개를 제외하고는 대부분의 정보들은 0 값을 가지고 있는 경우가 많음
  • 이러한 제약을 해결하기 위해서 점층적 생성자 패턴을 사용할 수도 있지만 매개변수가 많아질수록 코드를 작성하기 어렵거나 읽기 어려워지는 단점을 가지고 있습니다.

 

점층적 생성자 패턴(telescoping constructor pattern)

점층적 생성자 패턴은 필수 매개변수가 들어있는 생성자에서부터 출발하여 선택 매개변수를 늘려나가면서 생성자들을 오버로딩 방식으로 늘려가는 방식입니다.

public class NutritionFacts {
    private int servingSize;        // (mL)                 필수
    private int servings;           // (per container)      필수
    private int calories;           // (cal)                선택
    private int fat;                // (g)                  선택
    private int sodium;             // (mg)                 선택
    private int carbohydrate;       // (g)                  선택
	
	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;
	}

	@Override
	public String toString() {
		return "NutritionFacts [servingSize=" + servingSize + ", servings=" + servings + ", calories=" + calories
				+ ", fat=" + fat + ", sodium=" + sodium + ", carbohydrate=" + carbohydrate + "]";
	}
}

위와 같이 정의한 생성자는 사용자가 설정하길 원치 않는 매개변수까지 포함하기 쉬운데, 어쩔 수 없이 그런 매개변수에도 값을 지정해줘야 합니다. 위와 같은 패턴은 인스턴스를 생성시 각 값의 의미와 순서가 헷갈리고 매개변수가 몇개인지도 주의해서 세어보아야 합니다.

 

자바빈즈 패턴(javabeans pattern)

자바빈즈 패턴은 매개변수가 없는 공백 생성자로 인스턴스를 생성한후 setter 메서드를 통해서 필드멤버를 설정하여 인스턴스를 완성하는 패턴입니다.

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() {
	}
	
	// 설정자(Setter)
	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;
	}

	@Override
	public String toString() {
		return "NutritionFacts [servingSize=" + servingSize + ", servings=" + servings + ", calories=" + calories
				+ ", fat=" + fat + ", sodium=" + sodium + ", carbohydrate=" + carbohydrate + "]";
	}
}
	@Test
	void javaBeansPatternTest() {
		NutritionFacts nutri = new NutritionFacts();
		nutri.setServingSize(300);
		nutri.setServings(1);
		nutri.setCalories(200);
		nutri.setFat(20);
		nutri.setSodium(10);
		nutri.setCarbohydrate(5);
		
		Assert.assertEquals("NutritionFacts [servingSize=300, servings=1, calories=200, fat=20, sodium=10, carbohydrate=5]", nutri.toString());
	}

점층적 생성자 패턴의 단점인 각각의 필드멤버의 값의 의미와 순서를 헷갈릴 필요가 없습니다. 하지만 자바빈즈 패턴에서는 인스턴스 하나를 생성하기 위해서 메서드(setter)를 여러 개 호출해야 하고, 인스턴스가 완전히 생성되기 전까지는 일관성(consistency)이 무너진 상태로 놓이게 됩니다. 그리고 setter 메서드를 사용하기 때문에 클래스를 불변 클래스로 만들 수 없습니다.

 

빌더 패턴(builder pattern)

빌더 패턴은 클라이언트가 필요한 인스턴스를 직접 생성하는 대신, 필수 매개변수만으로 생성자(혹은 정적 팩토리)를 호출해 빌더 인스턴스를 얻고 빌더 객체가 제공하는 일종의 세터(setter) 메서드로 원하는 선택 매개변수들을 설정하는 패턴입니다.

 

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 int servingSize;
		private 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); //NutritionFacts3 클래스 생성자에 builder 객체를 인자로 넣음
		}
		
	}
	
	public NutritionFacts(Builder builder)
	{
		servingSize = builder.servingSize;
		servings = builder.servings;
		calories = builder.calories;
		fat = builder.fat;
		sodium = builder.sodium;
		carbohydrate = builder.carbohydrate;
	}

	@Override
	public String toString() {
		return "NutritionFacts [servingSize=" + servingSize + ", servings=" + servings + ", calories=" + calories
				+ ", fat=" + fat + ", sodium=" + sodium + ", carbohydrate=" + carbohydrate + "]";
	}	
}
	@Test
	void builderPatternTest() {
		NutritionFacts nutri = new NutritionFacts.Builder(300, 1)
                                                   .calories(200)
                                                   .fat(20)
                                                   .sodium(10)
                                                   .carbohydrate(5)
                                                   .build();
		
		Assert.assertEquals("NutritionFacts [servingSize=300, servings=1, calories=200, fat=20, sodium=10, carbohydrate=5]", nutri.toString());
	}

 

References

Source code : https://github.com/yonghwankim-dev/effective_java
[도서] Effective Java