Skip to main content

빌더 패턴

디자인 패턴에서의 빌더 패턴과, Java에서의 빌더 패턴에 대해 알아보고자 한다.

디자인 패턴에서의 빌더 패턴

빌더 패턴은 복잡한 객체의 ‘구성’과 ‘표현’을 분리하여, 동일한 생성 절차가 다른 표현을 생성할 수 있도록 하는 패턴이다.
한 마디로, 복잡한 객체의 생성을 단계적이고, 관리 가능하며, 상호 교환 가능한 작업들로 분해하는 것이다.

구성 요소

디렉터(Director)

빌더 인터페이스를 사용해 객체의 생성을 조절하는 요소. 객체 생성의 순서를 명시함.

빌더 인터페이스(Builder Interface)

생성 단계에 대해 개략적으로 설명하는 요소.

콘크리트 빌더(Concrete Builder)

빌더 인터페이스를 구현하여, 각 생성 단계별 세부 사항을 제공하며 조합을 관리함.

프로덕트(Product)

생성 중인 복잡한 객체. 일반적으로 시스템은 디렉터와 프로덕트와만 접촉하고, 콘크리트 빌더의 존재는 알지못함.

Java에서의 빌더 패턴

점층적 생성자 패턴안정성자바빈즈 패턴가독성을 겸비한 패턴.
빌더 클래스는 보통 생성할 클래스 안에 정적 멤버 클래스로 만들어 두며, 이러한 빌드 패턴은 명명된 선택적 매개변수(Named Optional Parameters)를 흉내낸 것이다.

  • 명명된 선택적 매개변수(Named Optional Parameters) : 위치 매개변수(Positional Parameters) 와 달리 값이 전달 될 때, 이름도 함께 전달되어야 하는 매개변수.

사용하게 된 배경

정적 팩토리 메서드public 생성자의 공통적인 단점인 ‘선택적 매개변수(Optional Parameter)‘가 많은 경우 적절한 대응이 힘들다는 점을 대처하기 위해 여러 패턴이 만들어지게 되었다.

점층적 생성자 패턴(Telescoping Constructor Pattern)

필수 매개변수만 받는 생성자, 필수 매개변수와 선택 매개변수 1개를 받는 생성자, … 형태로 선택 매개변수를 모두 받는 생성자까지 모든 경우의 수를 충족하는 방식이다.

단점
1. 사용자가 설정하기 원치않는 매개변수까지 포함하기 쉽다.
2. 매개변수 개수가 많아지면, 코드를 작성/읽는게 힘들다.

코드를 읽을 때, 각 값이 무엇을 의미하는지, 몇 개인지 파악하기 힘들고 실수로 순서가 바뀌면 런타임에 에러가 발생할 수 있다.

자바빈즈 패턴(JavaBeans Pattern)

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

장점
1. 인스턴스를 만들기 쉽고, 가독성도 좋다.
단점
1. 객체 하나를 만들기 위해 여러 메서드 호출이 필요하다.
2. 객체가 완전히 생성되기 전에는 일관성이 무너진 상태에 놓이게 된다.

점층적 생성자 패턴에서는 매개변수들의 유효 여부만 확인하면 일관성이 유지 되었지만, 자바빈즈 패턴에서는 그렇지 않음.

  • 일관성 : 객체의 모든 프로퍼티들이 정상적으로 값들로 채워져 있는지 여부.

이러한 특성 때문에 자바빈즈 패턴에서는 불변 클래스를 만들 수 없고, 스레드 안정성을 위한 추가 작업이 수반된다.

이러한 단점을 보완하기 위해 객체를 freezing 하는 방법도 있지만, 개발자가 freeze를 호출했는지를 컴파일러가 보증할 수 없어 런타임 오류에 취약하다.

  • 객체를 freeze하면, 속성 값을 수정하거나 삭제하는 것이 불가능해 진다. #

객체 생성과정

  1. 필수 매개변수로 생성자(또는 정적 팩토리 메서드)를 호출해 빌더 객체를 얻는다.
  2. 빌더 객체가 제공하는 setter 메서들로 원하는 선택 매개변수들을 설정한다.
  3. 매개변수가 없는 build 메서드를 호출해 (웬만하면 불변인)객체를 얻는다.

빌더의 setter 메서드들은 빌더 자신을 반환해, 연쇄적으로 호출하는 플루언트API(또는 메서드 연쇄)의 형태를 띈다.

  • 플루언트 API(Fluent API) / 메서드 연쇄(Method Chaining) : 메서드 호출이 흐르듯 연결되는 형태. 소스 코드의 가독성산문과 유사하게 만드는 것이 목적이다.

유효성 검사

