아이템 2. 생성자에 매개변수가 많다면 빌더를 고려하라

Bobby·2022년 5월 12일
0

이펙티브 자바

목록 보기
2/7
post-thumbnail

클래스에 멤버변수가 많은 경우

  • 정적 팩토리 메소드와 생성자에는 선택적 매개변수가 많을 때 적절히 대응하기가 어렵다는 제약이 있다.
  • 어떤 클래스에는 많은 멤버변수들이 있다고 생각해 보자. 이러한 멤버변수들 중 필수 값과 선택 값이 존재한다면 인스턴스를 만들 때는 어떻게 할까?

1. 점층적 생성자 패턴

다음과 같은 영양정보를 표현하는 클래스를 생각해보자.

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회 제공량)       선택
    
    ...
    
}
  • 원하는 매개변수를 가지는 생성자를 모두 생성해 주어야 한다.
  • 매개변수가 적은 생성자는 더 많은 매개변수를 가진 생성자를 호출하여 점층적으로 확장해 나갈 수 있다.
	// servingSize, servings 두 항목은 필수 값이므로 반드시 필요하다.
	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;
    }

장점

  • 클라이언트 코드가 비교적 간단하다. 생성자 하나만을 호출한다.
  • 생성자를 통해 초기 값을 설정 하므로 불변 객체를 만들 수 있다.(final)
  • 원하는 매개변수를 모두 포함한 생성자 중 가장 짧은 것을 골라 호출 하면 된다.
	// 원하는 매개변수 만을 포함한 생성자가 없다면 
    // 4번째 매개변수 처럼 원하지 않는 값을(0) 넘겨 주어야 한다.
    NutritionFacts cocaCola = new NutritionFacts(240, 8, 100, 0, 35, 27);

단점

  • 만약 더 많은 수의 선택적 매개변수를 가지는 클래스라면 더 많은 생성자를 만들어야 하고 이는 클라이언트 코드를 작성하거나 읽기 불편할 것이다.
  • 또한 매개변수의 순서와 개수도 주의하여 작성해야 한다. 순서와 개수가 잘못 입력 되더라도 컴파일 단계에서는 에러를 잡지 못하고 런타임에 원하지 않는 동작을 하게 될 수도 있다.

2. 자바빈즈 패턴

  • 매개변수가 없는 기본 생성자를 생성한 후, setter 메소드를 호출하여 원하는 매개변수의 값을 설정하는 방식이다.
public 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 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();

// 원하는 매개변수 값을 설정
cocaCola.setServingSize(240);
cocaCola.setServings(8);
cocaCola.setCalories(100);
cocaCola.setSodium(35);
cocaCola.setCarbohydrate(27);

장점

  • 인스턴스 생성이 간단하다.
  • 점층적 생성자 패턴과는 다르게 원하는 변수 값 만을 설정 할 수 있다.
  • 또한 어떤 값을 설정하는지 세터 메소드에 표현이 되므로 잘못 된 값을 넣는 실수를 방지 할 수 있다.

단점

  • 객체 하나를 만들기 위해 많은 메소드를 호출해야 한다.
  • 점층적 생성자 패턴에서는 매개변수들이 유효한지를 생성자에서만 확인하면 일관성을 유지 할 수 있었지만 자바빈즈 패턴에서는 값들이 완전히 설정되기 전까지는 일관성이 무너진 상태에 놓인다.
  • 객체 생성 후 메소드를 통해 값을 설정하므로 불변 객체로 만들 수 없다.

3. 빌더 패턴

  • 필요한 객체를 직접 생성하는 것이 아닌 내부의 빌더 객체를 통해서 생성한다.
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;
        }

		// 각 매개변수를 받아서 값을 설정하고 빌더를 리턴
        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;
        }

		// build() 메소드를 통해서 NutritionFacts 객체를 생성한다.
        public NutritionFacts build() {
            return new NutritionFacts(this);
        }
    }

	// 외부에서는 생성자를 호출하지 못하도록 private 
    // 빌더가 사용하는 모든 매개변수를 가진 생성자
    private NutritionFacts(Builder builder) {
        servingSize  = builder.servingSize;
        servings     = builder.servings;
        calories     = builder.calories;
        fat          = builder.fat;
        sodium       = builder.sodium;
        carbohydrate = builder.carbohydrate;
    }
  • 클라이언트에서 객체를 생성하는 방법
NutritionFacts cocaCola = 
	new NutritionFacts.Builder(10, 10) // 내부 빌더 객체 생성자
    			.calories(100) // 원하는 값만 설정
                .sodium(35)
                .build(); // 내부 빌더 객체의 메소드 

장점

  • 클라이언트 코드가 읽고 쓰기 쉽다.
  • 내부 빌더 객체가 생성자를 통해 객체를 생성하므로 불변 객체를 만들 수 있다.
  • 원하는 매개변수만 설정할 수 있다.
  • 어떤 값이 들어가는지 명시적으로 확인할 수 있다.

단점

  • 객체 내부에 빌더 클래스를 생성해야 하므로 코드가 길어지고 복잡해 진다.

핵심정리

  • 생성자나 정적 팩터리가 처리해야 할 매개변수가 많다면 빌더 패턴을 선택하는 것이 더 낫다. 매개변수 중 다수가 필수가 아니거나 같은 타입이면 특히 더 그렇다. 빌더는 점층적 생성자보다 클라이언트 코드를 읽고 쓰기가 훨씬 간결하고, 자바빈즈보다 훨씬 안전하다.
profile
물흐르듯 개발하다 대박나기

0개의 댓글