[Effective Java] 2. Builder 패턴

4 분 소요

생성자의 parameter가 많을때는 Builder 패턴을 고려하는것이 좋다.
특히나 선택적으로 필요한 parameter가 많을 경우 유용하다.

1. 점층적 생성자 패턴

보통 parameter가 많고 선택적인 인자가 있을 경우 아래와 같이 개발한다.
참고로 아래와 같은 패턴을 점층적 생성자 패턴(telescoping constructor pattern) 이라 한다.

public class ManyParameterClass {
    private String a;
    private String b;
    private String c;
	
    public ManyParameterClass(String a) { }
    public ManyParameterClass(String a, String b) { }
    public ManyParameterClass(String a, String b, String c) { }
}

1.1. 점층적 생성자 패턴의 단점

이렇게 paramter의 수에 맞게 점층적으로 생성자를 늘여가면 생성자의 개수가 늘어날뿐 아니라 상황에 따라 불필요한 parameter를 넘겨야 하는 경우가 생긴다.

요약하면 이 패턴은 동작상에 문제는 없지만 parameter 수가 늘어날수록 코드 작성의 양이 늘어나고, 가독성이 떨어진다.

2. 자바빈 패턴

parameter가 많을때 사용할 수 있는 또 다른 방법은 자바빈(JavaBeans) 패턴이다.
이는 빈 객체를 생성 후 setter를 이용해 필요한 파라미터 값을 채워가는 방식이다.

public class ManyParameterClass {
    private String a;
    private String b;
    private String c;
	
    public void setA(String a) { this.a = a; }
    public void setB(String b) { this.b = b; }
    public void setC(String c) { this.c = c; }
}	

ManyParameterClass instance = new ManyParameterClass();
instance.setA("a");
instance.setB("b");
instance.setC("c");

2.1. 자바빈 패턴의 단점

객체 생성과정을 한번에 끝낼 수 없기 때문에 객체 일관성이 일시적으로 깨질 수 있다.
만약 객체 일관성이 깨지는 문제가 발생할 경우에는 버그를 찾기가 매우 힘들다.

이 패턴의 또 다른 문제는 변경불가능한 class인 immutable class를 만들 수 없다.
물론 parameter를 set할때 일일이 제한을 두면 가능은 하지만 일반적으로 이렇게 구현하지는 않는다. 또한 버그의 가능성도 높다.

3. Builder 패턴

Builder 패턴은 점층적 생성자 패턴의 안전성과 자바빈 패턴의 가독성을 결합한 형태이다.

public class NutritionFacts {
    private final int servingSize;
    private final int servings;
    private final int calories;
    private final int fat;
	
    // Builder를 통해 객체 생성.
    private NutritionFacts(Builder builder) {
        this.servingSize = builder.servingSize;
        this.servings = builder.servings;
        this.calories = builder.calories;
        this.fat = builder.fat;
    }
	
    public static class Builder {
        // NutritionFacts 객체 생성을 위해 Builder도 동일한 멤버를 가짐.
        private final int servingSize;
        private final int servings;
        private final int calories;
        private final int fat;	
		
        // Builder 생성자를 통해 필수 값을 받을 수 있음.
        public Builder(int servingSize, int servings) {
            this.servingSize = servingSize;
            this.servings = servings;
        }
		
        // 각 멤버를 설정
        public Builder calories(int calories) {
            this.calories = calories;
            return this;
        }
		
        public Builder fat(int fat) {
            this.fat = fat;
            return this;
        }
		
        public NutritionFacts build() {
            return new NutritionFacts(this);
        }
    }
}

빌더의 setter 메서드는 빌더를 리턴하므로 사용 하는 곳에서는 연속적으로 호출이 가능하다.

NutritionFacts fact = 
	new NutritionFacts.Builder(1,2).calories(3).fat(4).build();

3.1. Builder의 장점

3.1.1. Parameter 검증이 가능하다.

객체 생성자와 마찬가지로 Builder 생성자나 Builder의 Setter 메서드 내에서 파라미터에 대한 불변식 검증이 가능하다.

