본문 바로가기
Dev Books/Effective Java

[item 2] 생성자에 매개변수가 많다면 빌더를 고려하라.

by Thumper 2024. 1. 14.

이번 시간에는 빌더패턴에 대해 정리해보자.

생성자나 정적 팩터리가 처리해야 할 매개변수가 많다면 빌더 패턴을 선택하는게 더 낫다.

매개변수 중 다수가 필수가 아니거나 같은 타입이면 특히 더 그렇다.

빌더는 점층적 생성자보다 클라이언트 코드를 읽고 쓰기가 훨씬 간결하고, 자바빈즈보다 훨씬 안전하다.

(이번 내용도 github에 정리되어 있으니 참고해주세요.)


생성자에 매개변수가 많다면 빌더를 고려하라

생성자 경우

이런 코드는 작성하기도 어렵고 읽기도 어렵다.

NutritionFacts cocaCola = new NutritionFacts(240, 8, 100, 0, 35, 27);

 

자바빈 경우

(우선) 생성자 경우보다 읽기 쉬운 코드가 되었다.

NutritionFacts cocaCola = new NutritionFacts();
cocaCola.setServingSize(240);
cocaCola.setServings(8);
cocaCola.setCalories(100);
cocaCola.setSodium(35);
cocaCola.setCarbohydrate(27);

하지만 심각한 단점을 가지고 있다.

    • 객체 하나를 만들려면 메서드를 여러개 호출해야 한다.
    • 객체가 완전히 생성되기 전까지는 일관성이 무너진 상태가 된다.
    • 일관성이 깨진 객체를 만들면 버그를 심은 코드와 버그 때문에 디버깅이 쉽지 않다.

자바빈즈 패턴에서는 클래스를 불변으로 만들 수 없으며 스레드 안전성을 얻으려면 추가적인 작업이 필요하다.

단점을 보완하고자 객체를 수동으로 얼리고(freezing) 얼리기 전에는 사용할 수 없도록 만든다.

JS진영에는 freeze라는 메서드가 있지만 자바에서는 어떻게 만드는지 소개되지 않고 만들기 어려워 잘 사용하지 않는다고 한다.

 

 

빌더패턴 경우

만들려는 객체를 바로 만들지 않고 클라이언트는 빌더(생성자 또는 static 팩토리)에 필수적인 매개변수를 주면서 호출해

Builder 객체를 얻은 다음 빌더 객체가 제공하는 세터와 비슷한 메소드를 사용해서 부가적인 필드를 채워넣고
최종적으로 build라는 메소드를 호출해서 만들려는 객체를 생성한다.

 

다음 예제 코드를 보자.

다음과 같이 Builder 메서드를 작성하여, user.builder()를 호출하여 객체를 생성하면 된다.

public class User {
    private final int age;
    private final int phoneNumber;
    private int weight;
    private int tall;
    private int birthday;

  // Builder를 선언해야 한다.
    public User(Builder builder) {
        this.age = builder.age;
        this.phoneNumber = builder.phoneNumber;
        this.weight = builder.weight;
        this.tall = builder.tall;
        this.birthday = builder.birthday;
    }

    public static class Builder {
        private final int age;
        private final int phoneNumber;
        private int weight;
        private int tall;
        private int birthday;

        public Builder(int age, int phoneNumber) {
            this.age = age;
            this.phoneNumber = phoneNumber;
        }

        public Builder weight(int weight) {
                        // validation 가능
            this.weight = weight;
            return this;
        }

        public Builder tall(int tall) {
            this.tall = tall;
            return this;
        }

        public Builder birthday(int birthday) {
            this.birthday = birthday;
            return this;
        }

        public User build() {
            return new User(this);
        }
    }
}

// 빌더패턴으로 객체 생성
User user = new User.Builder(20, 99998888)
                .weight(70)
                .tall(180)
                .birthday(1225)
                .build();



 

빌더 패턴은 계층적으로 설계된 클래스와 함께 쓰기에 좋다. 각 계층의 클래스에 관련 빌더를 멤버로 정의하고 추상 클래스는 추상 빌더를 구체 클래스는 구체 빌더를 갖게 한다.

다음 예제 코드를 보자.

추상 클래스 Pizza, 추상 클래스를 상속받아 구현한 Calzone 클래스가 있다.

Pizza 안에 추상 클래스인 Builder가 있는데 자신의 하위 클래스의 타입을 받도록 되어있다. 

그리고 self() 메서드에서 상속받은 클래스의 빌더를 리턴하도록 했다.

 

self()는 pizza의 하위클래스를 리턴하기 때문에 CalzonePizza 클래스는 자기 자신을 반환한다(return this).
이를 통해 형변환과 같은 별도의 캐스팅없이 builder 사용이 가능하다.

// Pizza라는 추상 클래스가 있다.
public abstract class Pizza {

    public enum Topping {
        HAM, MUSHROOM, ONION, PEEPER, SAUSAGE
    }

    final Set<Topping> toppings;

   // pizza안에 추상 클래스인 Builder는 자신의 하위 클래스의 타입을 받도록 했다.
   // (재귀적인 타입 제한) self() 메서드에서 상속받은 클래스의 빌더를 리턴하도록 했다.
    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(); // `Convariant 리턴 타입`을 위한 준비작업

      // 하위 클래스는 이 메서드를 재정의(overriding)하여
      // "this"를 반환하도록 해야 한다.
        protected abstract T self(); 
    }

    Pizza(Builder<?> builder) {
        toppings = builder.toppings.clone();
    }

}
// pizza 추상 클래스를 상속받아 구현한 CalzonePizza 클래스이다.
// self()는 pizza의 하위클래스를 리턴하기 때문에 CalzonePizza 클래스는 자기 자신을 반환한다. (return this)
// 이를 통해 형변환과 같은 별도의 캐스팅없이 builder 사용이 가능하다.
public class Calzone extends Pizza {

    private final boolean sauceInside;

    public static class Builder extends Pizza.Builder<Builder> {
        private boolean sauseInside = false;

        public Builder sauceInde() {
            sauseInside = 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.sauseInside;
    }

}
// 추상 빌더를 사용할 때
Calzone calzone = new Calzone.Builder()
    .addTopping(Pizza.Topping.HAM)
    .sauceInde()
    .build();

댓글