Item 2. Builder

μƒμ„±μžμ— λ§€κ°œλ³€μˆ˜κ°€ λ§Žλ‹€λ©΄ λΉŒλ”λ₯Ό κ³ λ €ν•˜λΌ

Item 1μ—μ„œ μ†Œκ°œ 된 정적 νŒ©ν„°λ¦¬ λ©”μ„œλ“œμ™€ public μƒμ„±μž λ‘˜ λ‹€ κ°–κ³ μžˆλŠ” 단점은 선택적 λ§€κ°œλ³€μˆ˜κ°€ λ§Žμ„ λ•Œ 적절히 λŒ€μ‘ν•˜κΈ° μ–΄λ ΅λ‹€λŠ” 것이닀. 선택적 λ§€κ°œλ³€μˆ˜κ°€ λ§Žμ•„μ§€λ©΄ 그만큼 λ„˜κ²¨μ•Ό ν•  λ§€κ°œλ³€μˆ˜μ˜ μˆ˜λ„ λŠ˜μ–΄λ‚˜κ²Œ λ˜μ–΄ μ‚¬μš©ν•˜κΈ° λΆˆνŽΈν•΄μ§„λ‹€. λ¨Όμ € λΉŒλ” νŒ¨ν„΄μ„ μ‚¬μš©ν•˜μ§€ μ•Šμ€ μ˜ˆμ‹œλ“€μ„ μ‚΄νŽ΄λ³΄λ©΄ λ‹€μŒκ³Ό κ°™λ‹€.

λΉŒλ”λ₯Ό μ‚¬μš©ν•˜μ§€ μ•Šμ€ μ˜ˆμ‹œ

전톡적인 점측적 μƒμ„±μž νŒ¨ν„΄(telescoping constructor pattern)

μ—¬λŸ¬ μƒμ„±μžλ₯Ό μ •μ˜ν•˜κ³ , 각각의 μƒμ„±μžμ—μ„œλŠ” ν•„μš”ν•œ λ§€κ°œλ³€μˆ˜λ§Œ λ°›λŠ” 방식이닀.

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) {
        this(servingSize, servings, 0);
    }

    public NutritionFacts(int servingSize, int servings, int calories) {
        this(servingSize, servings, calories, 0);
    }

    // ... parameters increase...

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

class Example {
    public static void main(String[] args) {
        NutritionFacts cocaCola = new NutritionFacts(240, 8, 100, 0, 35, 27);
    }
}
  • λ§€κ°œλ³€μˆ˜κ°€ λ§Žμ•„μ§€λ©΄ ν•΄λ‹Ή νŒŒλΌλ―Έν„°κ°€ μ–΄λ–€ ν•„λ“œ 값에 μ €μž₯λ˜λŠ”μ§€, λͺ‡ 개의 λ§€κ°œλ³€μˆ˜κ°€ μ–΄λ–€ μˆœμ„œλ‘œ μ „λ‹¬λ˜μ–΄μ•Ό ν•˜λŠ”μ§€ μ•ŒκΈ° 어렀움

  • ν˜„μž¬λŠ” IDEκ°€ μ‹λ³„ν•˜κΈ° μ‰½κ²Œ μ–΄λŠμ •λ„ 지원해주긴 ν•˜λ‚˜ κ²°κ΅­ 컴파일 μ—λŸ¬κ°€ λ°œμƒν•˜μ§€ μ•ŠλŠ” 이상 μ˜λ„ν•˜μ§€ μ•Šμ€ 값이 μ „λ‹¬λ˜κ±°λ‚˜ λŸ°νƒ€μž„μ— 였λ₯˜κ°€ λ°œμƒν•  수 있음

μžλ°” 빈즈(JavaBeans)

λ§€κ°œλ³€μˆ˜κ°€ μ—†λŠ” μƒμ„±μžλ‘œ 객체λ₯Ό λ§Œλ“  ν›„ setter λ©”μ„œλ“œλ“€μ„ ν˜ΈμΆœν•˜μ—¬ 값을 μ„€μ •ν•˜λŠ” 방식이닀.

