본문 바로가기
이펙티브 자바

생성자에 매개변수가 많다면 빌더를 고려하라

by 이상한나라의개발자 2023. 12. 21.

정적 팩터리와 생성자에는 똑같은 제약이 하나 있습니다. 선택적 매개변수가 많을 때 적절히 대응하기가 어렵다는 점입니다.

빌더패턴을 설명하기 이전에 점층적 생성자 패턴(telescoping constructor pattern)에 대해 알아 보겠습니다.

 

점층적 생성자 패턴은 객체 생성 시 다양한 매개변수 조합을 처리하기 위해 여러 생성자를 제공하는 방식입니다. 이 패턴은 매개변수가 적을 때는 잘 작동하지만, 매개변수의 수가 많아지면 코드의 가독성과 관리가 어려워질 수 있습니다. 

 

이펙티브 자바에서 예시로 이는 점층적 생성자 패턴 예시를 알아볼께요 

 

점층적 생성자 패턴 - 확장하기 어렵다. ( Telescoping Constructor Pattern )

 

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

    public NutritionFacts(int servingSize, int servings, int calories, int fat, int sodium) {
        this(servingSize, servings, calories, calories, sodium, 0);
    }

    public NutritionFacts(int servingSize, int servings, int calories, int calories1, int sodium, int carbohydrate) {
        this.servingSize = servingSize;
        this.servings = servings;
        this.calories = calories;
        this.fat = calories1;
        this.sodium = sodium;
        this.carbohydrate = carbohydrate;
    }

    @Override
    public String toString() {
        return "NutritionFacts{" +
                "servingSize=" + servingSize +
                ", servings=" + servings +
                ", calories=" + calories +
                ", fat=" + fat +
                ", sodium=" + sodium +
                ", carbohydrate=" + carbohydrate +
                '}';
    }

    public static void main(String[] args) {
        NutritionFacts nutritionFacts = new NutritionFacts(240, 8, 100, 0 35, 27);
        System.out.println("nutritionFacts = " + nutritionFacts);
    }
}

 

위 코드에서 필수를 제외한 모든 항목은 대부분 0 이다. 보통 이런 생성자는 사용자가 설정하길 원치 않는 매개변수까지 포함하기 쉬운데, 어쩔 수 없이 그런 매개변수에도 값을 지정해줘야 합니다. 앞 코드에서는 지방(fat)에 0을 넘겼습니다. 이 예에서는 매개변수가 6개 뿐이라 그리 나빠 보이지 않을 수 있지만 ( 전 6개도 어질어질 합니다 ) 수가 더 늘어나면 금세 걷잡을 수 없이 헷갈리게 됩니다. 

요약하자만 점층적 생성자 패턴도 쓸 수 있지만, 매개변수 개수가 많아지면 클라이언트 코드를 작성하거나 읽기 어렵습니다.

 

 

점층적 생성자 패턴의 또다른 예시

public class Coffee {
    private final String type;
    private final int sugar;
    private final int milk;
    private final int size;

    // 기본 생성자
    public Coffee(String type) {
        this(type, 0);
    }

    // 설탕을 추가하는 생성자
    public Coffee(String type, int sugar) {
        this(type, sugar, 0);
    }

    // 설탕과 우유를 추가하는 생성자
    public Coffee(String type, int sugar, int milk) {
        this(type, sugar, milk, 250);
    }

    // 모든 매개변수를 받는 생성자
    public Coffee(String type, int sugar, int milk, int size) {
        this.type = type;
        this.sugar = sugar;
        this.milk = milk;
        this.size = size;
    }

    @Override
    public String toString() {
        return "Coffee{" +
                "type='" + type + '\'' +
                ", sugar=" + sugar +
                ", milk=" + milk +
                ", size=" + size +
                '}';
    }
}

// 사용 예시
public class Main {
    public static void main(String[] args) {
        Coffee blackCoffee = new Coffee("Black");
        Coffee sweetCoffee = new Coffee("Latte", 2);
        Coffee sweetMilkyCoffee = new Coffee("Cappuccino", 1, 1);
        Coffee customCoffee = new Coffee("Espresso", 0, 0, 100);

        System.out.println(blackCoffee);
        System.out.println(sweetCoffee);
        System.out.println(sweetMilkyCoffee);
        System.out.println(customCoffee);
    }
}

 

해당 예시에서도 Coffee 클래스는 여러 public 생성자를 가지고 있으며, 각 생성자는 다른 조합의 매개변수를 받습니다. 이 방식은 각각의 생성자가 다른 매개변수 조합을 처리할 수 있도록 하지만, 매개 변수 수가 늘어날수록 복잡성이 대단히 증가합니다.

 

자반빈즈 패턴 ( JavaBeans Pattern )

