자바에서 생성자를 구현하는 방법은 여러가지가 있지만, 점층적 생성자 패턴과 자바빈즈 패턴, 그리고 빌드패턴에 대해 알아보자. 결론은 물론 정해져있다. 생성자에 매개변수가 많다면 빌더 패턴을 고려해야 한다.
점층적 생성자 패턴(telescoping constructor pattern)
아래와 같이 자바의 오버로딩 성격을 활용해서 클래스를 생성하는 점층적 생성자 패턴은 매개변수 개수가 많아지면 클라이언트 코드를 작성하거나 읽기가 어렵다.
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;
}
자바빈즈 패턴(Javabeans pattern)
자바빈즈는 객체를 생성한 뒤 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() { }
// Setters
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; }
public static void main(String[] args) {
NutritionFacts cocaCola = new NutritionFacts();
cocaCola.setServingSize(240);
cocaCola.setServings(8);
cocaCola.setCalories(100);
cocaCola.setSodium(35);
cocaCola.setCarbohydrate(27);
}
}
이러한 자바빈즈 패턴은 객체 하나를 생성하기 위해서 setter 메소드를 많이 호출해야 하고, 객체가 생성되기 전까지는 일관성이 무너진 상태에 놓이게 된다는 단점이 있다. 또한 클래스를 불변으로 만들 수 없으며 스레드 safe하지 않다.
빌더 패턴(builder pattern)
빌터 패턴이란, 필수 매개변수만 생성자를 통해 설정하고, 선택 매개변수들은 빌더 객체가 제공하는 setter메소드를 통해서 셋팅한다.
public class NutritionFacts {
private final int servingSize;
private final int servings;
private final int calories;
private final int fat;
public static class Builder{
// 필수 매개변수
private final int servingSize;
private final int servings;
// 선택 매개변수
private int calories =0;
private int fat =0;
public Builder(int servingSize , int servings){
this.servingSize = servingSize;
this.servings = servings;
}
public Builder calories(int val){ // 자기 자신(Builder)를 반환하기 때문에 연쇄적으로 호출할 수 있다.
calories = val;
return this;
}
public Builder fat(int val){
fat = val;
return this;
}
public NutritionFacts build(){
return new NutritionFacts(this);
}
}
private NutritionFacts(Builder builder){
servingSize = builder.servingSize;
servings = builder.servings;
calories = builder.calories;
fat = builder.fat;
}
public static void main(String[] args){
NutritionFacts cocaCola = new Builder(240,8)
.calories(100).fat(10).build();
}
}
이렇듯 빌더의 세터 메서드들은 자기 자신을 반환하기 때문에 연쇄적으로 호출할 수 있다. 이런 방식을 fluent API, method chaining이라고 한다.
빌더 패턴은 또한 계층적으로 설계된 클래스와 함께 쓰기 좋다.
Pizza의 Builder 클래스는 재귀적 타입 한정을 이용하는 제네릭 타입이다. 여기에 추상 메서드인 self를 더해 하위 클래스에서는 형변환하지 않고도 메소드 연쇄를 지원할 수 있다. self 타입이 없는 자바를 위한 이 우회 방법을 시뮬레이트한 셀프타입(self-type) 관용구라고 한다.
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(); // 아이템 50 참조
}
}
Pizza 클래스를 상속받은 NyPizza와 Calzone 클래스를 살펴보자. 각 하위 클래스의 빌더는 Pizza.Builder<Builder>를 상속받고있지만 리턴 타입은 각각 자기 자신인 NyPizza와 Calzone형이다. 이렇게 하위 클래스의 메소드가 상위 클래스의 메서드가 정의한 반환 타입이 아닌, 그 하위 타입을 반환하는 기능을 공변반환 타이핑(Covariant return typing)이라고 한다. 이 기능을 이용하면 클라이언트가 형변환에 신경쓰지 않고도 빌더를 사용할 수 있다.
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;
}
@Override public String toString() {
return toppings + "로 토핑한 뉴욕 피자";
}
}
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;
}
@Override public String toString() {
return String.format("%s로 토핑한 칼초네 피자 (소스는 %s에)",
toppings, sauceInside ? "안" : "바깥");
}
}
'Reading > 이펙티브 자바' 카테고리의 다른 글
아이템6. 불필요한 객체 생성을 피하라. (0) | 2022.04.24 |
---|---|
아이템9 . try-finally 보다는 try-with-resource를 사용하라 (0) | 2022.04.24 |
아이템 7. 다 쓴 객체 참조를 해제하라 (0) | 2022.04.24 |
아이템3. private 생성자나 열거 타입으로 싱글턴임을 보증하라 (0) | 2022.04.24 |
아이템1. 생성자 대신 정적 팩터리 메서드를 고려하라 (0) | 2022.04.24 |