class NutritionFacts {

    private int servingSize = -1;
    private int servings = -1;
    private int calories = 0;
    private int fat = 0;
    private int sodium = 0;
    private int carbohydrate = 0;

    public NutritionFacts() {
    }

    public void setServingSize(int servingSize) {
        this.servingSize = servingSize;
    }

    // all setter methods...

    public void setCarbohydrate(int carbohydrate) {
        this.carbohydrate = carbohydrate;
    }
}

class Example {
    public static void main(String[] args) {
        NutritionFacts cocaCola = new NutritionFacts();
        cocaCola.setServingSize(240);
        cocaCola.setServings(8);
        cocaCola.setCalories(100);
        cocaCola.setSodium(35);
        cocaCola.setCarbohydrate(27);
    }
}
  • λͺ…μ‹œμ μœΌλ‘œ ν•„λ“œ 이름을 ν˜ΈμΆœν•˜μ—¬ 값을 μ„€μ •ν•  수 μžˆμ–΄ 가독성 ν–₯상

  • ν•˜μ§€λ§Œ 객체 ν•˜λ‚˜λ₯Ό λ§Œλ“€λ €λ©΄ λ©”μ„œλ“œλ₯Ό μ—¬λŸ¬ 개 ν˜ΈμΆœν•΄μ•Ό ν•˜κ³ , 객체가 μ™„μ „νžˆ μƒμ„±λ˜κΈ° μ „κΉŒμ§€λŠ” 일관성(consistency)이 λ¬΄λ„ˆμ§

  • λͺ¨λ“  ν•„λ“œμ— λŒ€ν•΄ setter λ©”μ„œλ“œκ°€ μ—΄λ €μžˆκΈ° λ•Œλ¬Έμ— κ²°κ΅­ 클래슀의 λΆˆλ³€μ„±μ„ 보μž₯ν•  수 μ—†μŒ

λΉŒλ” νŒ¨ν„΄

본문의 주제인 λΉŒλ” νŒ¨ν„΄μ€ μœ„μ˜ 두 λ°©μ‹μ˜ μž₯점만 μ·¨ν•˜κ³  단점은 λ³΄μ™„ν•œ λ°©μ‹μœΌλ‘œ, λΉŒλ” νŒ¨ν„΄μ΄ λ™μž‘λ˜λŠ” 방식은 λ‹€μŒκ³Ό κ°™λ‹€.

  1. ν•„μˆ˜ λ§€κ°œλ³€μˆ˜λ§ŒμœΌλ‘œ μƒμ„±μž(ν˜Ήμ€ 정적 νŒ©ν„°λ¦¬ λ©”μ„œλ“œ)λ₯Ό ν˜ΈμΆœν•˜μ—¬ λΉŒλ” 객체λ₯Ό μ–»μŒ

  2. λΉŒλ” 객체가 μ œκ³΅ν•˜λŠ” μΌμ’…μ˜ μ„Έν„° λ©”μ„œλ“œλ“€λ‘œ μ›ν•˜λŠ” 선택 λ§€κ°œλ³€μˆ˜λ“€μ„ μ„€μ •

  3. λ§ˆμ§€λ§‰μœΌλ‘œ build() λ©”μ„œλ“œλ₯Ό ν˜ΈμΆœν•˜μ—¬ ν•„μš”ν•œ 객체λ₯Ό μ–»μŒ

λΉŒλ” 객체의 λ©”μ„œλ“œλ“€μ€ λͺ¨λ‘ thisλ₯Ό λ°˜ν™˜ν•˜λ„λ‘ κ΅¬ν˜„ν•˜μ—¬ λ©”μ„œλ“œ 연쇄(method chaining) 방식을 μ‚¬μš©ν•  수 μžˆλ‹€.

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;

    private NutritionFacts(Builder builder) {
        servingSize = builder.servingSize;
        servings = builder.servings;
        calories = builder.calories;
        fat = builder.fat;
        sodium = builder.sodium;
        carbohydrate = builder.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;
        }

        // λΉŒλ” 객체λ₯Ό 톡해 NutritionFacts 객체λ₯Ό 생성
        public NutritionFacts build() {
            return new NutritionFacts(this);
        }
    }
}