빌더 패턴을 사용할 땐 생성자 내에서, 그리고 build 메서드 내에서도 유효성 검사가 필요하다.

  • 빌더 클래스는 생성할 클래스 내에 멤버 클래스로 존재하므로 클래스의 private 생성자에 접근할 수 있고, build 메서드는 보통 private 생성자를 호출하므로 해당 private 생성자 내에서 객체가 유효한 상태를 갖고 생성되는지 확인해야 한다.
  • 빌더는 필드 값들이 복사 될 때 변형될 수 있어, 빌더의 필드가 아닌 defensive copy를 생성해 이 새로운 객체의 필드 값을 검증 해야한다. #
    • 방어적 복사(Defensive Copy) : 가변적인 객체(Mutable Object)의 단점인 검사시점/사용시점 공격(TOC/TOU) 의 위험을 방지하기 위해, 직접 필드 값을 반환하지 않고 매 번 생성자를 활용해 필드 값을 새로운 객체로 반환하는 것. #

빌더와 계층적으로 설계된 클래스

빌더 패턴은 계층적으로 설계된 클래스와 조합이 좋다.
각 계층의 클래스에 관한 빌더를 멤버로 정의한다.
즉, 추상 클래스추상 빌더를, 구체 클래스(Concrete Class)구체 빌더를 갖게 한다.

  • 구체 클래스(Concrete Class): 모든 메서드가 구현되어 있는, 즉 new 키워드를 통해 인스턴스를 만들 수 있는 클래스.
// 추상 클래스
public abstract class Pizza {
	...
	// 추상 빌더
    abstract static class Builder<T extends Builder<T>> {
		...
    }
    ...
}
// 구체 클래스
public class NyPizza extends Pizza {
	...
	// 구체 빌더
    public static class Builder extends Pizza.Builder<Builder> {
		...
    }
	...
}

이때, 추상 빌더 클래스는 재귀적 타입 한정(Recirsive Type Bound) 을 이용하는 제네릭 타입이다.

  • 재귀적 타입 한정(Recursive Type Bound) : 타입 매개변수가 자신을 포함하는 수식에 의해 한정되는 것.
    • ex) Builder<T extends Builder<T>>

추상 빌더에서 추상 메서드인 self를 하위 클래스에서 재정의(하위 클래스의 this 반환하도록)하게 해 상위<->하위 형변환 없이 메서드 연쇄를 할 수 있도록 한다. 이러한 방식을 시뮬레이트한 셀프 타입(Simulated Self-Type) 관용구라 한다.

// 추상 클래스
public abstract class Pizza {
	...
    abstract static class Builder<T extends Builder<T>> {
		...
        abstract **Pizza** build();
        // 하위 클래스는 이 메서드를 재정의(overriding)하여
        // "this"를 반환하도록 해야 한다.
        protected abstract T **self()**;
    }
}
// 구체 클래스
public class NyPizza extends Pizza {
	...
    public static class Builder extends Pizza.Builder<Builder> {
		...
        @Override public **NyPizza** build() {
            return new NyPizza(this);
        }
		// 재정의해 "this" 반환
        @Override protected Builder **self()** { return this; }
    }
}

또한, 하위 클래스의 빌더가 정의한 build 메서드는 해당하는 구체 하위 클래스를 반환하는, 공변반환 타이핑(Covariant Return Typing)을 이용해 형변환에 신경쓰지 않고 빌더를 사용 가능하다.

  • 공변반환 타이핑(Covariant Return Typing) : 하위 클래스 메서드가 override 대상 메서드(상위 클래스의 메서드)가 정의한 반환 타입이 아닌, 그 하위 타입을 반환하는 기능.
    • 즉, 위 코드처럼 NyPizza.BuilderNyPizza를 반환. 빌더를 이용하면 가변인수(varargs) 매개변수를 여러 개 사용 가능하다. 이는 두 가지 방법이 있다.
  1. 각각을 메서드들로 나눠서 선언한다.
  2. 다른 매개변수들로 여러 번 호출한 뒤 이를 한 필드로 모은다.

빌더 패턴의 단점

1. 객체를 만들기 전 먼저 빌더부터 만들어야 한다.

2. 성능에 민감한 경우 문제가 될 수 있다.

3. 매개변수가 4개 이상은 되야 값어치를 한다.

점층적 생성자 패턴보다 코드가 장황해지기 때문에 제대로 값어치를 하려면 매개변수가 4개 이상은 되어야 한다.

정리

API는 대게 시간이 갈수록 매개변수가 많아지는 경향을 띄므로 애초에 빌더로 시작하는 편이 나은 경우가 많다.
다시 말하자면, 처리해야 할 매개변수가 많고 특히, 개중에 Optional한 매개변수가 많다면 빌더 패턴을 사용하는게 생성자, 정적 팩토리 메서드 보다 낫다.