본문 바로가기

이펙티브 자바

[이펙티브 자바] 아이템 2. 생성자에 매개변수가 많다면 빌더를 고려하라

정적 팩토리 메서드와 public 생성자 모두 제약이 있다. 바로 선택적 매개변수가 많을 때 적절히 대응하기 어렵다는 점이다.

이 때 개발자들은 점층적 생성자 패턴을 사용했었다. 하지만 이것도 단점이 존재한다.

점층적 생성자 패턴도 쓸 수는 있지만, 배개변수 개수가 많아지면 클라이언트 코드를 작성하거나 읽기 어렵다.

 

점층적 생성자 패턴의 예시 : 

public class Pizza {
    private int size; // 필수
    private boolean cheese; // 선택적
    private boolean pepperoni; // 선택적
    private boolean bacon; // 선택적

    public Pizza(int size) {
        this(size, false); // 기본값 사용
    }

    public Pizza(int size, boolean cheese) {
        this(size, cheese, false); // 기본값 사용
    }

    public Pizza(int size, boolean cheese, boolean pepperoni) {
        this(size, cheese, pepperoni, false); // 기본값 사용
    }

    public Pizza(int size, boolean cheese, boolean pepperoni, boolean bacon) {
        this.size = size;
        this.cheese = cheese;
        this.pepperoni = pepperoni;
        this.bacon = bacon;
    }

    // Pizza 객체 사용 예
    Pizza pizza = new Pizza(12, true, true); // 크기 12, 치즈와 페페로니 추가
    Pizza cheesePizza = new Pizza(12, true); // 크기 12, 치즈 추가
    Pizza everythingPizza = new Pizza(16, true, true, true); // 크기 16, 모두 추가


}

 

지금은 4개 뿐이라서 이게 뭐가 단점이야? 라고 생각할 수 있지만 지금은 '고작' 4개인것 뿐이다.

이를 보완하기 위해 빌더 패턴을 사용한다.

빌더 패턴(Builder Pattern)은 복잡한 객체의 생성 과정을 단순화하는 디자인 패턴이다. 특히, 생성자나 정적 팩토리가 처리해야 할 매개변수가 많을 때 유용하다. 빌더 패턴은 필수 매개변수만으로 생성자를 호출하여 빌더 객체를 얻고, 빌더 객체가 제공하는 설정 메서드를 호출하여 선택적 매개변수를 설정한 다음, 마지막에는 build() 메서드를 호출하여 필요한 객체를 얻는다.

빌더패턴의 장점이 여러가지 있다.

1. 가독성 향상

2. 사용의 유연성

3. 불변성 유지

4. 객채 생성 코드의 재사용성

 

빌더 패턴의 예시 : 

public class Pizza {
    private final int size; // 불변 필드 선언으로 객체의 불변성을 유지
    private final boolean cheese;
    private final boolean pepperoni;
    private final boolean bacon;

    private Pizza(Builder builder) {
        this.size = builder.size; // Builder에서 설정된 값으로 초기화
        this.cheese = builder.cheese;
        this.pepperoni = builder.pepperoni;
        this.bacon = builder.bacon;
    }

    public static class Builder {
        private final int size; // 필수 매개변수
        private boolean cheese = false; // 선택적 매개변수는 기본값으로 초기화
        private boolean pepperoni = false;
        private boolean bacon = false;

        public Builder(int size) {
            this.size = size; // 필수 매개변수 설정
        }

        // 선택적 매개변수 설정 메서드는 체이닝을 통해 가독성과 사용의 유연성을 향상
        public Builder cheese(boolean value) {
            this.cheese = value;
            return this; // 메서드 체이닝을 가능하게 함
        }

        public Builder pepperoni(boolean value) {
            this.pepperoni = value;
            return this;
        }

        public Builder bacon(boolean value) {
            this.bacon = value;
            return this;
        }

        public Pizza build() {
            return new Pizza(this); // 최종 객체 생성 후 반환
        }
    }

    // Pizza 객체 사용 예
    public static void main(String[] args) {
        // 첫 번째 예시: 크기가 12인 치즈 피자
        // 가독성 향상: 각 설정 메서드의 이름을 통해 어떤 매개변수를 설정하는지 명확히 알 수 있음
        Pizza cheesePizza = new Pizza.Builder(12).cheese(true).build();

        // 두 번째 예시: 크기가 16인, 치즈, 페페로니, 베이컨이 모두 추가된 피자
        // 객체 생성 코드의 재사용성: Builder 객체를 통해 비슷한 객체를 생성할 때 설정 코드를 재사용할 수 있음
        Pizza everythingPizza = new Pizza.Builder(16)
                                .cheese(true)
                                .pepperoni(true)
                                .bacon(true)
                                .build();

        // 여기에서 cheesePizza와 everythingPizza 객체를 사용할 수 있습니다.
    }
}

 

 

빌더패턴은 계층적으로 설계된 클래스 구조(상속 구조) 에서 특히 유용하다.

상속 주고에서 사용되는 빌더 패턴의 예 : 

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();
        }

        // 최종적으로 Pizza 객체를 생성하는 메서드, 하위 클래스에서 구현해야 함
        abstract Pizza build();

        // 하위 클래스에서 이 메서드를 오버라이드하여 "this"를 반환하도록 함. 체이닝을 위해 사용됨.
        protected abstract T self();
    }

    // Builder를 사용하여 Pizza 객체를 생성하는 생성자
    Pizza(Builder<?> builder) {
        toppings = builder.toppings.clone(); // Item 50 참고, toppings 복제
    }
}

// 마르게리타 피자 클래스, Pizza 클래스를 상속받음
class MargheritaPizza extends Pizza {
    MargheritaPizza(Builder builder) {
        super(builder); // 상위 클래스의 생성자 호출
    }

    // 마르게리타 피자를 위한 Builder 정적 내부 클래스, Pizza.Builder를 상속받음
    static class Builder extends Pizza.Builder<Builder> {
        @Override
        MargheritaPizza build() {
            return new MargheritaPizza(this); // MargheritaPizza 객체 생성
        }

        @Override
        protected Builder self() {
            return this; // 체이닝을 위해 Builder 인스턴스 자신을 반환
        }
    }
}

 

결론 : 생성자나 정적 팩토리 메서드에서 처리해야 할 매개변수가 많다면 빌더 패턴을 선택하는게 낫다. API는 시간이 지날수록 매개변수가 많아지는 경향이 있기 때문에 매개변수가 적어도 처음부터 빌더 패턴으로 시작하자.