[Design Pattern] 빌더 패턴

Martin Kim·2022년 4월 6일
0

디자인패턴

목록 보기
6/6

의도

  • 복잡한 객체를 생성하는 방법과 표현하는 방법을 정의하는 클래스를 별도로 분리하여, 서로 다른 표현이라도 이를 생성할 수 있는 동일한 절차를 제공할 수 있도록 하기 위함이다.
  • 동일한 구성 코드를 사용하여 객체의 다양한 유형과 표현을 생성할 수 있다.

예시

객체의 가능한 모든 구성에 대한 하위 클래스를 만드는 것은프로그램을 너무 복잡하게 만들 수 있다.

예를 들어 집을 짓기 위한 House 라는 객체를 생성하는 방법을 생각해보자. 간단한 집을 지으려면 네 개의 벽과 바닥을 만들고 문을 설치하고 한 쌍의 창문을 맞추고 지붕을 만들어야 한다. 하지만 뒤뜰과 기타 물품(난방 시스템, 배관 및 전기 배선 등)이 있는 더 크고 밝은 집을 원한다면 어떻게 될까?

가장 간단한 솔루션은 기본 House 클래스를 확장하고 매개변수의 모든 조합을 포함하는 하위 클래스 집합을 만드는 것이다. 그러나 결국엔 상당한 수의 하위 클래스를 만드는 것으로 끝날 것이다. 현관 스타일과 같은 새로운 매개변수를 사용하려면 이 계층을 훨씬 더 키워야 한다.

하위 클래스를 만들지 않는 또 다른 접근 방식이 있다. 집 House 객체를 제어하는 모든 가능한 매개 변수를 사용하여 기본 클래스에서 바로 거대한 생성자를 만들 수 있다. 이 접근 방식은 실제로 하위 클래스의 필요성을 없애지만, 또다른 문제를 야기한다.

매개변수가 많은 생성자는 단점이 있다. 모든 매개변수 각각이 항상 필요한 것이 아니기 때문이다.

대부분의 경우에, 매개변수 전부가 사용되지는 않아서 생성자가 보기 좀 흉할 것이다. 예를 들어, 몇몇 집에만 수영장이 있을 것이므로, 수영장과 관련된 매개변수는 대부분 쓸모가 없을 것이다.

해결방법

Builder 패턴은 클래스 자체에서 객체 생성 코드를 추출하여 builders 라는 별도의 객체로 이동할 것을 제안한다.

빌더 패턴을 사용하면 복잡한 객체를 단계적으로 구성할 수 있다. 빌더는 빌드되는 동안 다른 객체가 제품에 액세스하는 것을 허용하지 않는다.

패턴은 객체의 구성을 어떠한 단계(buildWalls, buildDoor 등)로 구성한다. 객체를 생성하려면 빌더 객체에 대해 일련의 이러한 단계를 실행한다. 중요한 점은 모든 단계를 호출할 필요는 없다는 것이다. 객체의 특정 구성을 생성하는 데 필요한 단계만 호출할 수 있다.

제품의 다양한 표현을 작성해야하는 경우 일부 구성 단계에서는 다른 구현이 필요할 수 있다. 예를 들어서, 오두막의 벽은 나무로 지을 수 있지만, 성벽은 돌로 지어야 한다.

이 경우 동일한 빌드 단계 세트를 구현하지만, 다른 방식으로 여러 다른 빌더 클래스를 작성할 수 있다. 그런 다음 구성 프로세스에서 이러한 빌더(즉, 빌딩 단계에 대한 순서화된 호출 집합)를 사용하여 다양한 종류의 객체를 생성할 수 있다.

다른 빌더는 다양한 방식으로 동일한 작업을 실행한다.

예를 들어, 나무와 유리로 모든 것을 짓는 건축가, 돌과 철로 모든 것을 짓는 건축가, 금과 다이아몬드를 사용하는 건축가들이 있다. 같은 단계를 호출해서 첫 건축가에서 일반 주택, 그 다음 건축가로부터 작은 성, 마지막 건축가로부터 궁전을 얻을 수 있다. 그러나 이것은 빌드 프로세스(단계)를 호출하는 클라이언트 코드가 모두 공통의 인터페이스를 사용하여 빌더와 상호작용할 수 있는 경우에만 작동한다.

