Java/이펙티브 자바(Effective Java)

[이펙티브 자바(Effective Java)][객체 생성과 파괴] - 2. 생성자에 매개변수가 많다면 빌더를 고려해라

얄루몬 2022. 4. 5. 15:20

📖본 포스팅은 'Effective java - 조슈아' 님의 책을 보고 작성되었습니다.


[점층적 생성자 패턴(Telescoping Constructor Pattern)]

  • 필수 매개변수만 받는 생성자부터 ~ 필수 매개변수 + 선택 매개변수 1 ... n개 전부 다 받는 생성자까지 모든 생성자를 작성하는 방법

[점층적 생성자 패턴 예시 코드]

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, 10);
    }

    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;
    }
}
  • 점층적 생성자 패턴을 사용할 때 원하는 인스턴스 생성을 위해서는 원하는 매개변수를 모두 포함하고 있는 생성자 중 가장 짧은 것을 골라서 호출해주면 된다.

[점층적 생성자 패턴 장점]

  • 원하는 매개변수를 모두 포함한 생성자 중 가장 짧은 것을 호출하면 된다.

[점층적 생성자 패턴 단점]

  • 점층적 생성자 패턴은 선택적 매개변수가 많아지면 적절한 대응이 어려워진다.
  • 또한 매개변수가 많아지면 클라이언트 코드를 작성하거나 읽기 어렵다.

 

[자바빈즈 패턴(JavaBeans Pattern)]

  • 점층적 생성자 패턴의 단점을 해결하고자 자바빈즈 패턴을 사용해보았다.
  • 자바빈즈 패턴은 매개변수가 없는 생성자로 객체를 만든 뒤, 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; //필수
    
    
    //참고: 생성자는 리턴값이 없지만 void를 사용하지 않는다.
    public NutritionFacts() {} //매개변수 없는 생성자로 객체 생성
    
    
    //Setter 메서드를 통한 매개변수 값 설정.
    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);

[자바빈즈 패턴의 장점]

  • 코드가 길어지긴 하지만 인스턴스를 만들기 쉬워지고 결과적으로는 더 읽기 쉬운 코드가 된다.

[자바빈즈 패턴의 단점]

  • 객체 하나를 만들기 위해 여러 개의 메서드를 호출해야 하고, 객체가 완전히 생성되기 전까지 일관성이 무너진 상태에 놓이게 된다.
  • 클래스를 불변으로 만들 수 없고 스레드 안전성을 얻으러면 프로그래머가 추가 작업을 진행해야 한다.
  • 생성이 끝난 객체를 수동으로 freezing하기 전에는 사용할 수 없게 하여서 자바빈즈 패턴의 단점을 보완해보려 시도했다.
    • 그러나 이 방법은 다루기 어렵다
    • 또한 프로그래머가 freeze 메서드를 확실히 호출했는지 컴파일러의 보증이 없기 때문에 런타임 오류에 매우 취약하다(런타임 에러는 에러를 잡는 것이 매우 어렵다.)

 

[빌더 패턴(Builder Pattern)]

  • 빌더 패턴? 점층적 생성자 패턴의 안정성 + 자바빈즈 패턴의 가독성
  • 클라이언트
    • 필요한 객체를 직접 만들지 않고 필수 매개변수만으로 생성자를 호출한 뒤 빌더 객체를 얻는다.
    • 그 후 빌더 객체가 제공하는 일종의 Setter 메서드들로 원하는 선택 매개변수들을 설정한다.
    • 마지막으로 매개변수가 없는 build 메서드를 통해 우리에게 필요한 객체를 얻는다.

[빌더 패턴 예시 코드]

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 calories){
            this.calories = calories;
            return this;
        }

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

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

        public Builder carbohydrate(int carbohydrate){
            this.carbohydrate = carbohydrate;
            return this;
        }
		
        
        //매개변수가 없는 build 메서드를 사용하여 우리에게 필요한 객체를 얻는다. 
        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;
    }
}

[빌더 패턴의 특징]

  • NutritionFacts 클래스는 불변이고 모든 매개변수의 기본값들을 한곳에 모아 둔다.
  • 빌더의 Setter 메서드들은 빌더 자신을 반환하기 때문에 연쇄적인 호출이 가능하다.

[빌더 패턴의 인스턴스 생성]

NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
                                            .calories(100)
                                            .sodium(35)
                                            .carbohydrate(27).build();
  • 빌더 패턴 코드는 쓰기 쉽고, 읽기도 쉽다. 계층적으로 설계된 클래스와 함께 사용하면 더 좋다.

 

[빌더 패턴과 계층적 클래스를 사용한 방법]

추후에 정리...

 

[핵심]

  • 생성자나 정적 팩토리가 처리해야 할 변수가 많다면 빌더 패턴을 고려하자
    • 점층적 생성자보다 코드를 읽고 쓰기가 간결하다.
    • 자바빈즈보다 훨씬 안전하다.