@Builder class 선언 vs constructor 선언

Sera Lee·2022년 2월 28일
4

Lombok 잘 사용하기

목록 보기
1/2

Builder 패턴은 언제 사용하는게 좋을까?

  • 선택적 매개변수가 많을 때
    • 매개변수가 많고 게다가 선택적 매개변수가 많을 때 constructor 를 사용하려면 선택적매개변수가 각기 다른 생성자를 만들어줘야하는데, 매개변수가 많은 경우에는 정의해 줘야하는 생성자가 굉장히 많아진다.

Builder 패턴 예제 코드

  • 다음은 effectve java 아이템2 에 정의된 빌드패턴이다.
    • 빌드패턴은 다음을 만족해야한다.
      • class 내부에 static class Builder 가 정의된다.

      • Builder 클래스 내부에는 class의 매개변수들이 선언되는데 필수 매개변수는 final이 선언되어야 하고, 선택 매개변수는 기본값을 설정한다.

      • Builder 생성자에는 필수 매개변수들이 파라미터로 들어가게 된다.

        public Builder(servingSize, servings) {}
      • 선택 매개변수들은 method 이름으로 하나씩 정의한다.

        public Builder calories(int val) {
            this.calories = val;
            return this;
        }
        
        public Builder fat(int val) {
             this.fat = val;
             return this;
        }
      • 클래스를 리턴하는 build() 함수를 정의한다.

        public NutritionFacts build() {
             return new NutritionFacts(this);
        }
      • class 에 Builder 를 파라미터로 받는 생성자를 작성한다.

        public NutritionFacts(Builder builder) {
           servingSize = builder.servingSize;
           servings = builder.servings;
           calories = builder.calories;
           fat = builder.fat;
           sodium = builder.sodium;
           carbohydrate = builder.carbohydrate;
        }
  • 작성된 코드는 다음과 같다.

public class NutritionFacts {

    private final int servingSize; // 필수 매개변수
    private final int servings;    // 필수 매개변수
    private final int calories;    // 선택 매개변수
    private final int fat;         // 선택 매개변수

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

    public static class Builder {

        private final int servingSize; // 필수 매개변수는 반드시 final 을 선언한다.
        private final int servings;    // 필수 매개변수는 반드시 final 을 선언한다.

        // 선택 매개변수는 기본값으로 설정한다.
        private int calories = 0;    // 선택 매개변수
        private int fat = 0;         // 선택 매개변수

        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 NutritionFacts build() {
            return new NutritionFacts(this);
        }
    }
}
  • 이 클래스를 사용하는 클라이언트 코드
// 이 경우는 필수값 servingSize, servings 중 하나의 값이 비었으므로 컴파일에러가 난다.
new NutritionFacts.Builder(10).fat(1000).calories(1220).build(); 

// 옵셔널 한 값을 필수로 셋팅하지 않아도 된다.
new NutritionFacts.Builder(10, 10).build();

// 옵셔널 한 값은 다음처럼 사용될 수 있다
new NutritionFacts.Builder(10, 10).fat(1000).calories(1220).build();
new NutritionFacts.Builder(10, 10).fat(10000).build();

@Builder 를 사용하기

  • @Builder 를 사용하는 방법은 class 위에 선언하는 방법, constructor에 선언하는 방법이 있다.

@Builder를 class 위에 선언하기

  • compile 후 generate 된 NutritionFacts.class
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;
}

  public static NutritionFactsBuilder builder() {
	  return new NutritionFactsBuilder();
  }
}
  • 문제점
    • 모든 매개변수가 생성자의 파람으로 들어가게 된다.
    • 객체 생성 시 받지 않아야 할 매개변수들도 빌더에 노출이 된다.
    • 이를 해결 하기 위해 constructor 위에 @Builder 를 선언한다.

@Builder 를 constructor 위에 선언하기

  • compile 후 generate 된 NutritionFacts.class
public class NutritionFacts {
    private int servingSize;
    private int servings;
    private int calories;
    private int fat;
  
    public NutritionFacts(int servingSize, int servings) {
        this.servingSize = servingSize;
        this.servings = servings;
        this.calories = 0;
        this.fat = 0;
    }

    public static NutritionFacts.NutritionFactsBuilder builder() {
        return new NutritionFacts.NutritionFactsBuilder();
    }

