Item 2. 생성자 인자가 많을 때는 Builder Pattern 고려하기

2019. 3. 7. 13:10Dev/Effective Java

300x250
반응형

객체를 생성할 때 반드시 필요한 필수 인자와 옵션 인자.

이때, 옵션 인자가 너무 많을 경우 어떤 형태로 객체를 생성하는게 좋을까요?

  1. 점층적 생성자 패턴 ( telescoping constructor pattern)
  2. 자바빈 패턴 ( javabean pattern )
  3. 빌더 패턴 ( Builder pattern )

이 중에 빌더 패턴을 사용하기를 권장합니다!
나머지 두개의 패턴이 어떤 형태인지 보고, 왜 빌더 패턴을 권장하는지 알아보도록 합시다.


1. 점층적 생성자 패턴

필수 인자를 가진 생성자부터 시작해서, 선택적으로 필요한 인자들을 하나씩 추가한 점층적 생성자 패턴


영양 성분표 클래스를 예시로 들어보면
영양 성분표 중 servingSize, servings만 필수 인자이고 나머지는 옵션적으로 들어옵니다.

영양 성분표는 한번 생성되면 변경되지 않으니 각각 final 로 필드값을 지정할합니다. ( 변경 불가능한(immutable) 클래스로 생성 )

그리고 AllArgsConstructor에서 각 필드들을 초기화 하며,
다른 constructor에서는 AllArgsConstructor를 이용하는 형태로 만듭니다. 아래와 같이!

public class NutritionFacts {
    private final int servingSize;	//총 제공량
    private final int servings;	// 1회 제공량
    private final int calories;	// 1회 제공량당 칼로리
    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;
    }
}

사용할 때에는

NutritionFacts nf = new NutritionFacts(100, 300, 0, 10);

이렇게 사용하면 됩니다.


단점

  1. 설정할 필요없는 인자도 전달해야하는 경우가 있습니다.
  2. 인자가 너무 많아서 사용하기도 번거롭고 인자를 잘못된 위치에 넣어서 에러를 초래할 수 있습니다.

2. 자바빈 패턴

자바빈 패턴은 우리가 DTO 클래스를 정의 할 때, 자주 이용하고 있는 형태로,
NoArgsConstructor 호출로 객체 생성 후, 필요한 필드값을 setter로 설정합니다.

public class NutritionFacts {
    private int servingSize;	//총 제공량
    private int servings;		// 1회 제공량
    private int calories;	        // 1회 제공량당 칼로리
    private int fat;		// 총 지방함량
    private int sodium;		// 총 나트륨 함량
    private int carbohydrate;	// 총 탄수화물 함량

    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 nf = new NutritionFacts();
nf.setServingSize(100);
nf.setServings(300);
nf.setFat(10);
System.out.println(nf);

단점

  1. 1번의 함수 호출로 객체 생성을 끝낼 수 없다.
    : 객체의 일관성(consistency)가 일시적으로 깨질 수 있다.
  2. 변경 불가능(immutable) 클래스를 만들 수 없다.
    : setter를 통해 나중에 값을 설정하는 형태이기 때문에 final 설정이 불가능 하다.

3. 빌더 패턴

점층적 생성자 패턴의 안전성과 자바빈 패턴의 가독성의 장점을 합친 것이 빌더 패턴입니다.

public class NutritionFacts {
    private final int servingSize;	//총 제공량
    private final int servings;		// 1회 제공량
    private final int calories;		// 1회 제공량당 칼로리
    private final int fat;			// 총 지방함량
    private final int sodium;		// 총 나트륨 함량
    private final int carbohydrate;	// 총 탄수화물 함량

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

NutritionFacts 클래스 내에 필드들이 모두 final 처리 되어있는 것은 점층적 생성자 패턴과 같으나,
내부에 Builder라는 public static 클래스가 정의되어있는 것 부터 다릅니다.

Builder내의 필드는
필수 인자는 final 처리하고, (불변!) , 옵션 인자는 final처리를 하지 않습니다.

그리고, Builder의 servingSize, servings 는 생성자로 초기화를 시키고 나머지 옵션들은 설정메서드를 통해 설정을 하는데
이때 설정메서드의 리턴값은 Builder 자신이기 때문에 체인처럼 쭉쭉 이어서 쓸 수 있습니다.

바로 아래와 같은 형태로 사용할 수 있습니다.

new NutritionFacts.Builder(100, 300).calories(420).fat(10).sodium(20).carbohhydrate(90)

여기서는 각 설정메서드에 인자값들이 1개씩 들어있지만, (마치 setter 처럼)
필요에 따라 여러개씩 써서 한꺼번에 설정하는 것도 됩니다.

빌더 패턴은 유연합니다.
하나의 빌더 객체로 여러개의 객체를 생성할 수 있고,
설정 메서드를 호출하여 다음에 생성할 객체의 필드값을 바꿀 수도 있습니다.

자세한 사항은 아래의 코드를 보면 알 수 있습니다.

Builder builder = new NutritionFacts.Builder(100, 300);
NutritionFacts nf1 = builder.sodium(30).build();
builder.calories(500);

NutritionFacts nf2 = builder.build();
builder.fat(20).sodium(10);

NutritionFacts nf3 = builder.build();
NutritionFacts nf4 = builder.build();

System.out.println(nf1);
System.out.println(nf2);
System.out.println(nf3);
System.out.println(nf4);

builder.calories(500)을 호출함으로써 그다음에 생성하는 객체인 nf2, nf3, nf4의 칼로리 필드값이 500으로 설정된 것을 알 수 있습니다


출력 결과값>

NutritionFacts [servingSize=100, servings=300, calories=0, fat=0, sodium=30, carbohydrate=0]
NutritionFacts [servingSize=100, servings=300, calories=500, fat=0, sodium=30, carbohydrate=0]
NutritionFacts [servingSize=100, servings=300, calories=500, fat=20, sodium=10, carbohydrate=0]
NutritionFacts [servingSize=100, servings=300, calories=500, fat=20, sodium=10, carbohydrate=0]
300x250
반응형