생성자에 매개변수가 많다면 빌더를 고려하라(Effective Java Item 2)

hun·2024년 8월 25일

Java

목록 보기
3/5
post-thumbnail

일반적으로 클래스의 인스턴스화 하는 방법 중 하나는 생성자를 사용하는 것 이다.

public class Item2 {
	NuitionFacts nutritionFacts = new  NutritionFacts(240,5,100,0,27);
}

class NutritionFacts{
    private final int servingSize; //필수
    private final int servings; //필수
    private final int calories; //선택
    private final int fat;  //선택
    private final int sodium; //선택

    public NutritionFacts(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;
    }

}

생성자를 통한 인스턴스화는 매개변수가 많아지면 코드를 작성하거나 읽기 어렵다

다른 방법으로는 자바빈즈 패턴이 있다.

class NutritionFacts{
    private int servingSize; //필수
    private int servings; //필수
    private int calories; //선택
    private int fat;  //선택
    private int sodium; //선택

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

NutritionFacts nutritionFacts = new NutritionFacts();
        nutritionFacts.setServingSize(240);
        nutritionFacts.setServings(5);
        nutritionFacts.setCalories(100);
        nutritionFacts.setFat(0);
        nutritionFacts.setSodium(27);    

자바빈즈패턴에서는 객체 하나를 만들려면 메서드를 여러 개 호출해야 하고, 객체가 완전히 생성 되기 전까지 일관성이 무너진 상태에 놓이게 된다

이러한 문제로 대안으로 나온게 빌더 패턴(Builder pattern)이다.

class NutritionFacts{
    private final int servingSize; //필수
    private final int servings; //필수
    private final int calories; //선택
    private final int fat;  //선택
    private final int sodium; //선택


    public static class Builder{
        //필수
        private int servingSize;
        private int servings;

        //선택 - 기본값으로 초기화 해야한다
        private int calories = 0;
        private int fat = 0;
        private int sodium = 0;

        public Builder(int servingSize, int servings){
            this.servingSize = servingSize;
            this.servings = servings;
        }

        public Builder calories(int val){
        	if(val < 0) throw new IllegalArgumentException();
            calories = val;
            return this;
        }

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

        public Builder sodium(int val){
            sodium = 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;
    }
}

public class Item2 {
    NutritionFacts cocaCola = new NutritionFacts.Builder(240,8).calories(100).sodium(35).build();
}

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

또한 잘못된 매개변수를 최대한 일찍 발견하려면 빌더의 생성자와 매개변수에서 검사 할 수 있다.

빌더 패턴은 계층적으로 설계된 클래스와 함께 쓰기에 좋다

public class Item2 {

    NyPizza nyPizza = new NyPizza.Builder(NyPizza.Size.SMALL).addToping(Pizza.Topping.HAM).addToping(Pizza.Topping.ONION).build();
}

//피자
abstract class Pizza{
    public enum Topping{HAM, MUSHROOM, ONION, PEPPER}
    private final Set<Topping> toppings;

    abstract static class Builder<T extends Builder<T>>{ //Builder<T> 상속받는 class만 접근 가능한 재귀적 제너럴 타입

        //Topping이라는 Enum을 다루는 Set으로 비어있는 Set을 반환
        EnumSet<Topping> toppings = EnumSet.noneOf(Topping.class);
        public T addToping(Topping topping){
            toppings.add(topping);
            return self();
        }

        abstract Pizza build();
        //하위 클래스는 이 메서드를 재정의하여
        //this를 반환하도록 해야 한다.
        protected abstract T self();
    }

    Pizza(Builder<?> builder){
        toppings = builder.toppings;
    }
}

//뉴욕피자
class NyPizza extends Pizza {
    public enum Size {SMALL, MEDIUM, LARGE}
    private final Size size;

    public static class Builder extends Pizza.Builder<Builder>{

        private final Size size;

        public Builder(Size size){
            this.size = size;
        }

        @Override
        public NyPizza build() {
            return new NyPizza(this);
        }

        @Override
        protected Builder self() {
            return this;
        }
    }

    private NyPizza(Builder builder){
        super(builder);
        size = builder.size;
    }

}

Pizza.Builder 클래스는 재귀적 타입을 이용하는 제네릭 타입이다.
추상 메서드인 self를 추가해 하위 클래스에서는 형변환하지 않고도 메서드 연쇄를 지원 가능하다.

각 하위 클래스의 빌더가 정의한 build 메서드는 해당하는 구체 하위 클래스를 반환하도록 선언한다. NyPizza.Builder는 NyPizza를 반환한다.

※ @Override한 구현 메서드에서 반환형이 달라도 상속 관계면 가능하다는걸 알게 되었음

최근에는 Lombok 라이브러리를 통해서 @Builder를 사용했는데, 구체적인 원리를 알고 쓰는 기분이다. Lombok을 사용하여 자식클래스에서 부모클래스 멤버변수 build를 사용하려면 상속, 상속 받는 클래스에 @SuperBuilder를 사용하면 된다! 굳이 builder 클래스를 만들필요 없음

profile
짧더라도 확실한 기록

0개의 댓글