이번에는 선택 매개변수가 많을 때 활용할 수 있는 두번째 대안인 자바빈즈 패턴(JavaBeans Pattern)을 알아 보겠습니다. 매개변수가 없는 생성자로 객체를 만든 후, setter 메서드들을 호출해 원하는 매개변수의 값을 설정하는 방식입니다.

 

public class NutritionFactsJavaBeansPattern {

    private int servingSize = -1; // (ml, 1회 제공량) 필수
    private int servings = -1; // (회, 총 n회 제공량) 필수
    private int calories = 0; // (1회 제공량당) 선택
    private int fat = 0; // (g/1회 제공량) 선택
    private int sodium = 0; // (mg/1회 제공량) 선택
    private int carbohydrate = 0; // (g/1회 제공량) 선택

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

    @Override
    public String toString() {
        return "NutritionFactsJavaBeansPattern{" +
                "servingSize=" + servingSize +
                ", servings=" + servings +
                ", calories=" + calories +
                ", fat=" + fat +
                ", sodium=" + sodium +
                ", carbohydrate=" + carbohydrate +
                '}';
    }

    public static void main(String[] args) {
        NutritionFactsJavaBeansPattern nutritionFacts = new NutritionFactsJavaBeansPattern();
        nutritionFacts.setServingSize(240);
        nutritionFacts.setServings(8);
        nutritionFacts.setCalories(100);
        nutritionFacts.setFat(10);
        nutritionFacts.setSodium(35);
        nutritionFacts.setCarbohydrate(27);
        System.out.println("nutritionFacts = " + nutritionFacts);
    }
}

 

 

점층적 생성자 패턴의 단점들이 자바빈즈 패턴에서는 더 이상 보이자 않습니다. 자바 빈즈 패턴에서는 setter 메서드를 호출하여 각각의 필드에 값을 넣는 방식을 사용하기 때문입니다. 이렇게 하면 어떤 값을 넣을때 필요한 항목만 넣고 사용할 수 있게 되므로 복잡성일 줄어듭니다. 하지만, 심각한 단점으로 자바빈즈 패턴에서는 객체 하나를 만들여면 메서드를 여러개 호출해야 하고, 객체가 완전히 생성되기 전까지는 일관성(consistency) 이 무너진 상태에 놓이게 됩니다.

 

바로 이런 문제점을 해결하기 위해 이펙티브 자바에서는 "생성자 매개 변수가 많다면 빌더를 고려하라" 라고 합니다.

 

우리가 흔히 Lombok에서 사용하는 @Builder 는 이런 문제점을 해결해주는 빌더 패턴입니다.

빌더 패턴은 점층적 생성자 패턴과 자바빈스 패턴의 장점만 취했습니다.

 

위에서 예시로 만든 Coffee 클래스를 빌더 패턴으로 만들겠습니다.

 

빌더패턴 ( Builder Pattern , Method Chaining )

// 외부 클래스
public class Coffee {
    private final String type;
    private final int sugar;
    private final int milk;
    private final int size;

    private Coffee(Builder builder) {
        this.type = builder.type;
        this.sugar = builder.sugar;
        this.milk = builder.milk;
        this.size = builder.size;
    }
	
    // 정적 내부 클래스
    public static class Builder {
        private String type;
        private int sugar;
        private int milk;
        private int size;

        public Builder type(String type) {
            this.type = type;
            return this;
        }

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

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

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

        public Coffee build() {
            return new Coffee(this);
        }
    }

    @Override
    public String toString() {
        return "Coffee{" +
                "type='" + type + '\'' +
                ", sugar=" + sugar +
                ", milk=" + milk +
                ", size=" + size +
                '}';
    }
}

// 사용 예시
public class Main {
    public static void main(String[] args) {
        Coffee coffee = new Coffee.Builder()
                .type("Latte")
                .sugar(1)
                .milk(2)
                .size(300)
                .build();

        System.out.println(coffee);
    }
}

 

이 예시에서 Coffee 클래스는 여러 매개변수를 가지고 있지만 Builder 내부 클래스를 통해 객체를 단계적으로 구성할 수 있습니다.

클라이언트는 Coffee 객체를 생성할 때 필요한 매개변수만 설정하면 됩니다.

이를 메서드 체이닝이라고도 합니다. 메서드 체이닝은 객체의 메서드들을 연속적으로 호출하는 코딩 스타일로, 각 메서드가 객체 자신(this)을 반환함으로써 가능해집니다. 이 방식은 코드를 더 읽 쉽고 간결하게 만들며, 특히 위와같이 빌더패턴에서 자주 사용됩니다.

 

정적 내부 클래스란? 

 

정적내부 클래스는 외부 클래스 내부에 정의 되지만, 외부 클래스의 인스턴스와는 독립적입니다. 쉽게 말해 메모리 로드가 다릅니다.

이 부분은 JAVA 에서 다시 다루도록 하겠습니다.