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

노을·2023년 1월 11일
0

이펙티브 자바

목록 보기
2/14
post-thumbnail

⭐ 빌더를 사용해야 하는 이유


빌더는 선택적 매개변수가 만을 때의 생성자와 자바빈즈 패턴의 단점을 보완한다.

생성자의 경우 점층적 생성자 패턴을 사용할 수 있지만 그러면 클라이언트 코드가 지저분해진다는 단점이 있다.
자바 빈즈 패턴의 경우 객체 하나를 만들려면 메서드(setter)를 여러 개 호출해야 하고, 객체가 완전히 생성되기 전까지는 일관성이 무너진 상태에 놓이게 된다. 그리고 클래스를 불변으로 만들 수 없다. (setter로 객체 값 변경 가능하기 때문에)

이러한 단점을 보완하고자 빌더를 사용해야 한다고 책에서 말하고 있다.

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 void main(String[] args) {
        NutritionFacts cocaCola = new Builder(240, 8)
                .calories(100)
                .sodium(35)
                .carbohydrate(27).build();
    }

    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 nutritionFacts = new NutritionFacts.Builder(10, 10)
        .carbohydrate(1)
        .calories(10)
        .build();

이렇게 메서드 체이닝으로 객체를 만들 수 있다.


빌더의 장점
1. 불필요한 생성자를 만들지 않고 객체를 만들 수 있다.
2. 데이터 순서에 상관 없이 객체를 만들 수 있다.
3. 사용자가 봤을 때 명시적이고 이해할 수 있다.




⭐ 계층적으로 설계된 클래스에서의 빌더


19p.빌더 패턴은 계층적으로 설계된 클래스와 함께 쓰기가 좋다.

처음에 이 말이 이해가 잘 안갔는데
Pizza 라는 추상 클래스에 추상 빌더가 있고, NyPizza 라는 구현클래스에 구체 빌더가 있다고 하자.

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 참조
    }
}

여기서 중요한 건, addTopping 메서드는 Pizza가 아니라 Pizza를 상속받는 하위 타입 객체를 반환한다는 것이다.
그렇게 되면 addTopping이라는 메서드를 상속받아 사용하면서, NyPizza 만 가지고 있는 메서드들도 사용할 수 있게 된다.

NyPizza pizza = new NyPizza.Builder(SMALL)
        .addTopping(SAUSAGE)
        .addTopping(ONION).build();

NyPizza의 오버라이드 된 메서드를 타입변환 없이 그냥 사용하면 된다.
이렇게 하면 Pizza의 Builder를 상속 받아 쓰면서 NyPizza 객체를 쓸 수 있는 것이다.




⭐ 빌더의 단점, @Builder


이런 글을 읽다보면,,, 아 생성자랑 자바빈즈는 쓰지 말고 무조건 빌더를 사용해야겠다~! 라는 생각이 들기 마련이다.
하지만 빌더 패턴에도 단점이 있다.

빌더패턴으로 바꾸면 코드 양이 증가하고 난이도가 상승한다.
그래서 선택적 인자, 필수적 인자가 공존하는 경우에, 생성자가 너무 많고 복잡하며 불변객체로 만들고 싶을 때 사용해야 한다.

☑️ lombok의 @Builder

위 단점들을 보완하기 위해 요즘 롬복에서 제공하는 @Builder 어노테이션을 많이 쓴다.

@Builder
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 void main(String[] args) {
        NutritionFacts build = new NutritionFactsBuilder()
                .carbohydrate(1)
                .build();
    }

}

@Builder 를 사용하면 코드가 정말 깔끔해진다.



근데 여기에도 단점이 있다 !

단점1

@Builder 어노테이션을 사용하면 (package 레벨) 모든 필드를 파라미터로 받는 생성자가 만들어진다.
참고로 @NoArgsConstructor@AllArgsConstructor 등으로 내가 직접 생성자를 만들어주었다면 모든 필드를 파라미터로 받는 생성자가 생기지 않는다.

그렇다면 객체를 new로 생성할 수 있게 된다.

NutritionFacts2 nutritionFacts2 = new NutritionFacts2(1, 1, 1, 1, 1, 1);

생성자로 객체를 만들지 않으려고 빌더 패턴을 도입해놓았지만
클라이언트 입장에서는 이를 알지 못하고 new로 객체를 생성할 가능성이 생긴다.

해결방법
: @AllArgsConstructor(access = AccessLevel.PRIVATE)로 외부에서 접근 못하게 막으면 된다.



단점2

책에 나오는 NutritionFacts 예제에서는 필수 값을 지정할 수 있었다.

NutritionFacts nutritionFacts = new NutritionFacts.Builder(10, 10)
        .carbohydrate(1)
        .calories(10)
        .build();

근데 @Builder 를 사용하면 모든 필드를 선택적으로 주입받게 된다.
이를 원하지 않는다면 @Builder를 쓰지말고 빌더 패턴을 직접 구현해서 써야 한다.



@Builder 쓸 때 참고

사실 @Builder 애노테이션을 class 레벨에 사용하는 것은 추천하지 않는다.

  • @Builder를 Class에 적용시키면 생성자의 접근 레벨이 default이기 때문에, 동일 패키지 내에서 해당 생성자를 호출 할 수 있는 문제 발생
  • 모든 멤버 필드에 대해서 매개변수를 받는 기본 생성자를 만든다. -> @AllArgsConstructor 와 같은 효과
  • 객체에서 id 값이 데이터베이스 PK 생성전략에 의존하고 있다고 가정한다면 객체를 생성할 때 id값을 넘겨받지 않아야 한다.
  • class 레벨에 적용하면 객체생성에 제한을 두기가 어렵습니다.

따라서 private 생성자를 구현해서 @Builder를 지정하자.

참고 : [JAVA] 빌더 패턴(Builder pattern)

0개의 댓글