럿고의 개발 노트
아이템 2. 생성자에 매개변수가 많다면 빌더를 고려하라. 본문
이팩티브 자바 3판 - 2장. 객체 생성과 파괴
아이템 2. 생성자에 매개변수가 많다면 빌더를 고려하라.
- 매개 변수가 많은 코드들을 어떻게 관리해야 할까?
첫번째 대안 - 점층적 생성자 패턴(Telescoping Constructor Pattern)
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, 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 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);
}
}
- 단점은 클라이언트 코드를 작성하거나 읽기가 어렵고 확장성도 떨어진다.
두번째 대안 - 자바빈즈 패턴(JavaBeans Pattern)
- 매개변수가 없는 생성자로 객체를 만든 후,
setter
를 이용해 매개 변수의 값을 설정하는 방식이다.
public class NutritionFacts {
private int servingSize;
private int servings;
private int calories;
private int fat;
private int sodium;
private int carbohydrate;
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;
}
}
- 점층적 생성자 패턴의 단점들을 보안했지만, 심각한 단점이 있다. 객체 하나를 만들려면 메서드를 여러 개 호출해야 하고, 객체가 완전히 생성되기 전까지는 일관성이 무너진 상태에 놓이게 된다.
- 또한, 점층적 생성자 패턴에서는 매개변수들이 유효한지를 생성자에서만 확인하면 일관성을 유지할 수 있는데, 자바빈즈 패턴에서 그 장치가 사라졌다.
- 일관성이 깨진 객체가 만들어지면, 버그를 심은 코드와 그 버그 때문에 런타임에 문제를 겪는 코드가 물리적으로 멀리 떨어져 있을 것이므로 디버깅도 만만치 않다.
- 아울러 이 일관성이 무너지면
immutable class
를 만들수 없고 스레드의 안정성을 얻으려면 추가적인 작업이 필요하다.
세 번째 대안 - 빌더 패턴(Builder Pattern)
- 클라이언트는 필요한 객체를 직접 만드는 대신, 필수 매개변수만으로 생성자를 호출해 빌더 객체를 얻고 빌더 객체가 제공하는 일종의 세터 메서드들로 원하는 선택 매개변수들을 설정한다.
마지막으로 매개변수가 없는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;
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 calories){
this.calories = calories;
return this;
}
public Builder fat(int fat){
this.fat = fat;
return this;
}
public Builder sodium(int sodium){
this.sodium = sodium;
return this;
}
public Builder carbohydrate(int carbohydrate){
this.carbohydrate = carbohydrate;
return this;
}
public NutritionFacts build() {
return new NutritionFacts(this);
}
}
public NutritionFacts(Builder builder) {
this.servingSize = builder.servingSize;
this.servings = builder.servings;
this.calories = builder.calories;
this.fat = builder.fat;
this.sodium = builder.sodium;
this.carbohydrate = builder.carbohydrate;
}
}
NutritionFacts
는 불변이며, 모든 매개변수의 기본값들을 한곳에 모아뒀다.Builder
의setter
는 자기 자신을 반환하기 때문에 연쇄적으로 호출 할 수 있다. 이런 방식을 메서드 호출이 흐르듯 연결된다는 뜻으로 플루언트 API(Fluent API) 또는 메서드 연쇄(Method Chaining)라 한다.- 3개의 패턴들이 어떻게 생성되는지에 대해서 살펴보자.
public class Application {
public static void main(String[] args) {
// Telescoping Constructor Pattern
Item02.telescopingConstructorPattern.NutritionFacts cocaCola = new Item02.telescopingConstructorPattern.NutritionFacts(240, 8, 100, 0, 35, 27);
// JavaBean Pattern
Item02.javaBeanPattern.NutritionFacts cocaCola2 = new Item02.javaBeanPattern.NutritionFacts();
cocaCola2.setServings(240);
cocaCola2.setServings(8);
cocaCola2.setCalories(100);
cocaCola2.setSodium(35);
cocaCola2.setCarbohydrate(27);
// Builder Pattern
Item02.builderPattern.NutritionFacts cocaCola3 = new Item02.builderPattern.NutritionFacts.Builder(240, 8)
.calories(100)
.sodium(35)
.carbohydrate(27).build();
}
}
빌더 패턴의 자세한 설명
- 빌더 패턴은 (파이썬, 스칼라에 있는) 명명된 선택적 매개변수(named optional parameters)를 흉내 낸 것이다.
cf) 명명된 선택적 매개변수
def product(a, optional = 1):
print(a * optional)
product(3, 5);
product(3);
cf) 빌더 패턴 유효성 검사
- 잘못된 매개변수를 최대한 일찍 발견하려면 빌더의 생성자와 메서드에서 입력 매개변수를 검사하고,
build
가 호출하는 생성자에서 여러 매개변수를 걸친 불변식을 검사하자. 공격에 대비해 이런 불변식을 보장하려면 빌더로부터 매개변수를 복사한 후 해당 객체 필드들도 검사해야 한다. - 검사해서 잘못된 점을 발견하면 어떤 매개변수가 잘못되어있는지를 자세히 알려주는 메시지를 담아
IllegalArgumentException
을 던지면 된다.
cf) 불변식(invariant)
- 프로그램이 실행되는 동안, 혹은 정해진 기간 동안 반드시 만족해야 하는 조건이며 변경을 허용할 수 있으나 주어진 조건 내에서만 허용한다는 의미이다.
- 예를 들어 리스트의 인덱스가 음수라면 불변식이 깨졌다고 볼 수 있다.
- 따라서 가변 객체에도 불변식은 존재할 수 있으며, 넓게 보면 불변은 불변식의 극단적인 예로 볼 수 있다.
계층적으로 설계된 클래스와 빌더 패턴
- 각 계층의 클래스에 관련 빌더를 멤버로 정의하고 추상 클래스는 추상 빌더를, 구체 클래스는 구체 빌더를 갖게 한다.
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;
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();
}
}
import java.util.Objects;
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 Pizza build() {
return new Calzone(this);
}
@Override
protected Builder self() {
return this;
}
}
private Calzone(Builder builder) {
super(builder);
sauceInside = builder.sauceInside;
}
}
import static Item02.builderPattern.NyPizza.Size.*;
import static Item02.builderPattern.Pizza.Topping.*;
public class Test {
public static void main(String[] args) {
NyPizza nyPizza = new NyPizza.Builder(SMALL)
.addTopping(SAUSAGE)
.addTopping(ONION)
.build();
Calzone calzone = new Calzone.Builder()
.addTopping(HAM)
.sauceInside()
.build();
}
}
- 각 하위 클래스의 빌더가 정의한
build
는 해당 구체 하위 클래스를 반환하도록 선언한다. - 하위 클래스의 메서드가 상위 클래스의 메서드가 정의한 반환 타입이 아닌, 그 하위 타입을 반환하는 기능을 공변 반환 타이핑이라고 한다.
cf) 공변 반환 타이핑
builder
메서드는Pizza
가 아닌Calzone
또는NyPizza
를 반환한다. 재정의한 메서드의 반환 타입을 상위 클래스의 메서드가 반환하는 타입의 하위 타입일 수도 있다는 이야기이다.- 이 기능을 이용하면 클라이언트가 형변환에 신경 쓰지 않고도 빌더를 사용할 수 있다.
빌더 패턴의 장점
- 빌더를 이용하면 가변인수 매개변수를 여러 개 사용할 수 있다. 각각의 적절한 메서드로 나눠 선언하면 된다.
- 아니면 메서드를 여러 번 호출하도록 하고 각 호출 때 넘겨진 매개변수들을 하나의 필드로 모을 수 있다. 예로
addTopping
이다. - 빌더 패턴은 상당히 유연하다. 빌더 하나로 여러 객체를 순회하면서 만들 수 있고, 빌더에 넘기는 매개변수에 따라 다른 객체를 만들 수도 있다. 객체마다 부여되는 일련번호와 같은 특정 필드는 빌더가 알아서 채우도록 할 수 있다.
빌더 패턴 단점
- 객체를 만들려면, 그에 앞서 빌더부터 만들어야 한다. 빌더 생성 비용이 크지는 않지만 성능에 민감한 상황에서는 문제가 될 수 있다.
- 또한 코드가 장황해서 매개변수가 4개 이상은 되어야 값어치를 한다.
- 그러나 API를 만들다 보면 매개변수가 많아지는 경향이 있음을 알아두자.
정리
- 생성자나 정적 팩터리가 처리해야 할 매개변수가 많다면 빌더 패턴을 선택하는 게 더 낫다.
- 매개 변수 중 다수가 필수가 아니거나 같은 타입이면 특히 더 그렇다.
- 빌더는 점층적 생성자보다 클라이언트 코드를 읽고 쓰기가 훨씬 간결하고, 자바빈즈보다 훨씬 안전하다.
'Java Note > 이펙티브 자바 3판(EFFECTIVE JAVA 3E)' 카테고리의 다른 글
아이템 6. 불필요한 객체 생성을 피하라. (0) | 2020.03.23 |
---|---|
아이템 5. 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라. (0) | 2020.03.23 |
아이템 4. 인스턴스화를 막으려거든 private 생성자를 사용하라. (0) | 2020.03.17 |
아이템 3. private 생성자나 열거 타입으로 싱글턴임을 보증하라. (0) | 2020.03.16 |
아이템 1. 생성자 대신 정적 팩터리 메서드를 고려하라. (0) | 2020.03.14 |
Comments