iterm 2. 생성자에 매개변수가 많다면 빌더를 고려하라.
item 2. 생성자에 매개변수가 많다면 빌더를 고려하라.
정적 팩토리와 생성자에는 공통적으로 제약이 있다.
책에 나온 예시를 보고 설명해보겠다.
식품 포장의 영양정보를 표현하는 클래스가 있다고 생각해보자.
- 영양 정보 :
- servingSize →(ml, 1회 제공량) :
필수
- servings → (회, 총 n회 제공량) :
필수
- calories → (1회 제공량당) :
선택
- fat →(g/1회 제공량) :
선택
- sodium → (mg/1회 제공량) :
선택
- carbohydrate → (g/1회 제공량) :
선택
- servingSize →(ml, 1회 제공량) :
등등 …. 식품의 영양정보가 총 20개가 있다고 가정해보자.
대부분의 제품은 선택 항목 중 대다수의 값이 그냥 0이다.
이렇게 많은 필드를 가지고 있는 것들은 생성자와 정적 팩터리는 어떤 모습을 하고 있을까?
- 아래와 같은 예제를 점층적 생성자 패턴( 생성자 체이닝)이라고 부른다. → 아래 모습은
this
를 사용하지 않은 형태
public class NutritionFacts {
private final int servingSize; // (ml, 1회 제공량) 필수
private final int servings; // (회, 총 n회 제공량) 필수
private final int calories; // (1회 제공량당) 선택 선택
private final int fat; // (g/1회 제공량) 선택
private final int sodium; // (mg/1회 제공량) 선택
private final int carbohydrate; // (g/1회 제공량) 선택
public NutritionFacts2(int servingSize, int servings) {
this.servingSize = servingSize;
this.servings = servings;
this.calories = 0;
this.fat = 0;
this.sodium = 0;
this.carbohydrate = 0;
}
public NutritionFacts2(int servingSize, int servings, int calories) {
this.servingSize = servingSize;
this.servings = servings;
this.calories = calories;
this.fat = 0;
this.sodium = 0;
this.carbohydrate = 0;
}
public NutritionFacts2(int servingSize, int servings, int calories, int fat) {
this.servingSize = servingSize;
this.servings = servings;
this.calories = calories;
this.fat = fat;
this.sodium = 0;
this.carbohydrate = 0;
}
public NutritionFacts2(int servingSize, int servings, int calories, int fat, int sodium) {
this.servingSize = servingSize;
this.servings = servings;
this.calories = calories;
this.fat = fat;
this.sodium = sodium;
this.carbohydrate = 0;
}
public NutritionFacts2(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;
}
}
this
를 사용한 상태 - 책에 있는 상태이다. 훨씬 더 가독성이 좋다.(중복성 제거)
public class NutritionFacts {
private final int servingSize; // (ml, 1회 제공량) 필수
private final int servings; // (회, 총 n회 제공량) 필수
private final int calories; // (1회 제공량당) 선택 선택
private final int fat; // (g/1회 제공량) 선택
private final int sodium; // (mg/1회 제공량) 선택
private final int carbohydrate; // (g/1회 제공량) 선택
public NutritionFacts2(int servingSize, int servings) {
this(servingSize, servings, 0);
}
public NutritionFacts2(int servingSize, int servings, int calories) {
this(servingSize, servings, calories, 0);
}
public NutritionFacts2(int servingSize, int servings, int calories, int fat) {
this(servingSize, servings, calories, fat, 0);
}
public NutritionFacts2(int servingSize, int servings, int calories, int fat, int sodium) {
this(servingSize, servings, cal정
점층적 생성자 패턴
점층적 생성자 패턴이란?
- 생성자를 필수 매개변수 1개만 받는 생성자, 필수 매개변수 1개와 선택 매개변수 1개를 받는 생성자, 선택 매개변수 2개를 받는 생성자 등에 형태로 매개변수 개수만큼 생성자를 늘리는 방식이다.
점층적 생성자 패턴의 단점
인스턴스를 만들 때 어떤 파라미터를 줘야하는지 모른다.. 아래와 같이 첫번째는 servingSize, servings .. 하지만 기억하기 어렵다.
NutritionFacts cocaCola = new NutritionFacts(240, 8, 100, 0, 35, 27);
➡️단점을 극복 할려고 만든 기능 1
- 인텔리제이에서 제공하는 파라미터 정보들
➡️단점을 극복하려고 만든 기능 2
하지만.. 인텔리제이가 없는 곳이라면..? 🤔 → 일일이 클래스로 들어가서 체크하면서 파라미터를 넣어줬다고한다.
위와 같이 파라미터가 많은 경우
- 사용할 수 있는 무난한 선택방법은 생성자를 늘린다. → 단점은 파라미터가 많아지면 값이 많아져 어디로 들어가는지도 헷갈린다.
하지만 또 다른 방법이 있다.
바로..
자바 빈즈(JavaBeans pattern)
자바 빈즈란?
매개 변수가 없는 생성자로 객체를 만든 후, 세터(setter) 메서드들을 호출해 원하는 매개 변수의 값을 설정하는 방식이다.
자바 빈즈는 자바의 표준 스펙 중에 하나이다.
public class NutritionFacts3 {
//필드 (기본값이 있다면) 기본값으로 초기화한다.
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 NutritionFacts3() { // 기본 생성자
}
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;
}
}
자바빈즈의 장점
아래와 같이 인스턴스를 만드는 것이 간단해진다.
NutritionFacts cocaCola = new NutritionFacts();
자바빈즈의 단점
- 객체가 완전히 생성되기 전까지는 일관성(consistency)가 무너진 상태에 놓인다.
- setter로 값을 생성해주면 필수값이 아니게된다
- 어디까지 세팅을 해야줘야하는지 알기가 어려워진다. (이 객체는 어느 값까지? 세팅해줘야하나?)
- 주석을 남기고 문서화하는 방법밖에없다. → 더욱 번거로워짐
- 클래스를 불변 객체로 만들 수 없다.
- setter를 통해서 값을 설정하기 때문에 한번 설정한 값을 변경되지 않게 하려고한다면 되질 않는다.
불변 객체로 만들려면 ?
- 생성자로 다 만들어서 setter를 주기 말아야한다.
- freezing 이라는 것을 쓰자 → 자바는 없다.(구현) 자바스크립트에 오브젝트가 가지고 있는 프리징이라는 function이 있다고 한다. 📢참고
현업에서 많이 사용되지않음.
빌더(Builder)
빌더패턴은 점층적 생성자 패턴의 안전성과 자바빈즈 패턴의 가독성을 겸비한 패턴
- ① 필요한 객체를 직접만듬(필수 매개변수,선택 매개변수)
- ② 필수 매개변수만 생성자(혹은 정적 팩토리)를 만들어줌
- ③ 빌더 객체가 제공하는 일종의 세터 메서드들로 원하는 선택 매개변수들을 설정한다.
- 특징 : 리턴이 void가 아니라 Builder 타입이다. (setter와 차이점) → 플루언트 API 또는 메서드 체이닝(…….)을 하게 한다.
- ④ 최종적으로 빌드 메서드
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 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;
}
//③ 설정을해야하는 것들 -> setter와 같은 기능이라고 생각하면된다.
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) {
this.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;
}
}
위와같이 코드를 작성하면.. 메소드 체이닝이 가능하다.
- 아래와 같이 코드 방식을 메서드 호출이 흐르듯 연결된다는 뜻으로
플루언트API
또는메서드 체이닝
이라 한다
public static void main(String[] args) {
NutritionFacts cocaCola = new Builder(240, 8)
.calories(100)
.sodium(35)
.carbohydrate(27)
.build();
}
빌더 패턴의 장점
- 인스턴스를 생성할 때 필수에 해당하는 것들을 생성자로 만들 수 있다.(자바빈즈보다 훨씬 안전함 → consistency하게 할 수 있음)
- 자바빈즈의 장점 + 점층적 생성자 패턴 장점 → 빌더 패턴
빌더 패턴의 단점
- 모든 경우에 빌더를 쓰는 것이 아니다. → 매개변수가 많을 경우 고려
- 빌더 패턴을 사용시 코드가 길어짐 (중복도 있음)
빌더는 이럴 때 쓰자.
필수적인 필드와 선택적인 필드가 섞여 있을경우 매개변수의 생성자가 많고 불변(immutable)하게 만들고 싶을 때 고려해보자!
번외
빌더 패턴의 단점 극복
- 롬복(Lombok)을 사용한다! → @Builder
import lombok.Builder;
@Builder
public class NutritionFacts4 {
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) {
NutritionFacts4 nutritionFacts4 = new NutritionFacts4Builder()
.servingSize(100)
.servings(101)
.build();
}
}
코드가 확 줄어드는 것(간결)을 알 수 있다.
간략한 롬복 원리
룸복이 컴파일 시점에서 코드를 조작한다.
NutritionFacts4Builder
빌더를 만들어준다.
컴파일 코드를 확인해보면
- target → classes 에서 클래스 폴더를 찾아들어가면
NutritionFacts4Builder
빌더를 확인할 수 있다.
하지만
단점이 존재한다.
@Builder만 쓸 경우의 단점
- 롬복 빌더의 첫번째 단점 : 모든 파라미터를 받는 생성자가 생긴다. → 확인 방법 : Command + p
모든 파라미터를 받는 생성자를 만들기 싫다면..?
@AllArgsConstructor
를 사용한다. → 외부에서는 NutritionFacts4
를 못쓴다. 내부적으로 builder만 사용가능하다.
@AllArgsConstructor(access = AccessLevel.PRIVATE)
- 롬복 빌더의 두 번째 단점 : 필수 값을 지정해줄 수 없다.
public static void main(String[] args) {
NutritionFacts cocaCola = new Builder(240, 8) // X 안됨
.calories(100)
.sodium(35)
.carbohydrate(27)
.build();
}
필수 매개변수를 생성자로 넣고 선택 매개변수를 넣는 것이 어려워진다.