++)더 나아가 제품을 director 라는 별도의 클래스로 구성하는 데 사용하는 빌더 단계에 대한 호출들을 추출할 수 있다. 이 클래스는 빌드 단계를 실행하는 순서를 정의하는 반면 빌더는 이러한 단계에 대한 구현을 제공한다.

프로그램에 director 클래스를 포함하는 것이 꼭 필요한 것은 아니다. 클라이언트 코드에서 직접 특정 순서로 빌드 단계를 항상 호출할 수 있다. 하지만 director 클래스는 다양한 구성 루틴을 배치하여 프로그램 전체에서 재사용할 수 있는 좋은 장소일 수 있다. 마치 배치 스크립트 처럼?

또한 director 클래스는 클라이언트 코드에서 제품 구성의 세부 정보를 완전히 숨긴다. 클라이언트는 건축업자를 감독과 연결하고 감독과 건설을 시작하고 건축업자로부터 결과를 얻기만 하면 된다.

구조

  1. Builder 인터페이스는 모든 유형의 빌더에 공통적인 제품의 구성 단계를 선언한다.
  2. ConcreteBuilder 는 건설 단계의 다양한 구현을 제공한다. 빌더는 공통 인터페이스에 더해 인터페이스에 정의되지 않은 메서드를 구현하여 다양한 제품을 생성하도록 할 수 있다.
  3. Product 는 빌더에 의해 생성된 객체이다. 다른 빌더에 의해 생성된 product는 동일한 클래스 계층 또는 인터페이스에 속할 필요가 없다.
  4. Director 클래스는 구성 단계를 호출하는 순서를 정의하므로 제품의 어떤 특정한 구성을 만들고 재사용할 수 있다.
  5. Client 는 빌더 객체 중 하나를 Director 와 연결해야 한다. 일반적으로는 생성자의 매개변수를 통해 주입받아 단 한번만 수행된다. 그런 다음에 Director는 모든 추가 구성에 해당 빌더 객체를 사용한다. 그러나 Client 가 빌더 객체를 Director 의 프로덕션 메서드에 전달할 때를 위한 대체 접근 방식(위 사진에서는 changeBuilder)이 있다. 이 경우 이 메서드를 사용해서 Director와 함께 무언가를 제작할 때마다 다른 빌더를 사용할 수 있다.

구현방법

  1. 우선, 사용 가능한 모든 제품의 표현을 작성하기 위한 공통의 구성 단계를 명확하게 정의할 수 있는지 확인한다. 그럴 수 없다면, 패턴 구현을 진행할 수 없기 때문이다.
  2. 기본 빌더의 인터페이스에서 이 단계들을 선언한다.
  3. 각 제품에 대한 구체적인 빌더 클래스를 만들고 인터페이스에 선언한 해당 단계들을 구현한다.
    1. 구성 결과를 가져오는 메서드(get 메서드)를 구현하는 것을 잊지 않는다. 빌더 인터페이스 내에서 이 메서드를 선언할 수 없는 이유는 다양한 빌더가 공통 인터페이스를 따르지 않는, 확장된 어떤 제품을 구성할 수 있기 때문이고, 따라서 이러한 메서드의 리턴 타입이 무엇인지 알 수 없기 때문이다. 그러나 단일의, 확장할 계획이 없는 제품을 만들기로 한 경우, 가져오는 메서드를 기본 인터페이스에 안전하게 추가할 수 있다.
  4. Director 클래스를 만드느 것에 대해 고민해 본다. 동일한 빌더 객체를 사용하여 제품을 구성하는 다양한 방법을 캡슐화할 수 있다.
  5. 클라이언트 코드는 빌더와 Director 객체 모두를 생성한다. 구성을 시작하기 전에 클라이언트는 빌더 객체를 Director 에게 전달 해야 한다. 일반적으로 클라이언트는 의존성 주입을 통해, 즉 Director 의 생성자 매개변수를 통해 이 작업을 수행한다. Director 는 모든 추가 구성에서 빌더 객체를 사용한다. 아니면, 빌더가 Director 의 프로퍼티로 전달 될 수 있는 대체 접근 메서드를 구현할 수도 있다.
  6. 모든 제품이 동일한 인터페이스를 따르는 경우에만 Director 로부터 직접 구성 결과를 얻을 수 있다. 그렇지 않으면 클라이언트 코드는 빌더에서 결과를 가져와야 한다.

