빌더 패턴
의도
빌더는 복잡한 객체들을 단계별로 생성할 수 있도록 하는 생성 디자인 패턴입니다. 이 패턴을 사용하면 같은 제작 코드를 사용하여 객체의 다양한 유형들과 표현을 제작할 수 있습니다.
문제
많은 필드와 중첩된 객체들을 힘들게 단계별로 초기화해야 하는 복잡한 객체를 상상해 보세요. 이러한 초기화 코드는 일반적으로 많은 매개변수가 있는 괴물 같은 생성자 내부에 묻혀 있습니다. 또, 더 최악의 상황에는 클라이언트 코드 전체에 흩어져 있을 수도 있습니다.
예를 들어 House
(집) 객체를 만드는 방법에 대해 생각해 봅시다. 간단한 집을 지으려면 네 개의 벽과 바닥을 만든 후 문도 설치하고 한 쌍의 창문도 맞춘 후 지붕도 만들어야 합니다. 하지만 뒤뜰과 기타 물품(난방 시스템, 배관 및 전기 배선 등)이 있는 더 크고 현대적인 집을 원하면 어떻게 해야 할까요?
위 문제의 가장 간단한 해결책은 기초 House
클래스를 확장하고 매개변수의 모든 조합을 포함하는 자식 클래스들의 집합을 만드는 것입니다. 그러나 당신은 결국 상당한 수의 자식 클래스를 만들게 될 것입니다. 새로운 매개변수(예: 현관 스타일)를 추가할 때마다 이 계층구조는 훨씬 더 복잡해질 것입니다.
자식 클래스들을 늘리지 않는 다른 접근 방식이 있습니다. 기초 House
클래스에 House
객체를 제어하는 모든 가능한 매개변수를 포함한 거대한 생성자를 만드는 것입니다. 이 접근 방식은 실제로 자식 클래스들의 필요성을 제거하나, 다른 문제를 만들어 냅니다.
보통 대부분의 매개변수가 사용되지 않아 생성자 호출들의 코드가 매우 못생겨질 것입니다. 예를 들어, 극소수의 집들에만 수영장이 있으므로 수영장과 관련된 매개변수들은 십중팔구 사용되지 않을 것입니다.
해결책
빌더 패턴은 자신의 클래스에서 객체 생성 코드를 추출하여 builders(건축업자들)라는 별도의 객체들로 이동하도록 제안합니다.
이 패턴은 객체 생성을 일련의 단계들(buildWalls
(벽 건설), buildDoor
(문 건설) 등)로 정리하며, 객체를 생성하고 싶으면 위 단계들을 builder(빌더) 객체에 실행하면 됩니다. 또 중요한 점은 모든 단계를 호출할 필요가 없다는 것으로, 객체의 특정 설정을 제작하는 데 필요한 단계들만 호출하면 됩니다.
일부 건축 단계들은 제품의 다양한 표현을 건축해야 하는 경우 다른 구현들이 필요할 수 있습니다. 예를 들어, 오두막의 벽은 나무로 지을 수 있지만 성벽은 돌로 지어야 합니다.
이런 경우 같은 건축 단계들의 집합을 다른 방식으로 구현하는 여러 다른 빌더 클래스를 생성할 수 있으며, 그런 다음 건축 프로세스(즉, 건축 단계에 대한 순서화된 호출들의 집합)내에서 이러한 빌더들을 사용하여 다양한 종류의 객체를 생성할 수 있습니다.
예를 들어, 나무와 유리로 모든 것을 건축하는 건축가, 돌과 철로 모든 것을 건축하는 두 번째 건축가, 금과 다이아몬드로 모든 것을 건축하는 세 번째 건축가가 있다고 상상해 보세요. 이 세 건축가에 대해 같은 단계들의 집합을 호출하면 첫 번째 건축업자에게서부터는 일반 주택을, 두 번째 건축업자에게서부터는 작은 성을, 세 번째 건축업자에게서부터는 궁전을 얻습니다. 그러나 위에 예시된 경우는 건축 단계들을 호출하는 클라이언트 코드가 공통 인터페이스를 사용하여 빌더들과 상호 작용할 수 있는 경우에만 작동합니다.
디렉터 (관리자)
더 나아가 제품을 생성하는 데 사용하는 빌더 단계들에 대한 일련의 호출을 디렉터 (관리자)라는 별도의 클래스로 추출할 수 있습니다. 디렉터 클래스는 제작 단계들을 실행하는 순서를 정의하는 반면 빌더는 이러한 단계들에 대한 구현을 제공합니다.
프로그램에 디렉터 클래스를 포함하는 것은 필수사항은 아닙니다. 당신은 언제든지 클라이언트 코드에서 생성 단계들을 직접 특정 순서로 호출할 수 있습니다. 그러나 디렉터 클래스는 다양한 생성 루틴들을 배치하여 프로그램 전체에서 재사용할 수 있는 좋은 장소가 될 수 있습니다.
또한 디렉터 클래스는 클라이언트 코드에서 제품 생성의 세부 정보를 완전히 숨깁니다. 클라이언트는 빌더를 디렉터와 연관시키고 디렉터와 생성을 시행한 후 빌더로부터 결과를 얻기만 하면 됩니다.
구조
-
빌더 인터페이스는 모든 유형의 빌더들에 공통적인 제품 생성 단계들을 선언합니다.
-
구상 빌더들은 생성 단계들의 다양한 구현을 제공합니다. 또 구상 빌더들은 공통 인터페이스를 따르지 않는 제품들도 생산할 수 있습니다.
-
제품들은 그 결과로 나온 객체들입니다. 다른 빌더들에 의해 생성된 제품들은 같은 클래스 계층구조 또는 인터페이스에 속할 필요가 없습니다.
-
디렉터 클래스는 생성 단계들을 호출하는 순서를 정의하므로 제품들의 특정 설정을 만들고 재사용할 수 있습니다.
-
클라이언트는 빌더 객체들 중 하나를 디렉터와 연결해야 합니다. 일반적으로 위 연결은 디렉터 생성자의 매개변수들을 통해 한 번만 수행되며, 그 후 디렉터는 모든 추가 생성에 해당 빌더 객체들을 사용합니다. 그러나 클라이언트가 빌더 객체를 디렉터의 프로덕션 메서드에 전달할 때를 위한 대안적 접근 방식이 있습니다. 이 경우 디렉터와 함께 무언가를 만들 때마다 다른 빌더를 사용할 수 있습니다.
의사코드
아래 빌더 패턴 예시는 자동차와 같은 다양한 유형의 제품들을 생성할 때 동일한 객체 생성 코드를 재사용하고 그에 해당하는 매뉴얼을 만드는 방법을 보여줍니다.
자동차는 수백 가지 방법으로 생성될 수 있는 복잡한 객체입니다. 거대한 생성자로 Car
(자동차) 클래스를 부풀리는 대신, 우리는 자동차 조립 코드를 별도의 자동차 빌더 클래스로 추출했습니다. 이 클래스에는 자동차의 다양한 부분들을 설정하기 위한 메서드들의 집합이 있습니다.
클라이언트 코드가 미세하게 조정된 특별한 자동차 모델을 조립해야 하는 경우 빌더와 직접 작동할 수 있습니다. 반면에 클라이언트는 조립을 디렉터 클래스로 위임할 수 있으며, 이 디렉터 클래스는 빌더를 사용하여 가장 인기 있는 자동차 모델들을 조립하는 방법들을 알고 있습니다.
모든 자동차에는 사용설명서가 필요합니다. 사용설명서에는 해당 자동차의 모든 기능이 설명되어 있으므로 설명서의 세부 사항은 모델마다 다릅니다. 그러므로 실제 자동차들과 그들의 해당 사용설명서들 모두에 기존 제작 프로세스들을 재사용하는 것이 합리적입니다. 물론 사용설명서를 만드는 것은 자동차를 만드는 것과 같지 않기 때문에 사용설명서 작성을 전문으로 하는 또 다른 빌더 클래스를 제공해야 합니다. 이 클래스는 자동차 제작 관련 형제 클래스와 같은 제작 메서드들을 구현하지만, 자동차 부품들을 제작하는 대신 설명합니다. 이러한 빌더들을 같은 디렉터 객체에 전달하여 자동차 또는 설명서를 제작할 수 있습니다.
마지막 부분은 결과 객체를 가져오는 것입니다. 금속으로 만들어진 자동차와 종이 사용설명서는 관련은 있지만 여전히 매우 다른 사물들입니다. 디렉터를 구상 제품 클래스들에 결합하지 않고 디렉터에 결과를 가져오는 메서드를 배치할 수는 없습니다. 따라서 우리는 작업을 수행한 빌더로부터 생성의 결과를 얻습니다.
적용
'점층적 생성자'를 제거하기 위하여 빌더 패턴을 사용하세요.
10개의 선택적 매개변수가 있는 생성자가 있다고 가정합니다. 이렇게 복잡한 생성자를 호출하는 것은 매우 불편합니다. 따라서 이 생성자를 오버로드하고 더 적은 수의 매개변수들을 사용하는 더 짧은 생성자 버전들을 여러 개 만듭니다. 이러한 생성자들은 여전히 주 생성자를 참조하며, 생략된 매개변수들에 일부 기본값들을 전달합니다.
빌더 패턴을 사용하면 실제로 필요한 단계들만 사용하여 단계별로 객체들을 생성할 수 있으며, 패턴을 구현한 후에는 더 이상 수십 개의 매개변수를 생성자에 집어넣을 필요가 없습니다.
빌더 패턴은 당신의 코드가 일부 제품의 다른 표현들(예: 석조 및 목조 주택들)을 생성할 수 있도록 하고 싶을 때 사용하세요.
빌더 패턴은 제품의 다양한 표현의 생성 과정이 세부 사항만 다른 유사한 단계를 포함할 때 적용할 수 있습니다.
기초 빌더 인터페이스는 가능한 모든 생성 단계들을 정의하고 구상 빌더들은 이러한 단계들을 구현하여 제품의 여러 표현을 생성합니다. 또 한편 디렉터 클래스는 건설 순서를 안내합니다.
빌더를 사용하여 복합체 트리들 또는 기타 복잡한 객체들을 생성하세요.
빌더 패턴을 사용하면 제품들을 단계별로 생성할 수 있으며, 또 최종 제품을 손상하지 않고 일부 단계들의 실행을 연기할 수 있습니다. 그리고 재귀적으로 단계들을 호출할 수도 있는데, 이는 객체 트리를 구축해야 할 때 매우 유용합니다.
빌더는 생성 단계들을 수행하는 동안 미완성 제품을 노출하지 않으며, 이는 클라이언트 코드가 불완전한 결과를 가져오는 것을 방지합니다.
구현방법
-
사용할 수 있는 모든 제품 표현을 생성하기 위한 공통 생성 단계들을 명확하게 정의할 수 있는지 확인하세요. 그렇게 하지 못하면, 패턴 구현을 진행할 수 없습니다.
-
기초 빌더 인터페이스에서 이 단계를 선언하세요.
-
각 제품 표현에 대한 구상 빌더 클래스를 만들고 해당 생성 단계들을 구현하세요.
생성 결과를 가져오는 메서드를 구현하는 것을 잊지 마세요. 빌더 인터페이스 내에서 이 메서드를 선언할 수 없는 이유는 다양한 빌더들이 공통 인터페이스가 없는 제품들을 생성할 수 있기 때문입니다. 따라서 이러한 메서드의 반환 유형이 무엇인지 알 수 없습니다. 그러나 단일 계층구조의 제품들을 처리하는 경우 생성 결과를 가져오는 메서드를 기초 인터페이스에 안전하게 추가할 수 있습니다.
-
디렉터 클래스를 만드는 것에 대해 생각해보세요. 같은 빌더 객체를 사용하여 제품을 제작하는 다양한 방법을 캡슐화할 수 있습니다.
-
클라이언트 코드는 빌더 객체들과 디렉터 객체들을 모두 생성합니다. 제작이 시작되기 전에 클라이언트는 빌더 객체를 디렉터에게 전달해야 합니다. 일반적으로 클라이언트는 디렉터의 클래스 생성자의 매개변수들을 통해 위 작업을 한 번만 수행합니다. 그 후 디렉터는 모든 추가 제작에서 빌더 객체를 사용합니다. 또 대안적인 접근 방식이 있는데, 이 방식은 빌더가 디렉터의 특정 제품 제작 메서드에 전달되는 것입니다.
-
모든 제품이 같은 인터페이스를 따르는 경우에만 디렉터로부터 직접 생성 결과를 얻을 수 있습니다. 그렇지 않으면 클라이언트는 빌더에서 결과를 가져와야 합니다.
장단점
- 객체들을 단계별로 생성하거나 생성 단계들을 연기하거나 재귀적으로 단계들을 실행할 수 있습니다.
- 제품들의 다양한 표현을 만들 때 같은 생성 코드를 재사용할 수 있습니다.
- 단일 책임 원칙. 제품의 비즈니스 로직에서 복잡한 생성 코드를 고립시킬 수 있습니다.
- 패턴이 여러 개의 새 클래스들을 생성해야 하므로 코드의 전반적인 복잡성이 증가합니다.
다른 패턴과의 관계
-
많은 디자인은 복잡성이 낮고 자식 클래스들을 통해 더 많은 커스터마이징이 가능한 팩토리 메서드로 시작해 더 유연하면서도 더 복잡한 추상 팩토리, 프로토타입 또는 빌더 패턴으로 발전해 나갑니다.
-
빌더는 복잡한 객체들을 단계별로 생성하는 데 중점을 둡니다. 추상 팩토리는 관련된 객체들의 패밀리들을 생성하는 데 중점을 둡니다. 추상 팩토리는 제품을 즉시 반환하지만 빌더는 제품을 가져오기 전에 당신이 몇 가지 추가 생성 단계들을 실행할 수 있도록 합니다.
-
당신은 복잡한 복합체 패턴 트리를 생성할 때 빌더를 사용할 수 있습니다. 왜냐하면 빌더의 생성 단계들을 재귀적으로 작동하도록 프로그래밍할 수 있기 때문입니다.
-
빌더를 브리지와 조합할 수 있습니다. 디렉터 클래스는 추상화의 역할을 하고 다양한 빌더들은 구현의 역할을 합니다.