정정 팩토리 메소드와 public 생성자 모두 매개변수가 많을 경우 불편해진다.
해결책 1. 생성자
점층적 생성자 패턴을 사용할 수도 있으나 가독성이 떨어진다.
*점층적 생성자 패턴
필수 매개변수만 받는 생성자부터 필수 매개변수와 선택 매개변수 1개를 받는 생성자 형태로 선택 매개변수를 전부 다 받는 생성자까지 늘려가는 방식
해결책 2. 자바빈
기본 생성자로 인스턴스를 만들어 setter를 사용해서 필드를 설정하는 것이다.
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 val) { ... }
public void setServings(int servings) { ... }
public void setCalories(int calories) { ... }
public void setFat(int fat) { ... }
public void setSodium(int sodium) { ... }
public void setCarbohydrate(int carbohydrate) { ... }
}
그러나 단점이 많은 ..
객체 하나 만드는 데 메소드를 여러 개 호출해야 하며, 객체가 완전히 생성되기 전까지는 일관성이 무너진 상태에 있게 된다.
이를 완화하는 방법이 freezing이라는데 런타임 오류에 취약해서 거의 안 쓰인다고 한다.
해결책 3. 빌더
빌더는 점층적 생성자 패턴의 안전성과 자바빈의 가독성의 장점을 모두 취한다.
필수 매개변수 만으로 생성자(또는 정적 팩토리)를 호출해 빌더 객체를 얻는다. 이후 빌더 객체가 제공하는 세터 메소드로 선택 매개변수를 설정한다. 마지막으로 매개변수가 없는 build 메소드를 호출해 객체를 얻는다.
빌더는 생성할 클래스 안에 정적 멤버 클래스로 만드는 것이 보통이다.
실제로 개발할 땐 롬복 @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 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;
}
}
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8).
.calories(100)
.fat(35)
.sodium(40)
.carbohydrate(1)
.build();
빌더 패턴은 계층적 구조인 클래스에 특히 적합하다.
추상 빌더를 가지고 있는 추상 클래스를 만들고 하위 클래스에는 추상 클래스를 상속받으며 각 하위 클래스용 빌더도 추상 빌더를 상속받아 만들 수 있다.
public abstract class Pizza {
public enum Topping {
HAM, MUSHROOM, ONION, PEEPER, 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(); // `Convariant 리턴 타입`을 위한 준비작업
protected abstract T self(); // `self-type` 개념을 사용해서 메소드 체이닝이 가능케 함
}
Pizza(Builder<?> builder) {
toppings = builder.toppings.clone();
}
}
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;
}
}
public class Calzone extends Pizza {
private final boolean sauceInside;
public static class Builder extends Pizza.Builder<Builder> {
private boolean sauseInside = false;
public Builder sauceInde() {
sauseInside = 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.sauseInside;
}
}
NyPizza nyPizza = new NyPizza.Builder(SMALL)
.addTopping(Pizza.Topping.SAUSAGE)
.addTopping(Pizza.Topping.ONION)
.build();
Calzone calzone = new Calzone.Builder()
.addTopping(Pizza.Topping.HAM)
.sauceInde()
.build();
물론 빌더도 단점을 가지고 있다. 객체를 만들기 전에 먼저 빌더를 만들어야 하는데 성능에 민감한 상황에서는 그 점이 문제가 될 수 있다. 또 생성자를 사용하는 것보다 코드가 장황하기 때문에 매개변수가 많거나 늘어날 가능성이 있는 경우 사용하는 것이 좋다.
요약
대체로 매개변수가 많다면 빌더 패턴을 선택하는 게 좋다. 빌더는 점층적 생성자보다 클라이언트 코드를 읽고 쓰기가 간결하고, 자바빈보다 안전하다.
'Language > Java' 카테고리의 다른 글
| [effective java] 아이템 4. 인스턴스화를 막으려거든 private 생성자를 사용하라. (0) | 2023.03.02 |
|---|---|
| [effective java] 아이템 3. private 생성자나 열거 타입으로 싱글턴임을 보증하라. (0) | 2023.03.02 |
| [effective java] 아이템 6. 불필요한 객체 생성을 피하라. (0) | 2023.03.01 |
| [effective java] 아이템 1. 생성자 대신 정적 팩터리 메서드를 고려하라. (0) | 2023.03.01 |
| [코딩 테스트를 위한 Java 문법] 입출력 (0) | 2023.01.27 |