본문은 Effective Java를 읽고 정리한 내용을 기반으로 작성된 글입니다.
정적 팩터리와 생성자는 똑같은 제약이 하나 있다. 선택적 매개변수가 많을 때 적절하게 대응하기 어렵다는 점이다.
ex) 식품 포장 영양정보 표현 클래스 ↔ 항목이 엄청 많은데 대부분의 값이 0인 경우
[점층적 생성자 패턴 - 확장하기 어렵다!]
: 필수 생성자 1개, 선택 매개변수를 늘여가며 생성자를 만드는 패턴
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 Main {
public static void main(String[] args) {
NutritionFacts cocaCola = new NutritionFacts(240, 8, 100, 0, 35, 27);
}
}
이 클래스의 인스턴스를 만들려면 원하는 매개변수를 모두 포함한 생성자 중 가장 짧은 것을 골라 호출하면 된다.
- 하지만 설정하길 원치 않는 매개변수까지 포함하여 값을 지정해줘야 함
- 점층적 생성자 패턴도 쓸 수는 있지만, 매개변수 개수가 많아지면 클라이언트 코드를 작성하거나 읽기 어렵다
[자바빈즈 패턴 - 일관성이 깨지고, 불변으로 만들 수 없다!]
: 매개변수가 없는 생성자로 객체를 만든 후, 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 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);
}
}
심각한 단점 🌋
: 객체 하나를 만들려면 메서드를 여러개 호출해야 함 → 객체가 완전히 생성되기 전까지는 일관성이 무너진 상태에 놓이게 된다.
- 매개변수들이 유효한지 생성자를 통해 확인 불가
- 일관성 깨진 객체가 만들어지면 → 버그를 심은 코드와 버그 때문에 런타임을 겪는 코드가 물리적으로 멀리 떨어지게 됨 → 디버깅이 어렵다
따라서 자바빈즈 패턴에서는 클래스를 불변으로 만들 수 없음 → 스레드 안정성을 얻으려면 추가로 freeze메서드 등의 장치를 걸어야 하는데, 이조차 컴파일러가 보증할 방법이 없어서 런타임 오류에 취약하다.
[빌더 패턴]
: Builder를 이용해 필수 매개변수로 객체 생성 → 일종의 setter를 사용하여 선택 매개변수 초기화 → build() 메서드를 호출하여 완전한 객체를 생성하는 패턴
- 점층적 생성자 패턴의 안정성 + 자바빈즈 패턴의 가독성을 겸비함
- 빌더는 보통 생성할 클래스 안에 정적 멤버 클래스로 만들어둔다
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;
}
// 선택 매개변수의 setter
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;
}
// build()
public NutritionFacts build() {
return new NutritionFacts(this);
}
}
}
- 빌더의 setter 메서드들은 빌더 자신을 반환 → 연쇄적으로 호출 가능 ↔ 플루언트 API(fluent API), 메서드 연쇄(method chaining)
☑️ 빌더 패턴은 계층적으로 설계된 클래스와 잘어울린다
ex)
- abstract class Pizza
- NyPizza extends Pizza
- Calzone extends 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();
protected abstract T self();
}
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 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;
}
}
- 각 하위 클래스의 빌더가 정의한 build 메서드는 해당하는 구체 하위 클래스를 반환하도록 선언
- 공변환 타이핑(covariant return typing) : 하위 클래스의 메서드가 사우이 클래스의 메서드에서 정의한 반환 타입이 아닌, 그 하위 타입을 반환하는 기능
☑️ 생성자로는 누릴 수 없는 사소한 이점
- 빌더를 이용하면 가변인수 매개변수를 여러 개 사용할 수 있다
- 메서드를 여러번 호출하도록 하고, 각 호출때 넘겨진 매개변수들을 하나의 필드로 모을 수도 있다.
- 각각 메서드 호출을 통해 속성 설정 → 최종으로 객체 생성할 때 모든 속성 합치기
ex) 메서드를 각각 사용하여 설정 → 직관적인 메서드 체인 구성 가능
Person person = new Person.PersonBuilder()
.setName("Alice")
.setAge(30)
.addAddress("123 Main St")
.addAddress("456 Second St")
.build();
☑️ 빌더 패턴의 단점
- 객체를 만들려면, 빌더부터 만들어야 한다.
- 코드가 장황해서 매개변수가 4개 이상은 있어야 값어치를 한다.
[핵심 정리]
생성자나 정적 팩터리가 처리해야 할 매개변수가 많다면 빌더 패턴을 선택하는 게 더 낫다. 특히
- 매개변수 중 다수가 필수가 아닐 때
- 매개변수 중 다수가 같은 타입일 때 (가독성을 위해)
“애초에 빌더로 시작하는 편이 나을 때가 많다”
'Book > 이펙티브 자바' 카테고리의 다른 글
[Effective Java] item 3+4. private 생성자나 열거 타입으로 싱글턴임을 보증하라, 인스턴스화를 막으려거든 private 생성자를 사용하라 (0) | 2024.01.03 |
---|---|
[Effective Java] item 1. 생성자 대신 정적 팩터리 메서드를 고려하라 (0) | 2023.12.28 |
[Effective Java] item 25. 톱레벨 클래스는 한 파일에 하나만 담으라 (0) | 2023.12.23 |