    public static class NutritionFactsBuilder {
        private int servingSize;
        private int servings;
        private int calories;
        private int fat;

        NutritionFactsBuilder() {
        }

        public NutritionFacts.NutritionFactsBuilder servingSize(int servingSize) {
            this.servingSize = servingSize;
            return this;
        }

        public NutritionFacts.NutritionFactsBuilder servings(int servings) {
            this.servings = servings;
            return this;
        }

        public NutritionFacts.NutritionFactsBuilder calories(int calories) {
            this.calories = calories;
            return this;
        }

        public NutritionFacts.NutritionFactsBuilder fat(int fat) {
            this.fat = fat;
            return this;
        }

        public NutritionFacts build() {
            return new NutritionFacts(this.servingSize, this.servings, this.calories, this.fat);
        }

        public String toString() {
            return "NutritionFacts.NutritionFactsBuilder(servingSize=" + this.servingSize + ", servings=" + this.servings + ", calories=" + this.calories + ", fat=" + this.fat +")";
        }
    }
}
  • class 선언 시 문제가 되었던 객체 생성 시 받지 않아야 할 매개변수들도 빌더에 노출이 되는 문제점을 해결할 수 있다.
  • 선택적 매개변수인 calories, fat 는 Builder에 노출이 되지 않는 것을 확인할 수 있다.
  • 하지만 테스트코드를 짤때, 귀찮아진다.
    • 만약, calories 를 초기값으로 세팅을 하고자 할 때 Builder 를 통해서는 제공이 안되므로 calories 를 @Setter 를 제공해서 다음과 같이 NutrionFacts를 생성한 후 calories 를 set 할 수 있다.

      NutritionFacts nutritionFacts = NutritionFacts.builder.servings(10).servingSize(1).build();
      nutritionFacts.setCalories(1000);
    • 부득이하게 @Setter 를 선언하게 될 수 밖에 없고,

      • 객체의 안전성이 보장받기 힘들어진다.
      • 특히 엔티티에서는 @Setter를 사용 시 해당 업데이트 문이 언제 발생했는지 추적하기가 어렵다.
  • Builder에서도 선택적 매개변수를 세팅할 수 있도록 우아하게 @Builder를 사용할 수 있는 방법이 없을까?

💎빌더패턴의 이점을 살려(필수매개변수,선택매개변수) @Builder 사용하기

@Builder(builderMethodName = "")을 사용한다.

@Builder(builderMethodName = "innerBuilder")
public class NutritionFacts {

    private final int servingSize; // 필수 매개변수
    private final int servings;    // 필수 매개변수
    @Builder.Default private int calories = 0;    // 선택 매개변수
    @Builder.Default private int fat = 0;         // 선택 매개변수

    public static NutritionFactsBuilder builder(int servingSize, int servings) {
        return innerBuilder().servings(servings).servingSize(servingSize);
    }
}
  • builder 함수를 쓰기 위해서는 무조건 servingSize, servings 를 주입 받아야 하는 custom method 를 선언을 해두면, 처음에 선언한 innerBuilder() 를 통해서 객체를 만들고, 다음에 롬복에서 제공하는 builder 패턴을 사용하겠다는 의미이다.
  • @Builder.Default 값이 할당 되지 않은 경우 초기값을 설정하겠다는 뜻이다.
  • compile 후 generate 된 NutritionFacts.class
public class NutritionFacts {
    private final int servingSize;
    private final int servings;
    private int calories;
    private int fat;

    public static NutritionFactsBuilder builder(int servingSize, int servings) {
        return innerBuilder().servings(servings).servingSize(servingSize);
    }

    private static int $default$calories() {
        return 0;
    }

    private static int $default$fat() {
        return 0;
    }

    NutritionFacts(int servingSize, int servings, int calories, int fat) {
        this.servingSize = servingSize;
        this.servings = servings;
        this.calories = calories;
        this.fat = fat;
    }

    public static NutritionFactsBuilder innerBuilder() {
        return new NutritionFactsBuilder();
    }
}
  • 이 클래스를 사용하는 클라이언트 코드는 다음과 같다.
NutritionFacts nutritionFacts = NutritionFacts.builder(10, 200).fat(130).calories(222).build();

0개의 댓글