2023. 11. 12. 20:06ㆍBook/이펙티브 자바
객체 생성과 파괴
정적 팩토리와 생성자에는 똑같은 제약이 하나 있다. 선택적 매개변수가 많을 때 적절히 대응하기 어렵다는 점이다.
이런 클래스용 생성자 혹은 정적 팩토리를 살펴보자
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, 0, 0, 0);
}
public NutritionFacts(int servingSize, int servings, int calories) {
this(servingSize, servings, calories, 0, 0, 0);
}
public NutritionFacts(int servingSize, int servings, int calories, int fat) {
this(servingSize, servings, calories, fat, 0, 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 cocaCola = new NutritionFacts(240, 8, 100, 0, 35, 27);
단점
- 매개변수가 많아지면 클라이언트 코드를 작성하거나 읽기 어렵다.
- 클라이언트가 실수로 매개변수의 순서를 바꿔 건네줘도 컴파일러는 알아채지 못할 수 있다.
2) 자바빈즈 패턴
public class NutritionFacts2 {
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 void setServingSize(int val) {
servingSize = val;
}
public void setServings(int val) {
servings = val;
}
public void setCalories(int val) {
calories = val;
}
public void setFat(int val) {
fat = val;
}
public void setSodium(int val) {
sodium = val;
}
public void setCarbohydrate(int val) {
carbohydrate = val;
}
}
NutritionFacts2 cocaCola = new NutritionFacts2();
cocaCola.setServingSize(240);
cocaCola.setServings(8);
cocaCola.setCalories(100);
cocaCola.setSodium(35);
cocaCola.setCarbohydrate(27);
단점
- 객체 하나를 만들기 위해 메서드를 여러 개 호출해야 하고, 객체가 완전히 생성되기 전까지는 일관성이 무너진 상태에 놓이게 된다.
- 불변클래스로 만들 수 없고 스레드 안전성을 얻으려면 추가 작업이 필요하다.
3) 빌더 패턴
public class NutritionFacts3 {
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;
}
public NutritionFacts3 build() {
return new NutritionFacts3(this);
}
}
private NutritionFacts3(Builder builder) {
servingSize = builder.servingSize;
servings = builder.servings;
calories = builder.calories;
fat = builder.fat;
sodium = builder.sodium;
carbohydrate = builder.carbohydrate;
}
}
4) 빌더 패턴은 계층적으로 설계된 클래스와 함께 사용하기 좋다.
package effective.study.chapter01.item02.hierarchicalbuilder;
import java.util.EnumSet;
import java.util.Objects;
import java.util.Set;
public abstract class Pizza {
public enum Topping {HAM, MUSHROOM, ONION, PEPPER, SAUSAGE}
final Set<Topping> toppings;
//재귀적 타입 한정 : Pizza 빌더의 하위 클래스만 타입 한정
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));
//그냥 this를 반환하면 Pizza 추상클래스가 반환되므로
//self를 추상 메서드로 놓고 하위클래스를 리턴받는다.
//후에 타입캐스팅을 하지 않기 하기 위함이다
return self();
}
abstract Pizza build();
// 하위 클래스는 이 메서드를 재정의하여 "this" 반환하도록 해야 한다.
protected abstract T self();
}
Pizza(Builder<?> builder) {
toppings = builder.toppings.clone();
}
}
Pizza.Builder 클래스는 재귀적 타입 한정을 이용하는 제네릭 타입이다. 여기에 추상 메서드인 self를 더해 하위클래스에서는 형변환하지 않고도 메서드 체이닝을 지원할 수 있다.
public class NyPizza extends Pizza{
public enum Size {SMALL, MEDIUM, LARGE}
private final Size size;
public static class Builder extends Pizza.Builder<NyPizza.Builder> {
private final Size size;
public Builder(Size size) {
this.size = Objects.requireNonNull(size);
}
@Override
NyPizza build() {
return new NyPizza(this);
}
@Override
protected Builder self() {
return this;
}
}
private NyPizza(Builder builder) {
super(builder);
size = builder.size;
}
@Override
public String toString() {
return String.format("뉴욕 피자 (사이즈 : $s)", size);
}
}
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
public class Calzone extends Pizza{
private final boolean sauceInside;
public static class Builder extends Pizza.Builder<Calzone.Builder> {
private boolean sauceInside = false;
public Builder sauceInside() {
sauceInside = true;
return this;
}
@Override Calzone build() { return new Calzone(this); }
@Override protected Builder self() { return this; }
}
private Calzone(Builder builder) {
super(builder);
sauceInside = builder.sauceInside;
}
@Override
public String toString() {
return String.format("%s로 토핑한 칼초네 피자 (소스는 $s에)",
toppings, sauceInside ? "안" : "바깥");
}
}
각 하위클래스의 빌더가 정의한 build 메서드는 해당하는 구체 하위 클래스를 반환한다. 하위클래스의 메서드가 상위 클래스의 메서드가 정의한 반환 타입이 아닌, 그 하위 타입을 반환하는 기능을 공변 반환 타이핑이라 한다. 이 기능을 이용해 클라이언트는 타입 캐스팅을 신경쓰지 않아도 된다.
NyPizza pizza = new NyPizza.Builder(SMALL)
.addTopping(SAUSAGE)
.addTopping(ONION).build();
Calzone calzone = new Calzone.Builder()
.addTopping(HAM).sauceInside().build();
System.out.println(pizza);
System.out.println(calzone);
생성자로는 누릴 수 없는 사소한 이점으로, 빌더를 이용하면 가변인수 매개변수를 여러개 사용할 수 있다. 각각을 적절한 메서드로 나눠 선언하면 된다. 아니면 메서드를 여러 번 호출하도록 하고 호출 때 넘겨진 매개변수들을 하나의 필드로 모을 수도 있다. Pizza 추상클래스의 addTopping 메서드가 그렇다.
빌더 패턴에 장점만 있는 것은 아니다. 객체를 만드려면 빌더부터 만들어야 한다. 생성 비용이 크지 않지만 성능에 민감한 상황에서는 문제가 될 수 있다. 또한 코드가 장황하다. 하지만 API는 시간이 지날수록 매개변수가 늘어나는 경향이 있다. 나중에 바꾸느니 애초에 빌더로 시작하는 편이 나을 때가 많다.
'Book > 이펙티브 자바' 카테고리의 다른 글
Item6 - 불필요한 객체 생성을 피하라 (0) | 2023.11.17 |
---|---|
Item5 - 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라 (0) | 2023.11.16 |
Item4 - 인스턴스화를 막으려거든 private 생성자를 사용하라 (0) | 2023.11.15 |
Item3 - 생성자나 열거 타입으로 싱글톤임을 보증하라. (1) | 2023.11.14 |
Item1 - 생성자 대신 정적 팩터리 메서드를 고려하라 (0) | 2023.11.08 |