class Example {
    public static void main(String[] args) {
        NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8) // 1. ν•„μˆ˜ λ§€κ°œλ³€μˆ˜λ§ŒμœΌλ‘œ λΉŒλ” 객체λ₯Ό μ–»μŒ
                .calories(100) // 2. λΉŒλ” 객체가 μ œκ³΅ν•˜λŠ” μΌμ’…μ˜ μ„Έν„° λ©”μ„œλ“œλ“€λ‘œ μ›ν•˜λŠ” 선택 λ§€κ°œλ³€μˆ˜λ“€μ„ μ„€μ •
                .sodium(35)
                .carbohydrate(27)
                .build(); // 3. λ§ˆμ§€λ§‰μœΌλ‘œ build() λ©”μ„œλ“œλ₯Ό ν˜ΈμΆœν•˜μ—¬ ν•„μš”ν•œ 객체λ₯Ό μ–»μŒ
    }
}

λΉŒλ” νŒ¨ν„΄μ€ 특히 κ³„μΈ΅μ μœΌλ‘œ μ„€κ³„λœ ν΄λž˜μŠ€μ™€ ν•¨κ»˜ 쓰기에 μ’‹λ‹€. μƒμœ„ 계측을 좔상 λΉŒλ”κ°€ ν¬ν•¨λœ 클래슀둜 μ •μ˜ν•˜κ³ , ν•˜μœ„ ν΄λž˜μŠ€λ“€μ€ 좔상 λΉŒλ”λ₯Ό 상속받아 κ΅¬ν˜„ν•˜λ„λ‘ ν•˜λ©΄ λœλ‹€.

abstract class Pizza {

    final Set<Topping> toppings;

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

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

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

        // !!ν•˜μœ„ ν΄λž˜μŠ€λŠ” 이 λ©”μ„œλ“œλ₯Ό `this` λ°˜ν™˜ν•˜λ„λ‘ κ΅¬ν˜„ ν•„μš”
        protected abstract T self();
    }
}

class OguPizza extends Pizza {

    private final Size size;

    private OguPizza(Builder builder) {
        super(builder);
        size = builder.size;
    }

    public enum Size {SMALL, MEDIUM, LARGE}

    public static class Builder extends Pizza.Builder<Builder> {

        private final Size size;

        public Builder(Size size) {
            this.size = Objects.requireNonNull(size);
        }

        @Override
        public OguPizza build() { // 상속 받은 ν΄λž˜μŠ€κ°€ μ•„λ‹Œ OguPizzaλ₯Ό λ°˜ν™˜ν•˜λ„λ‘ μ˜€λ²„λΌμ΄λ”©
            return new OguPizza(this);
        }

        // ν•˜μœ„ ν΄λž˜μŠ€μ—μ„œλŠ” λ°˜ν™˜ νƒ€μž…μ΄ OguPizza.Builderκ°€ λ˜λ„λ‘ μ˜€λ²„λΌμ΄λ”©
        @Override
        protected Builder self() {
            return this;
        }
    }
}

class Example {
    public static void main(String[] args) {
        OguPizza pizza = new OguPizza.Builder(OguPizza.Size.SMALL)
                .addTopping(Pizza.Topping.HAM)
                .addTopping(Pizza.Topping.ONION)
                .build();
    }
}

