본문 바로가기

Java

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

요약

생성자에 매개변수가 많은 경우에는

생성자, 정적 팩터리 메서드(아이템1:https://jun9-dailycoding.tistory.com/10?category=993134)을 사용한다면, 가독성이 떨어지거나 데이터 불변성을 보장할 수 없다는 단점이 있다. 

빌더패턴을 통해 생성자, 정적 팩터리 메서드의 단점을 제거하고 장점만을 취할 수 있다.

 

 

1. 점층적 생성자 패턴(telescoping constructor pattern)

필수 매개변수만 받는 생성자,

필수 매개변수와 선택 매개변수 1개를 받는 생성자,

필수 매개변수와 선택 매개변수 2개를 받는 생성자,

...

필수 매개변수와 선택 매개변수를 전부 다 받는 생성자

 

형태로 생성자의 매개변수를 늘려가는 패턴이다.

예시) 식품의 영양정보를 표현하는 클래스

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

    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 carbonhydrate) {
        this.servingSize = servingSize;
        this.servings = servings;
        this.calories = calories;
        this.fat = fat;
        this.sodium = sodium;
        this.carbonhydrate = carbonhydrate;
    }
}

 

보다시피 가독성이 매우 떨어진다.

원하는 생성자를 찾기 위해 생성자마다 매개변수가 몇 개 인지 세어가며 읽어야 하고

타입이 같은 매개변수가 여러개라면 값을 바꿔쓰기 쉽고 찾기 어려운 버그로 이어질 수 있다.

위 예시에서는 int형이 늘어져 있어 servitSize와 servings의 값을 바꾸어써도 컴파일 에러가 발생하지 않아 버그를 놓치기 쉽다.

 

2. 자바 빈즈 패턴

매개변수가 없는 생성자로객체를 만들고, setter로 메서드를 호출해 원하는 매개변수의 값을 설정하는 방식이다.

 

아래 예시처럼 setter함수를 호출하여 값을 지정한다.

NutritionFacts cocaCola = new  NutritionFacts();
cocaCola.setServingSize(240);
cocaCola.setServings(8);
cocaCola.setCalories(100);
cocaCola.setSodium(35);
cocaCola.setCarbonhydrate(27);

매개변수가 많다면, 그 매개변수 많은 setter함수를 호출해줘야 한다.

모든 매개변수가 setting될 때까지 객체의 일관성이 무너진 상태가 된다. 

위 예시에서는 carbonhydrate까지 setting되기 전까지는 해당 객체의 일관성을 보장할 수 없다.!!

 

 

3. (BEST) 빌더 패턴

1. 필수 매개변수만으로 생성자(혹은 정적 팩터리)를 호출해 빌더 객체를 호출한다. 

2. 빌더가 제공하는 세터 메서드들로 원하는 선택 매개변수를 지정한다.

3. 매개변수가 없는 build메서드를 호출하여 객체를 얻는다.

 

보통 클래스 내부의 static 클래스로 생성한다.

예시)

package com.naver.calendar;

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

    private NutritionFacts(NutritionFactsBuilder builder) {
        this.servingSize = builder.servingSize;
        this.servings = builder.servings;
        this.calories =  builder.calories;
        this.fat =  builder.fat;
        this.sodium =  builder.sodium;
        this.carbonhydrate =  builder.carbonhydrate;
    }
    
    private static final class NutritionFactsBuilder {
        private int servingSize; //필수
        private int servings; // 필수
        private int calories = 0;
        private int fat = 0;
        private int sodium = 0;
        private int carbonhydrate = 0;

        public NutritionFacts build() {
            return new NutritionFacts(this);
        }
        
        private NutritionFactsBuilder(int servingSize, int servings) {
            this.servingSize = servingSize;
            this.servings = servings;
        }

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

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

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

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

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

        public NutritionFactsBuilder withCarbonhydrate(int carbonhydrate) {
            this.carbonhydrate = carbonhydrate;
            return this;
        }
        
    }

}

빌더의 세터들을 자기자신을 반환하기 때문에 연쇄적으로 호출할 수 있다.

NutritionFactsBuilder cocaCola = new NutritionFacts.NutritionFactsBuilder(240,8)
                                                                                           .calories(100).sodium(35).carbonhydrate(27).build();

가독성이 좋다!!

또한 빌더 패턴 내에 유효성 검사 코드를 넣으면 생성시 어떤 매개변수가 잘못되었는지 알 수 있다. 마찬가지로 불변검사 코드도 넣을 수 있다.

 

빌더패턴은 이런 방식으로 점층적 생성자 패턴의 안전성과 자바빈즈 패턴의 가독성을 겸비했다.