정적 팩터리와 생성자에는 공통적으로 선택적 매개변수가 많을 때 적절히 대응하기가 어렵다는 제약이 있다.
선택적 매개변수는 객체를 생성할 때 필요에 따라 제공될 수 있는 추가적인 값들로, 이러한 경우 객체 생성 메서드가 복잡해지거나 비효율적일 수 있다.
이 문제를 좀 더 구체적으로 살펴보자
확장하기 어렵다.
예시
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;
}
}
일관성이 깨지고, 불변으로 만들 수 없다.
예시
public 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() {}
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;
}
}
public class Main {
public static void main(String[] args) {
NutritionFacts cocaCola = new NutritionFacts();
cocaCola.setServingSize(240);
cocaCola.setServings(8);
cocaCola.setCalories(100);
cocaCola.setFat(0);
cocaCola.setSodium(35);
cocaCola.setCarbohydrate(27);
}
}
getter
와 setter
메서드가 지나치게 많아지면, 성능 저하를 일으킬 수 있다. 많은 속성을 가진 클래스에서는 getter
와 setter
메서드의 사용이 코드의 복잡성을 증가시킬 수 있다.일관성(consistency)
이 무너진 상태에 놓이게 된다. 이는 버그 때문에 런타임에 문제를 겪는 코드가 버그를 심은 코드와 물리적으로 멀리 떨어져 있기 때문에 디버깅도 어렵다.점층적 생성자 패턴과 자바빈즈 패턴의 장점만 취했다.
불변(immutable)
일 때 유용하며, 클라이언트가 명확하고 가독성 높은 코드를 작성할 수 있게 돕는다.예시
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;
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;
// 필수 매개변수만을 담은 Builder 생성자
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);
}
}
}
빌더를 사용하여 NutritionFacts
객체를 다음과 같이 생성할 수 있습니다. 이 빌더의 setter 메서드는 빌더 자신을 반환하기 때문에 연쇄적으로 호출할 수 있다. (method chaining)
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
.calories(100)
.sodium(35)
.carbohydrate(27)
.build();
아래와 같이 Lombok의 @Builder 어노테이션을 사용하여 NutritionFacts 클래스를 리팩토링 하면 훨씬 코드가 간결해짐
@Builder
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 void main(String[] args) {
NutritionFacts.builder()
.servingSize()
.servings()
.calories()
.fat()
.sodium()
.carbohydrate()
.build();
}
}
Pizza 클래스
public abstract class Pizza{
public enum Topping { HAM, MUSHROOM, ONION, PEPPER, SAUSAGE }
final Set<Topping> toppings;
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();
}
abstract Pizza build();
// 하위 클래스는 이 메서드를 overriding하여 this를 반환하도록 해야 한다.
protected abstract T self();
}
Pizza(Builder<?> builder) {
toppings = builder.toppings.clone();
}
}
NyPizza 클래스
public class NyPizza extends Pizza {
public enum Size { SMALL, MEDIUM, LARGE }
private final Size size;
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
는 Pizza
의 서브 클래스이며, 뉴욕 스타일의 피자를 모델링Calzone 클래스
public class Calzone extends Pizza {
private final boolean sauceInside;
public static class Builder extends Pizza.Builder<Builder> {
private boolean sauceInside = false;
public Builder sauceInside() {
sauceInside = true;
return this;
}
@Override public Calzone build() {
return new Calzone(this);
}
@Override protected Builder self() { return this; }
}
private Calzone(Builder builder) {
super(builder);
sauceInside = builder.sauceInside;
}
}
계층적 빌더를 사용하는 클라이언트 코드 사용 예시
public class Main {
public static void main(String[] args) {
NYPizza pizza = new NYPizza.Builder(SMALL)
.addTopping(SAUSAGE)
.addTopping(ONION)
.build();
Calzone calzone = new Calzone.Builder()
.addTopping(HAM)
.sauceInside()
.build();
}
}
NYPizza
와 Calzone
객체는 빌더 패턴을 사용하여 생성됨. 빌더 패턴을 통해 객체 생성 시 다양한 선택적 매개변수를 유연하게 설정할 수 있다.NYPizza
와 Calzone
객체는 생성 후 변경할 수 없는 불변 객체이다. 이는 객체의 일관성을 유지하며, 멀티스레드 환경에서 안전하게 사용할 수 있다.이 구조는 복잡한 객체를 안전하고 유연하게 생성할 수 있게 해주며, 특히 다양한 옵션을 가진 객체를 만들 때 유용함. 빌더 패턴을 사용하면 코드의 가독성과 유지보수성이 높아지고, 객체 생성 과정에서의 오류를 줄일 수 있다.
생성자나 정적 팩터리가 처리해야 할 매개변수가 많다면 빌더 패턴을 선택하는 게 더 낫다.
특히, 매개변수 중 다수가 필수가 아니거나 같은 타입이면 더욱 그렇다.