본문 바로가기
  • Where there is a will there is a way.
개발/java

빌더로 생성자 대체하기

by 소확행개발자 2020. 7. 6.

 

내가 자바로 웹 애플리케이션을 개발할때 인스턴스 생성에 대한 고민이 있었다.

 

Goods goods = new Goods();

goods.setGoodsId();
goods.setGoodsTitle();
goods.setGoodsCategory();
goods.set...

이와 같이 상품의 데이터가 많을때 ( null 이 허용되는 선택적 매개변수가 많을 때 ) 어떻게 대응하여 인스턴스를 생성할 것인가?

 

식품 포장의 영양정보를 표현하는 클래스를 생각해보자.

영양정보의 항목은 20개가 넘어간다고 해보자 그런데 대부분 제품은 이 선택 항목중 대다수 값이 0으로 표현된다고 하자

( 안쓰는 경우가 많다 라고 표현해보자 ) 

 

이런 클래스용 생성자는 어떤 모습일까 ?

 

기존에 개발자들은 점층적 생성자 패턴을 즐겨 사용했다고 한다.

 

/**
 * 식품 포장의 영양정보를 표현하는 클래스
 */
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){
        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);
    }

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




}
    /**
     * 지금은 매개변수가 6개 정보라 나빠보이지 않을 수 있다.
     * 하지만 수가 더 늘어난다면 금세 걷잡을 수 없게 된다.
     */
    @Test
    public void 생성자_테스트() {

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

        System.out.println(nutritionFacts);


    }

 

점층적 생성자 패턴은 매개변수가 많이질수록 코드를 작성하거나 읽기 어려워 진다.

 

그렇다면 자바빈즈 패턴은 어떨까?

 

package designpattern.builder.beans;


/**
 * 식품 포장의 영양정보를 표현하는 클래스
 */
public class NutritionFacts {

    private int servingSize;
    private int servings;
    private int calories;
    private int fat;
    private int sodium;
    private int carbohydrate;


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


    @Override
    public String toString() {
        return "NutritionFacts{" + "servingSize=" + servingSize + ", servings=" + servings
                + ", calories=" + calories + ", fat=" + fat + ", sodium=" + sodium
                + ", carbohydrate=" + carbohydrate + '}';
    }
}
    @Test
    public void 생성자_테스트_with_beans_pattern() {

        designpattern.builder.beans.NutritionFacts nutritionFacts = new designpattern.builder.beans.NutritionFacts();
        nutritionFacts.setServingSize(240);
        nutritionFacts.setServings(8);
        nutritionFacts.setCalories(100);
        nutritionFacts.setFat(0);
        nutritionFacts.setSodium(35);
        nutritionFacts.setCarbohydrate(27);

        System.out.println(nutritionFacts);
    }

 

자바 빈즈 패턴은 심각한 문제를 가지고 있다.

  • 객체 하나를 만들려면 메서드를 여러 개 호출해야 하고, 객체가 완전히 생성되기 전까지는 일관성이 무너진 상태에 놓이게 된다.
  • 일관성이 깨진 객체가 만들어지면 버그를 심은 코드가 될 수도 있다.
  • 자바 빈즈 패턴에서는 클래스를 불변으로 만들 수 없다. 스레드 안정성을 얻으려면 프로그래머가 추가 작업을 해줘야만 한다.

 

그렇다면 어떻게 해야할까 ?

 

점층적 생성자 패턴의 안정성과 자바 빈즈패턴의 가독성을 겸비한 빌더 패턴을 사용하면 된다.

 

package designpattern.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;


    private NutritionFacts(Builder builder) {
        servingSize = builder.servingSize;
        servings = builder.servings;
        calories = builder.calories;
        fat = builder.fat;
        sodium = builder.sodium;
        carbohydrate = builder.carbohydrate;
    }


    @Override
    public String toString() {
        return "NutritionFacts{" + "servingSize=" + servingSize + ", servings=" + servings
                + ", calories=" + calories + ", fat=" + fat + ", sodium=" + sodium
                + ", carbohydrate=" + carbohydrate + '}';
    }


    /**
     * 이너 클래스 static builder
     * <p>
     * 빌더는 생성할 클래스 안에 정적 멤버 클래스로 만들어두는 게 보통이다.
     * <p>
     * final 로 선언할 수 있기 떄문에 lombok builder 와 차이가 있다.
     */
    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;


        // 필수 매개 변수는 final 선언과 생성자 선언을 해주어야 한다.
        public Builder(int servingSize, int servings) {
            this.servingSize = servingSize;
            this.servings = servings;
        }


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


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


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


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


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


    }

}

 

/**
     * NutritionFacts 는 불변이며, 모든 매개변수의 기본값들을 한곳에 모아 뒀다.
     * <p>
     * 이 클러이언트 코드는 쓰기 쉽고, 무엇보다도 읽기 쉽다.
     */
    @Test
    public void 생성자_테스트_with_builder_pattern() {

        designpattern.builder.builder.NutritionFacts nutritionFacts =
                new designpattern.builder.builder.NutritionFacts.Builder(240, 8).calories(100)
                        .fat(0).sodium(35).carbohydrate(27).build();

        System.out.println(nutritionFacts);

    }

 

심화 응용 한 빌더를 보겠다. -> effective java 3판

 

package designpattern.builder.builder;

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

    }


}
package designpattern.builder.builder;

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


    @Override
    public String toString() {
        return "NyPizza{" + "size=" + size + ", toppings=" + toppings + '}';
    }
}


public class NyPizzaTest {

    @Test
    public void 심화_builder_test() {

        NyPizza pizza = new NyPizza.Builder(NyPizza.Size.SMALL).addTopping(Pizza.Topping.SAUSAGE).addTopping(
                Pizza.Topping.MUSHROOM).build();


        System.out.println(pizza);
    }

}

'개발 > java' 카테고리의 다른 글

자바 직렬화에 대한 생각정리  (0) 2020.07.26
객체의 생성  (0) 2020.07.09
Iterator design pattern  (0) 2020.05.28
JPA 2차 캐시란  (0) 2020.04.26
JPA 락  (0) 2020.04.25

댓글