생성자에 매개변수가 많다면 빌더를 고려하라
생성자에 매개변수가 많다면?
영양 정보를 제공해야 하는데, 클래스 내부에 멤버 필드가 매우 많다고 가정하자.
static 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;
...
}
- 위 상황에서 경우의 수 별로 생성자로 만드는 것은 무리가 있다.
- 총 6개의 필드가 있고 이 중에 3개를 뽑는 것만 해도 경우의 수가
6*5*4/3*2
가 나온다. - 혹여나 만든다해도 실제 객체를 생성할 때, 실수하기도 쉽고 어떤 생성자가 있는지 찾아보기도 매우 귀찮다.
- 총 6개의 필드가 있고 이 중에 3개를 뽑는 것만 해도 경우의 수가
자바빈즈 패턴으로 해결해보기
자바빈즈 패턴
이란 일단 빈 생성자로 객체를 만든 뒤에setter
를 통해 값을 설정하는 것이다.
NutritionFacts cocaCola = new NutritionFacts();
cocaCola.setServingSize(240);
cocaCola.setServings(8);
...
자바빈즈 패턴
은 커다란 단점이 몇가지 있다.- 객체 하나를 만들려면 메서드 여러개를 호출해야 한다.
- 객체가 완전히 생성되기 전까지는 일관성(consistency)이 무너진 상태에 놓이게 된다.
- 생성자에서 필드 값을 받을 때처럼 클래스 혹은 필드를 불변으로 만들 수 없다.
- 불변 클래스가 아니기에
Threadsafe
하기 위해 프로그래머의 추가 작업이 필요하다. - 불변 클래스를 설명하는 좋은 글
- 불변 클래스가 아니기에
빌더 패턴 적용해보기
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; }
public NutritionFacts build() {
return new NutritionFacts(this);
}
}
private NutritionFacts(Builder builder) {
servingSize = builder.servingSize;
servings = builder.servings;
calories = builder.calories;
fat = builder.fat;
sodium = builder.sodium;
carbohydrate = builder.carbohydrate;
}
}
일반 클래스 내부에 정적 클래스인
Builder
클래스를 만들어 구현한다.Builder
의 생성자는 멤버 중 필수인 값을 받아둔다. 필수가 아닌 나머지 값은 빌더의 메서드 체인으로 받는다.
NutritionFacts
클래스 내부에Builder
라는 정적 클래스를 둔다.Builder
클래스를 초기화할 때 필요한 값은 필수 값이며, 불변 값이다.build()
를 통한 객체 생성 시에불변식(invariant)
을 통해 매개변수를 검사할 수 있다.- 잘못된 점을 발견하면
IllegalArgumentException
으로 이유를 알려주면 된다.
- 잘못된 점을 발견하면
build()
에서는this
즉, 현재의Builder
객체를 넘겨서 최종 객체를 만든다.
계층적으로 설계된 Pizza 예제로 빌더 패턴 체험하기
계층적으로 설계된 클래스와 빌더 패턴은 함께 쓰기 매우 좋다.
Pizza 추상 클래스
public abstract class Pizza {
public enum Topping { HAM, MUSHROOM, ONION, PEPPER, SAUSAGE }
final Set<Topping> toppingSet;
abstract static class Builder<T extends Builder<T>> {
EnumSet<Topping> toppingSet = EnumSet.noneOf(Topping.class);
public T addTopping(Topping topping) {
toppingSet.add(Objects.requireNonNull(topping));
return self();
}
abstract Pizza build();
// 하위 클래스는 이 메서드를 재정의 해서 `this`를 반환하게 해야 한다.
protected abstract T self();
}
Pizza(Builder<?> builder) {
toppingSet = builder.toppingSet.clone();
}
}
- 추상
Pizza
클래스이다.- 토핑을 얹는 기능을 제공한다.
- 일반적인 빌더 패턴과 같이 내부에 정적 빌더 클래스를 가지고 있다.
- 다른 종류의
Pizza
구현체Builder
는 자신(Pizza.Builder
)의 하위 타입Builder
를 제너릭으로 받게 될 것이다.- 이를 재귀적 타입 바운드(Recursive Type Bound)라고 한다.
- 무언가를 비교할 때
T
라는 제너릭 타입을 받는다고 치면<T extends Comparable<T>>
와 같이 재귀적 타입 바운드를 해주면 비교할 수 있음이 확실하다.
self()
메서드는 형변환하지 않고도 해당 타입 그 자체로 메서드 연쇄를 지원할 수 있게 해준다.
NewYorkPizza 구현 클래스
public class NewYorkPizza 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 NewYorkPizza build() {
return new NewYorkPizza(this);
}
@Override
protected Builder self() { return this; }
}
public NewYorkPizza(Builder builder) {
super(builder);
this.size = builder.size;
}
}
super()
를 통해 받은 토핑을 올린다.size
를 통해 받은 사이즈를 적용한다.size
는null
이 들어오지 않도록Builder
의 생성자에서 검증한다.
CalzonePizza 구현 클래스
public class CalzonePizza 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 CalzonePizza build() {
return new CalzonePizza(this);
}
@Override
protected Builder self() {
return this;
}
}
private CalzonePizza(Builder builder) {
super(builder);
this.sauceInside = builder.sauceInside;
}
}
- 하위 클래스가 상위 클래스의 메서드가 정의한 반환 타입이 아닌 그 하위 타입을 반환하는 기능을 공변환 타이핑이라고 한다.
- 추상 클래스에서
build()
메서드는Pizza
인터페이스를 반환하게 되어 있으나, 현재 구현체에서는CalzonePizza
를 반환하고 있다.
- 추상 클래스에서
학습 테스트 코드 작성하기
NewYorkPizza newYorkPizza = new NewYorkPizza
.Builder(MEDIUM) // 필수로 사이즈를 정해야 함
.addTopping(HAM)
.addTopping(SAUSAGE)
.addTopping(MUSHROOM)
.build();
CalzonePizza calzonePizza = new CalzonePizza
.Builder()
.sauceInside()
.addTopping(ONION)
.build();
- 생성자로는 누릴 수 없는
가변인수(varargs)
라는 이점을 누릴 수 있다.
정리
- 빌더 패턴은 상당히 유연하다.
- 빌더 하나로 여러 객체를 순회하며 만들 수도 있다.
build()
메서드가 실행되는 시점에야 실제 객체가 만들어질 것이다.
- 빌더에 넘기는 매개변수에 따라 다른 객체를 만들 수도 있다.
- 특정 필드는 빌더가 알아서 채우도록 할 수도 있다.
- 빌더 하나로 여러 객체를 순회하며 만들 수도 있다.
단, 간혹 빌더라는 객체 하나를 더 생성하는 비용 자체가 민감한 경우도 있을 수 있다 이럴 땐 주의해야 한다.
생성자나 정적 팩터리가 처리해야 할 매개변수가 많다면, 빌더 패턴을 선택하는게 더 낫다.
클라이언트 코드를 읽고쓰기가 간결하고, 자바 빈즈보다 훨씬 안전하다.
반응형
'Java > 이펙티브 자바' 카테고리의 다른 글
이펙티브 자바, 쉽게 정리하기 - item5. 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라 (0) | 2021.12.25 |
---|---|
이펙티브 자바, 쉽게 정리하기 - item4. 인스턴스화를 막으려면 private 생성자를 사용하라 (0) | 2021.12.25 |
이펙티브 자바, 쉽게 정리하기 - item3. private 생성자나 열거 타입으로 싱글턴임을 보증하라 (0) | 2021.12.24 |
이펙티브 자바, 쉽게 정리하기 - item1. 생성자 대신 정적 팩터리 메서드를 고려하라 (0) | 2021.12.22 |
이펙티브 자바 - 들어가면서... (0) | 2021.12.22 |