불변식 검증은 Builder 객체에서 할 수도 있지만 Builder 객체가 실제 객체를 만들때도 할 수 있다.

불변식에 위반되는 경우 IllegalStateException을 던지도록 구현해야 한다. 그리고 Exception을 던질때 어떤 부분이 위배되었는지 알려야 한다.

3.1.2. 여러개의 args Parameter를 사용할 수 있다.

String... args 와 같은 paramter는 메서드에 하나밖에 사용할 수 없다.
즉, 생성자를 사용할 경우에도 하나밖에 사용할 수 없는데 Builder 패턴의 경우 각 parameter를 따로 받기 때문에 각 setter 메서드마다 args를 활용할 수 있다.

이건 활용방법에 따라서 다양하게 사용될 수 있겠다.
특정 paramter를 설정할때 다양한 옵션이 존재하는 경우가 있다면 이를 활용할 수 있을것 같다.

3.1.3. Builder 패턴은 유연하다.

하나의 Builder 객체로 여러 객체 생성이 가능하다. (Builder의 build()만 하면 객체 생성이 되니까.)
또한, 생성된 Builder 객체에서 특정 설정만 바꿔서 새로운 객체를 만드는 과정이 용이하다.

객체가 생성될때마다 일련번호를 부여하는 것과 같이 객체의 특정 필드를 자동으로 채우는 구현이 용이하다.

3.1.4. 좋은 추상 팩토리이다.

Builder 객체를 만들어 특정 메서드에 넘기면 해당 메서드가 객체를 생성하도록 할 수 있다. 이 때 Generic을 활용하면 아래와 같은 코딩이 가능하다.

public interface Builder<T>{
	public T build();
}

모든 Builder는 이 Interface를 implements 한다.

Tree buildTree(Builder<? extends Node> nodeBuilder) {
	...
}

Tree 객체를 만들고자 한다면 buildTree() 메서드에 Builder 객체를 넘기기만 하면 된다. 또한, Generic 특징을 사용하여 이 buildTree() 메서드는 Node를 상속하는 class의 Builder만 사용가능하도록 제한한다.

예를 들어 아래 MyNode의 Builder는 buildTree() 사용이 가능하지만 MyNode2의 Builder는 buildTree()의 parameter가 될 수 없다.

public class MyNode extends Node {
	
    public static class Builder implements Builder<MyNode> {
        public MyNode build() {
		
        }
    }
}

pulic class MyNode2 {
    public static class Builder implements Builder<MyNode2> {
        public MyNode build() {
		
        }
    }
}

3.2. Builder의 단점

객체 생성을 위해서 Builder 객체를 무조건 생성해야 한다.
실제 성능에 크게 영향을 미치는 부분은 아니지만 성능이 정말 중요한 경우는 오버헤드가 될 수 있다.

또한, Builder 코드 양이 많기 때문에 parameter가 정말 많은 경우에만 유리하다.

정리하면 Builder 패턴은 parameter가 많은 생성자나 정적 팩토리가 필요한 클래스를 설계할때, 특히 대부분의 parameter가 선택적인 상황에서 유리하다.

<참고> 추상 팩토리의 다른 예

자바에서 추상 팩토리의 다른 예로 Class 객체가 있다.
Class.newInstance()가 객체의 build 역할을 한다.

이 경우 여러문제가 있는데, 우선 newInstance()는 항상 parameter가 없는 생성자를 호출한다는 것이다.
하지만 모든 Class가 default생성자를 가지고 있는것은 아니다. 보통 이런경우 컴파일 에러가 발생해야 하지만 Class.newInstance()를 사용하면 컴파일 에러가 발생하지 않는다.

대신 런타임에 InstantiationException 이나 IllegalAccessException을 발생시킨다.
문제는 newInstance()는 이 Exception을 throw하는 부분이 없기 때문에 개발 당시 놓치기 슆고, 개발 입장에서는 런타임에 발생하는 이러한 Exception을 처리해줘야 하는것도 귀찮은 문제이다.

댓글남기기