[15] 디자인 패턴 목록 - 생성 디자인 패턴 - 빌더 패턴(Builder Pattern)
출처 : 디자인 패턴에 뛰어들기 - 알렉산더 슈베츠 도서
빌더 패턴
복잡한 객체들을 단계별로 생성할 수 있도록 하는 생성 디자인 패턴이다.
이 패턴을 사용하면 같은 제작 코드를 사용하여 객체의 다양한 유형들과 표현을 제작할 수 있다.
문제
많은 필드와 중첩된 객체들을 초기화해야 하는 복잡한 객체가 있을 때, 초기화 코드는 보통 많은 매개변수가 있는 생성자 내부에 있거나, 클라이언트 코드 전체에 흩어져 있을 수 있다.
객체의 모든 설정에 자식 클래스를 만들어 프로그램을 복잡하게 만들 수 있다.
예를 들어, House(집) 객체를 만들 때 간단한 집부터 시작해 더 크고 현대적인 집을 원할 때, 모든 매개변수를 포함하는 자식 클래스들을 만드는 방식은 계층구조를 매우 복잡하게 만든다.
또 다른 접근 방식은 모든 가능한 매개변수를 포함한 거대한 생성자를 만드는 것이지만, 이는 매개변수가 많아 사용하기 어렵고, 대부분의 매개변수가 사용되지 않아 코드가 지저분해질 우려가 있다.
매개변수가 많은 생성자의 단점은 모든 매개변수가 항상 필요한 것은 아니라는 점을 알아야 한다.
해결책
빌더 패턴은 객체 생성 코드를 별도의 빌더 객체로 이동시킨다.
빌더 패턴은 복잡한 객체들을 단계별로 생성할 수 있도록 한다.
이 패턴은 객체 생성을 일련의 단계들로 나누며, 필요한 단계들만 호출하여 객체를 생성한다.
특정 설정을 위해서는 건축 단계들을 다른 방식으로 구현하는 여러 빌더 클래스를 생성할 수 있다.
다양한 빌더들은 다양한 방식으로 같은 작업을 실행한다.
예를 들어, 나무와 유리로 건축하는 건축가, 돌과 철로 건축하는 건축가, 금과 다이아몬드로 건축하는 건축가가 있다고 가정할 때, 같은 단계들을 호출하면 각각 다른 결과물을 얻을 수 있다.
디렉터 (관리자)
디렉터는 제품을 생성하는 데 사용하는 빌더 단계들에 대한 호출을 관리하는 별도의 클래스이다.
디렉터는 생성 단계들의 순서를 정의하며, 빌더는 단계들의 구현을 제공한다.
디렉터는 제품을 얻기 위해 어떤 단계들을 실행해야 하는지 알고 있다.
프로그램에 디렉터 클래스를 포함하는 것은 필수는 아니다.
클라이언트 코드에서 직접 생성 단계를 호출할 수 있으나, 디렉터 클래스는 다양한 생성 루틴들을 재사용할 수 있게 해준다.
디렉터 클래스는 클라이언트 코드에서 제품 생성의 세부 정보를 완전히 숨긴다.
클라이언트는 빌더를 디렉터와 연관시키고, 디렉터를 통해 생성을 실행한 후, 빌더로부터 결과를 얻기만 하면 된다.
구조
1. 빌더 인터페이스
- 모든 유형의 빌더에 공통적인 제품 생성 단계들을 선언합니다.
- 모든 유형의 빌더에 공통적인 제품 생성 단계들을 선언합니다.
2. 구상 빌더
- 생성 단계들의 다양한 구현을 제공합니다. 공통 인터페이스를 따르지 않는 제품도 생산할 수 있습니다.
- 생성 단계들의 다양한 구현을 제공합니다. 공통 인터페이스를 따르지 않는 제품도 생산할 수 있습니다.
3. 제품
- 빌더에 의해 생성된 결과 객체입니다. 다른 빌더들에 의해 생성된 제품들은 같은 클래스 계층구조나 인터페이스에 속할 필요가 없습니다.
- 빌더에 의해 생성된 결과 객체입니다. 다른 빌더들에 의해 생성된 제품들은 같은 클래스 계층구조나 인터페이스에 속할 필요가 없습니다.
4. 디렉터 클래스
- 생성 단계들을 호출하는 순서를 정의하여 특정 설정을 만들고 재사용할 수 있습니다.
- 생성 단계들을 호출하는 순서를 정의하여 특정 설정을 만들고 재사용할 수 있습니다.
5. 클라이언트
- 빌더 객체를 디렉터와 연결해야 합니다.
- 일반적으로 디렉터 생성자의 매개변수들을 통해 한 번만 수행된다.
- 이후 디렉터는 모든 추가 생성에 해당 빌더 객체들을 사용한다.
- 대안으로, 클라이언트가 빌더 객체를 디렉터의 프로덕션 메서드에 전달할 수도 있다.
- 이 경우 디렉터와 함께 무언가를 만들 때마다 다른 빌더를 사용할 수 있습니다.
- 이 경우 디렉터와 함께 무언가를 만들 때마다 다른 빌더를 사용할 수 있습니다.
의사코드
아래 의사코드 예시는 자동차와 같은 다양한 제품들을 생성할 때 동일한 생성 코드를 재사용한다.
해당 제품들에 맞는 사용자 설명서를 만드는 방법을 보여준다.
자동차들의 단계별 생성과 해당 자동차 모델들에 맞는 사용자 설명서들의 예시
자동차는 복잡한 객체로 수백 가지 방식으로 생성될 수 있다.
자동차 빌더 클래스는 자동차 부품을 설정하기 위한 메서드들을 제공하여 이런 복잡성을 다루고 있다.
클라이언트 코드는 특정 자동차 모델을 직접 조립하거나, 디렉터 클래스에 조립을 위임할 수 있다.
디렉터는 다양한 자동차 모델을 빌더를 통해 조립하는 방법을 알고 있다.
- 모든 자동차에는 사용설명서가 필요하다.
- 각 모델에 맞는 사용자 설명서를 생성하기 위해 기존 제작 프로세스를 재사용한다.
- 사용설명서 작성은 자동차 제작과는 다른 프로세스이므로 설명서 전용 빌더 클래스를 사용한다.
- 이 클래스는 자동차 부품을 설명하는 제작 메서드를 구현하여 사용된다.
생성된 자동차 객체와 해당 사용설명서는 서로 다른 객체이다.
디렉터는 이 둘을 구분하여 생성된 결과를 클라이언트에 제공한다.
// 빌더 패턴을 사용하는 것은 제품에 매우 복잡하고 광범위한 설정이
// 필요한 경우에만 의미가 있습니다.
// 다음 두 제품은 공통 인터페이스는 없지만 관련되어 있습니다.
class Car is
// 자동차에는 GPS, 트립 컴퓨터 및 몇 개의 좌석이 있을 수 있습니다.
// 다른 모델의 자동차(스포츠카, SUV, 오픈카)에는 다른 기능들이
// 설치되거나 활성화되어 있을 수 있습니다.
class Manual is
// 각 자동차에는 자동차의 설정에 해당하는, 모든 기능을 설명하는
// 사용 설명서가 있어야 합니다.
// 빌더 인터페이스는 제품 객체들의 다른 부분들을 만드는 메서드들을 지정합니다.
interface Builder is
method reset()
method setSeats(...)
method setEngine(...)
method setTripComputer(...)
method setGPS(...)
// 구상 빌더 클래스들은 빌더 인터페이스를 따르고 빌드 단계들의 특정 구현들을 제공합니다.
// 당신의 프로그램에는 각기 다르게 구현된 여러 가지 빌더 변형들이 있을 수 있습니다.
class CarBuilder implements Builder is
private field car:Car
// 새로운 빌더 인스턴스에는 인스턴스가 추가적인 조립과정에서
// 사용하는 빈 제품 객체가 포함되어야 합니다.
constructor CarBuilder() is
this.reset()
// reset 메서드는 구축 중인 객체를 지웁니다.
method reset() is
this.car = new Car()
// 모든 생성 단계들은 같은 제품의 인스턴스와 작동합니다.
method setSeats(...) is
// 차량의 좌석 수를 설정하세요.
method setEngine(...) is
// 해당 엔진을 설치하세요.
method setTripComputer(...) is
// 트립 컴퓨터를 설치하세요.
‘method setGPS(...) is
// GPS를 설치하세요.
// 구상 빌더들은 결과들을 가져오기 위한 자체 메서드들을 제공해야 합니다.
// 왜냐하면 다양한 유형의 빌더들은 모두 같은 인터페이스를 따르지 않는 완전히
// 다른 제품들을 생성할 수 있기 때문입니다.
// 따라서 이러한 메서드는 빌더 인터페이스에서 선언할 수 없습니다.
// 적어도 이는 정적 타입 언어에서는 불가능합니다.
// 최종 결과를 클라이언트에 반환한 후 일반적으로 빌더 인스턴스는
// 다른 제품 생산을 시작할 준비가 되어 있을 것이라고 예상됩니다.
// 이것이 `getProduct` 메서드의 본문 끝에서 reset 메서드를
// 호출하는 것이 일반적인 관행인 이유입니다.
// 하지만 반드시 이렇게 해야 하는 것은 아니라서,
// 빌더가 클라이언트 코드로부터 명시적으로 reset 호출을 받을 때까지
// 이전 결과를 삭제하지 않고 기다리게 만들 수 있습니다.
method getProduct():Car is
product = this.car
this.reset()
return product
// 다른 생성 패턴과 달리 빌더를 사용하면 공통 인터페이스를
// 따르지 않는 제품들을 생성할 수 있습니다.
class CarManualBuilder implements Builder is
private field manual:Manual
constructor CarManualBuilder() is
this.reset()
method reset() is
this.manual = new Manual()
method setSeats(...) is
// 자동차 좌석의 기능들을 문서화하세요.
method setEngine(...) is
// 엔진 사용 지침을 추가하세요.
method setTripComputer(...) is
// 트립 컴퓨터 사용 지침을 추가하세요.
method setGPS(...) is
// GPS 사용 지침을 추가하세요.
method getProduct():Manual is
// 매뉴얼을 반환하고 빌더를 초기화하세요.
// 디렉터는 특정 순서로 생성 단계들을 실행하는 책임만 있습니다.
// 이것은 특정 순서나 설정에 따라 제품들을 생성할 때 유용합니다.
// 엄밀히 말하면, 클라이언트가 빌더들을 직접 제어할 수 있으므로
// 디렉터 클래스는 선택 사항입니다.
class Director is
// 디렉터는 클라이언트 코드가 전달하는 모든 빌더 인스턴스와 함께 작동합니다.
// 그러면 클라이언트 코드는 새로 조립된 제품의 최종 유형을 변경할 수 있습니다.
// 디렉터는 같은 생성 단계들을 사용하여 여러 제품 변형들을 생성할 수 있습니다.
method constructSportsCar(builder: Builder) is
builder.reset()
builder.setSeats(2)
builder.setEngine(new SportEngine())
builder.setTripComputer(true)
builder.setGPS(true)
method constructSUV(builder: Builder) is
// …
// 클라이언트 코드는 빌더 객체를 만든 후 이를 디렉터에게 전달한 다음 생성 프로세스를 시작합니다.
// 최종 결과는 빌더 객체에서 가져옵니다.
class Application is
method makeCar() is
director = new Director()
CarBuilder builder = new CarBuilder()
director.constructSportsCar(builder)
Car car = builder.getProduct()
CarManualBuilder builder = new CarManualBuilder()
director.constructSportsCar(builder)
// 디렉터는 구상 빌더들 및 제품들에 의존하지 않고 인식하지 못하기 때문에
// 최종 제품은 종종 빌더 객체에서 가져옵니다.
Manual manual = builder.getProduct()
적용
'점층적 생성자' 문제 해결
‘점층적 생성자'를 제거하기 위하여 빌더 패턴을 사용하세요.
복잡한 생성자를 사용해야 하는 경우, 빌더 패턴을 사용하여 이를 해결할 수 있다.
점층적 생성자는 많은 선택적 매개변수를 처리하기 어렵고, 여러 버전의 생성자를 만들어야 한다.
// 이렇게 복잡한 생성자를 만드는 것은 C#나 자바와 같이 메서드 오버로딩을 지원하는 언어들에서만 가능합니다.
// 복잡한 생성자를 만드는 예시 (C# 또는 자바)
class Pizza {
Pizza(int size) { ... }
Pizza(int size, boolean cheese) { ... }
Pizza(int size, boolean cheese, boolean pepperoni) { ... }
// …
빌더 패턴을 사용하면 필요한 단계만을 사용하여 객체를 단계별로 생성할 수 있다.
생성자에 많은 매개변수를 전달할 필요가 없다.
제품의 다양한 표현 생성
빌더 패턴은 당신의 코드가 일부 제품의 다른 표현들(예: 석조 및 목조 주택들)을 생성할 수 있도록 하고 싶을 때 사용하세요.
빌더 패턴은 제품의 다양한 표현을 생성할 때 유용하다.
- 예를 들어, 석조 및 목조 주택의 생성 과정은 비슷하지만 세부 사항이 다르다.
- 빌더 인터페이스는 모든 생성 단계를 정의하고, 구상 빌더는 단계들을 구현하여 다양한 표현을 생성한다.
- 디렉터 클래스는 생성 순서를 안내한다.
복합 객체 생성
빌더를 사용하여 복합체 트리들 또는 기타 복잡한 객체들을 생성하세요.
빌더 패턴은 복잡한 객체, 특히 복합체 트리의 생성에 적합하다.
단계별로 제품을 생성하며, 일부 단계의 실행을 연기할 수 있다.
재귀적으로 단계들을 호출하여 객체 트리를 구축할 수 있다.
빌더는 미완성 제품을 노출하지 않아, 클라이언트 코드가 불완전한 결과를 가져오는 것을 방지한다.
구현방법
1. 생성 단계 정의
- 모든 제품 표현을 생성할 수 있는 공통 생성 단계를 정의한다.
- 이를 명확하게 정의할 수 없는 경우 패턴 구현을 중단해야 한다.
2. 기초 빌더 인터페이스 선언
- 기초 빌더 인터페이스에서 이 단계를 선언한다.
- 기초 빌더 인터페이스에서 이 단계를 선언한다.
3. 구상 빌더 클래스 구현
- 각 제품 표현에 대해 구상 빌더 클래스를 만들고, 생성 단계를 구현한다.
- 생성 결과를 반환하는 메서드도 구현한다.
- 다양한 제품을 생성하는 경우, 이 메서드는 빌더 인터페이스에 선언되지 않을 수 있다.
4. 디렉터 클래스 고려
- 디렉터 클래스를 만들어 같은 빌더 객체를 사용해 제품을 제작하는 다양한 방법을 캡슐화한다.
- 디렉터 클래스를 만들어 같은 빌더 객체를 사용해 제품을 제작하는 다양한 방법을 캡슐화한다.
5. 클라이언트 코드 작성
- 클라이언트 코드는 빌더와 디렉터 객체를 생성하고, 빌더 객체를 디렉터에게 전달한다.
- 이는 디렉터의 생성자 매개변수를 통해 이루어지며, 이후 디렉터는 빌더 객체를 사용해 제품을 제작한다.
6. 생성 결과 반환
- 모든 제품이 같은 인터페이스를 따를 경우 디렉터로부터 직접 결과를 얻는다.
- 그렇지 않으면 클라이언트가 빌더에서 결과를 가져와야 한다.
장단점
장점
- 객체를 단계별로 생성하거나 생성 단계를 연기, 재귀적으로 실행할 수 있습니다.
- 다양한 표현의 제품을 만들 때 동일한 생성 코드를 재사용할 수 있습니다.
- 단일 책임 원칙을 준수하여 제품의 비즈니스 로직에서 복잡한 생성 코드를 분리합니다.
단점
- 여러 개의 새 클래스를 생성해야 하므로 코드의 전반적인 복잡성이 증가합니다.
다른 패턴과의 관계
팩토리 메서드와의 관계
- 단순한 팩토리 메서드에서 시작해 추상 팩토리, 프로토타입 또는 빌더 패턴으로 발전할 수 있다.
- 단순한 팩토리 메서드에서 시작해 추상 팩토리, 프로토타입 또는 빌더 패턴으로 발전할 수 있다.
추상 팩토리와의 차이
- 빌더는 복잡한 객체를 단계별로 생성하며, 추상 팩토리는 관련 객체들의 패밀리를 생성한다.
- 빌더는 복잡한 객체를 단계별로 생성하며, 추상 팩토리는 관련 객체들의 패밀리를 생성한다.
복합체 패턴
- 빌더는 생성 단계를 재귀적으로 작동하도록 프로그래밍할 수 있어 복잡한 복합체 트리를 생성할 때 유용하다.
- 빌더는 생성 단계를 재귀적으로 작동하도록 프로그래밍할 수 있어 복잡한 복합체 트리를 생성할 때 유용하다.
브리지 패턴과의 조합
- 디렉터는 추상화 역할을 한다.
- 다양한 빌더들은 구현 역할을 한다.
싱글턴 패턴
- 추상 팩토리, 빌더 및 프로토타입은 모두 싱글턴으로 구현될 수 있다.