활용성

  • 빌더 패턴을 사용하여 ‘텔레스코픽 생성자' → 망원경 생성자? 제거하기
    • 10개의 선택적 매개변수가 있는 생성자가 있다고 가정하자. 이런 생성자를 호출하는 것은 매우매우 불편하다. 따라서 생성자를 오버로딩하고, 더 적은 수의 매개변수를 사용하여 더 짧은 버전의 생성자를 여러 개 만든다. 이러한 생성자는 여전히 기본 생성자를 참조하여 생략된 매개변수에 일부 기본값을 전달한다.
    • 빌더 패턴을 사용하면 실제로 필요한 단계만 사용하여 단계별로 객체를 빌드할 수 있다. 패턴을 구현한 후에는 더 이상 엄청난 수의 매개변수를 생성자에 집어넣지 않아도 된다.
  • 코드의 일부 제품(예를 들어, 석조 및 목조 주택)의 다른 표현을 생성하도록 하려면 빌더 패턴을 사용한다.
    • 빌더 패턴은 제품의 다양한 표현을 구성할 때 세부 사항만 다른 비슷한 단계를 포함할 때 적용할 수 있다.
    • 기본 빌더 인터페이스는 가능한 모든 구성 단계를 정의하고, 구체적인 빌더는 이러한 단계를 구현하여 제품의 특정 표현을 구성한다. Director 클래스는 어떻게 생성할지 생성 단계를 안내하는 역할을 한다.
  • 빌더를 사용하여 합성 트리 또는 기타 복잡한 객체를 구성한다.
    • 빌더 패턴을 사용하면 제품을 단계별로 구성할 수 있다. 최종 제품을 손상시키지 않고, 일부 단계의 실행을 연기할 수 있다. 재귀적으로 단계를 호출할 수도 있다. 이것은 객체 트리를 구축해야할 때 유용하다.
    • 빌더는 단계를 수행하는 동안 미완성 제품을 노출시키지 않는다. 이렇게 하면 클라이언트가 미완성 제품을 가지고 불완전한 결과를 내는 것을 미연에 방지할 수 있다.

장점

  • 객체를 단계별로 구성하거나 구성 단계를 연기하거나 재귀적으로 단계를 실행할 수 있다.
  • 제품의 다양한 표현을 작성할 때 동일한 구성 코드를 재사용할 수 있다.
  • 단일 책임 원칙(Single Responsibility Principle). 제품의 비즈니스 로직에서 복잡한 구성 코드를 분리할 수 있다.

단점

  • 패턴이 여러 개의 새 클래스를 생성해야 하기 때문에 코드의 전반적인 복잡성은 증가한다.

In Swift

import XCTest

/// 빌더 인터페이스. 제품 객체를 생성하는 단계의 메서드를 기술한다.
protocol Builder {

    func producePartA()
    func producePartB()
    func producePartC()
}

/// 구체 빌더 클래스.  빌더 인터페이스를 채택하고 구체적인 구성 단계의 구현을 기술한다. 
/// 프로그램은 아마 몇몇 빌더의 바리에이션이 존재할 것인데, 이를 서로 다르게 구현할 수 있다.
class ConcreteBuilder1: Builder {

    /// 갓 생성된 빌더 인스턴스는 텅빈 제품 객체를 가질 것이다.
    private var product = Product1()

    func reset() {
        product = Product1()
    }

