java

클래스 빌더 패턴

nownow 2025. 3. 4. 11:35

클래스의 생성자, 정적 팩터리 패턴에서는 선택적 매개변수가 많을 때 적용하기 불리한 점이 있다

영양성분을 클래스를 표현한다고 가정하면. 여러 성분 중 해당 식품에 필요한 정보만 선택적으로 입력해야 한다.

 

class 영양성분{
	영양성분(탄수화물,단백질,지방){};
	영양성분(탄수화물,단백질,지방,칼로리){};
	영양성분(탄수화물,단백질,지방,칼로리,비타민A,비타민B){};
}

이러한 방식으로 모든 상황을 가정하여 생성자를 지정할 수도 있지만(점층적 생성자 패턴)

매개변수가 너무 많아진다면 활용하기 어려워진다.

class 영양성분{
	int vitamina;
	int vitaminb;
	영양성분(탄수화물,단백질,지방){};
	public setvitaminA(int num){
	vitamina=num;
	}
	public setvitaminB(int num){
	vitaminb=num;
	}
}

main(){
	영양성분 snack = new 영양성분(1,2,3);
	snack.setvitaminA(4);
	// 여기서 문제
	snack.setvitaminB(5);
}

그런 상황에 대응하기 위해 set메서드를 통해 필드 데이터를 설정해주면(자바빈즈 패턴)

각 필드를 설정하는 도중에 일관성이 깨지게 되고 스레드 안정성을 위해 부가 처리가 필요하다.

 

 

이런 문제를 클래스 내부에 정적 빌더 객체를 추가함으로서 해결할 수 있다.

아래는 계층적 빌더 패턴의 예시다.

public abstract class Coffee {
    public enum topping {SUGAR,SYRUP,CREAM,CINNAMON};
    final Set<topping> toppings;
    static public abstract class Builder<T extends Builder<T>>{
        EnumSet<topping> toppingEnumSet = EnumSet.noneOf(topping.class);
        public T addTopping(topping topping){
            toppingEnumSet.add(topping);
            return self();
        }
        abstract Coffee build();
        abstract T self();
    }
    Coffee(Builder<?> build){
        toppings=build.toppingEnumSet.clone();
    }
}

Coffee는 추상 클래스로 구현될 하위 클래스가 존재한다.

메서드체이닝을 활용할 수 있도록 재귀적 제네릭을 사용한다. (Builder<T extends Builder<T>>)

하위 클래스의 공통 기능이기에 상위 클래스에 구현해둔 addTopping 메서드에서 Builder 클래스를 반환한다면

하위클래스를 생성할 때 메서드체이닝 활용이 불가능하다.

 

그렇기에 타입 매개변수 제한으로 Builder<T> 클래스를 상한으로 두고

하위클래스를 반환할 수 있도록 구현한다.

 

생성자에서 참조형 객체인 EnumSet을 .clone() 하고 있다.

Coffee.Builder은 가변형 자료구조인 EnumSet을 사용하고 있기에 그를 = 연산자로 참조하게 되면

static class인 Builder를 사용하는 다른 객체에 의해서 EnumSet이 사용되면 불변성을 유지할 수 없기에

.clone() 메서드를 통해 새로운 EnumSet을 복사 생성하여 관리한다.

.clone() 메서드는 참조형 자료구조의 내부 요소를 얕은복사 하기에 다른 Set 자료구조라면 문제가 생길 수 있지만

enum은 하나의 인스턴스만 존재하기에 얕은 복사로도 불변을 유지할 수 있다.

public class Latte extends Coffee{
    public enum milkEnum {NORMAL, VEGAN};
    final milkEnum milk;
    static public class Builder extends Coffee.Builder<Builder>{
        private final milkEnum milk;
        public Builder(milkEnum milk){
            this.milk=milk;
        }
        @Override
        Latte build(){
            return new Latte(this);
        }
        @Override
        Builder self() {
            return this;
        }
    }
    Latte(Builder builder){
        super(builder);
        milk=builder.milk;
    }
}

Coffee를 상속한 클래스에서의 Builder는 extends 부분에 상위 클래스의 타입 매개변수를 지정해준다.

추상 메서드였던 self()를 구현하고 있는데, 이도 메서드체이닝을 위한 구현으로

상위 클래스에서 addToppings 후 self()를 반환하고 있는데, 하위클래스의 self() 구현에서 자신을 return 하게 함으로서

공통 상위 메서드에서 구체화 된 클래스를 return 하며 메서드 체이닝이 작동할 수 있도록 한다.

 

모든 작업을 끝낸 뒤 build() 메서드를 통해 필요한 필드 값을 지정한 인스턴스를 반환할 수 있다.