μœ„ μ½”λ“œλ₯Ό 보면 OguPizza ν΄λž˜μŠ€λŠ” Pizza 클래슀의 λΉŒλ”λ₯Ό 상속받아 κ΅¬ν˜„ν•˜λ―€λ‘œ Pizza 클래슀의 λΉŒλ”κ°€ μ œκ³΅ν•˜λŠ” λ©”μ„œλ“œλ“€μ„ μ‚¬μš©ν•  수 있게 λœλ‹€. ν•˜μœ„ ν΄λž˜μŠ€μ—μ„œ κ΅¬ν˜„λœ build() λ©”μ„œλ“œλŠ” μƒμœ„ 클래슀의 λ©”μ„œλ“œκ°€ μ •μ˜ν•œ λ°˜ν™˜ νƒ€μž…(Pizza)이 μ•„λ‹Œ ν•΄λ‹Ήν•˜λŠ” ν•˜μœ„ 클래슀(OguPizza)λ₯Ό λ°˜ν™˜ν•˜λ„λ‘ μ˜€λ²„λΌμ΄λ”©ν•˜μ˜€λ‹€.(= 곡변 λ°˜ν™˜ 타이핑(covariant return typing))) μ΄λ ‡κ²Œ κ΅¬ν˜„ν•˜λ©΄ ν΄λΌμ΄μ–ΈνŠΈλŠ” ν˜•λ³€ν™˜μ— 신경쓰지 μ•Šκ³ λ„ ν•˜μœ„ 클래슀의 λΉŒλ”λ₯Ό μ‚¬μš©ν•  수 있게 λœλ‹€.

λΉŒλ” νŒ¨ν„΄μ˜ 단점

λΉŒλ” νŒ¨ν„΄μ—λ„ μ•„λž˜μ™€ 같은 단점이 μ‘΄μž¬ν•œλ‹€.

  • μœ„μ—μ„œ 확인할 수 μžˆλ“―μ΄ λΉŒλ” νŒ¨ν„΄μ„ μ‚¬μš©ν•˜λ©΄ 클래슀의 μΈμŠ€ν„΄μŠ€λ₯Ό λ§Œλ“œλŠ”λ° λ§Žμ€ μ–‘μ˜ μ½”λ“œ 증가

  • μ„±λŠ₯이 μ€‘μš”ν•œ μƒν™©μ—μ„œ λΉŒλ” 호좜 λ•Œλ§ˆλ‹€ μƒˆλ‘œμš΄ 객체λ₯Ό λ§Œλ“€κΈ° λ•Œλ¬Έμ— μ„±λŠ₯ μ €ν•˜κ°€ λ°œμƒν•  수 있음

ν•˜μ§€λ§Œ μ²˜λ¦¬ν•΄μ•Όν•  λ§€κ°œλ³€μˆ˜κ°€ 많고 μ„±λŠ₯에 큰 영ν–₯을 λ―ΈμΉ˜μ§€ μ•ŠλŠ” μƒν™©μ—μ„œλŠ” 보톡 λΉŒλ” νŒ¨ν„΄μ„ μ‚¬μš©ν•˜λŠ” 것이 μ’‹λ‹€.

Lombok @Builder

Lombok의 @Builder μ• λ„ˆν…Œμ΄μ…˜μ„ μ‚¬μš©ν•˜λ©΄ λΉŒλ” νŒ¨ν„΄μ„ κ΅¬ν˜„ν•˜κΈ° μœ„ν•΄ ν•„μš”ν•œ μ½”λ“œλ₯Ό μžλ™μœΌλ‘œ 생성해주기 λ•Œλ¬Έμ— 단점이 될 수 μžˆλŠ” μ½”λ“œλŸ‰μ˜ 증가λ₯Ό 방지할 수 μžˆλ‹€.

// dto
@Getter
public class OrderSaveRequest {

    private BigDecimal totalAmount;

    public OrderInfo toEntity(User user) {
        OrderInfo orderInfo = OrderInfo.builder()
                .user(user)
                .totalAmount(this.totalAmount)
                .build();

        return orderInfo;
    }
}

// entity
@Getter
@Builder
public class OrderInfo {

    private User user;
    @Builder.Default
    private String orderNumber = getUniqueOrderNumber();
    private BigDecimal totalAmount;
    @Builder.Default
    private LocalDateTime orderDate = LocalDateTime.now();

    private static String getUniqueOrderNumber() {
        return UUID.randomUUID().toString();
    }
}

Last updated