    /// 모든 생성 단계는 같은 제품을 가지고 이뤄진다.
    func producePartA() {
        product.add(part: "PartA1")
    }

    func producePartB() {
        product.add(part: "PartB1")
    }

    func producePartC() {
        product.add(part: "PartC1")
    }

    /// 구체적 빌더는 결과를 내보내는 자신만의 고유한 메서드를 제공해야 한다. 다양한 유형의 빌더가 동일한 인터페이스를 따르지 않는, 타입이 다른 제품을 만들 수 있기 때문이다.
		/// 따라서 이러한 메서드는 기본 빌더의 인터페이스에서 선언할 수 없다.(적어도 정적으로 형식화된 프로그래밍 언어에서는)
		/// 일반적으로, 최종 결과를 클라이언트에 반환한 후 빌더 인스턴스는 다른 제품 생산을 시작할 준비가 되어야 한다. 
		/// 이것이 아래 `getProduct` 메서드 본문의 끝에서 reset 메서드를 호출하는 것이 일반적으로 관행인 이유이다. 
		/// 그러나 이 동작은 필수가 아니며, 빌더가 이전 결과를 삭제하기 전에 클라이언트 코드에서 명시적인 리셋 메서드호출을 기다리게 할 수 있다.
    func retrieveProduct() -> Product1 {
        let result = self.product
        reset()
        return result
    }
}

/// Director는 특정 순서에 따라 각 단계를 실행하는 것에 대해서만 책임이 있다. 
/// 특정 주문이나 구성에 따라 제품을 생산해야 할 때 유용하다. 
/// 엄밀히 말하자면, Director 클래스는 선택 사항이다. 클라이언트도 빌더를 직접 제어할 수 있기 때문이다.
class Director {

    private var builder: Builder?

    /// The Director works with any builder instance that the client code passes
    /// to it. This way, the client code may alter the final type of the newly
    /// assembled product.
    func update(builder: Builder) {
        self.builder = builder
    }

    /// Director는 동일한 구성 단계를 사용해서 여러 제품의 변형을 구성할 수 있다.
    func buildMinimalViableProduct() {
        builder?.producePartA()
    }

    func buildFullFeaturedProduct() {
        builder?.producePartA()
        builder?.producePartB()
        builder?.producePartC()
    }
}

/// 제품이 꽤 복잡하고, 광범위한 구성이 필요한 경우만 빌더 패턴을 사용하는 것이 좋다.
///
/// 다른 생성 패턴과는 달리, 다른 콘크리트 빌더는 다양한 제품을 생산할 수 있다. 
/// 즉, 다양한 빌더의 결과는 항상 동일한 인터페이스를 따르지 않을 수 있다.
class Product1 {

    private var parts = [String]()

    func add(part: String) {
        self.parts.append(part)
    }

    func listParts() -> String {
        return "Product parts: " + parts.joined(separator: ", ") + "\n"
    }
}

/// 클라이언트 코드는 빌더 객체를 만들면서 감독 객체로 의존성 주입을 한다. 
/// 그리고 생성된 제품을 빌더 객체로부터 받는다.
class Client {
    // ...
    static func someClientCode(director: Director) {
        let builder = ConcreteBuilder1()
        director.update(builder: builder)
        
        print("Standard basic product:")
        director.buildMinimalViableProduct()
        print(builder.retrieveProduct().listParts())

        print("Standard full featured product:")
        director.buildFullFeaturedProduct()
        print(builder.retrieveProduct().listParts())

        // 잊지마십쇼… 클라이언트는 director 객체 없이도 빌더를 사용할 수 있습니다.
        print("Custom product:")
        builder.producePartA()
        builder.producePartC()
        print(builder.retrieveProduct().listParts())
    }
    // ...
}

/// Let's see how it all comes together.
class BuilderConceptual: XCTestCase {

    func testBuilderConceptual() {
        let director = Director()
        Client.someClientCode(director: director)
    }
}

출처: Refactoring Guru
Builder in Swift

profile
학생입니다

0개의 댓글