[DesignPattern] Builder 패턴

개요

Builder 패턴은 프로퍼티가 다수 존재하는 복잡한 객체를 생성할 때,

이니셜라이저를 통해 모든 입력을 미리 요구하는 대신 단계별로 입력을 제공하여 생성할 수 있도록 도와주는 디자인 패턴임

builder_diagram.png

주요 구성은 위 그림에서 볼 수 있듯 Director, Builder, Product로 구성되어진다

Direct는 Product를 사용 / 보유 할 수 있다

Direct는 Builder객체를 사용 / 보유할 수 있다

Builder는 builde() 메소드를 사용해 Product를 생성한다 ( dependency 관계인 이유임 )

정리해보면 Director는 Builder를 사용하여 Product를 생성하고 생성된 Product를 사용 / 보유할 수 있음

3가지 구성요소에 대해 조금더 알아보자

  • Director : 입력을 받고 Builder와 함께 이 입력을 조정함, 보통 ViewController나 ViewController에서 사용하는 Helper Class로 사용되어짐
  • Builder : 단계별로 Director로 부터 입력을 받고 이 입력을 기반으로 실제로 Product를 생성하는 역할
  • Product : 생성되어지는 복잡한 객체, 참조 타입에 따라 구조체, 클래스로 생성되어진다 일반적으로 Model 역할의 객체이지만 상황에 따라 모든 Type이 될 수 있음

언제 사용하는가?

앞서 말하였듯이 복잡한 객체를 생성할 때 일련의 단계에 따라 생성하려는 경우에 사용함

빌더 패턴은 Product에 여러 입력이 필요할 때(초기화가 필요한 프로퍼티들이 많은 경우) 특히나 유용하다

반대로 말하면 간단한 객체를 빌더 패턴으로 사용한다면 복잡성만 증가하게될 것임

Builder는 입력이 Product를 만드는데 어떻게 사용되는지를 추상화하고 Director가 어떤순서로 입력을 요구하든 모두 받아들인다.

예를 들어 햄버거를 만드는 Builder를 구현한다고 생각해보자

Product는 패티, 소스, 토핑를 가지고 있는 햄버거 Model이라고 생각할 수 있다

Director는 햄버거를 만드는 직원 객체이거나, 사용자의 입력을 받는 ViewController일 수 도 있다

Builder는 고기를 선택하고, 토핑, 소스를 어떤 순서대로든지 입력받고 만들어 오는 요청이 오면 햄버거를 생성한다

“패티는 소고기로, 소스는 핫칠리소스, 양상추, 양파 많이, 토마토는 뺴주세요”

“소스는 갈릭마요 토핑은 양상추, 양파, 치즈 2장, 패티는 치킨패티요!”

순서는 상관이 없다, 어떤 순서로 만들어도 햄버거다

예제 코드

// Product
struct Hamburger {
    let meat: String
    let toppings: [String]
    let sauce: String
    
    var description: String {
        return "\(meat) burger, toppings : \(toppings), sauce : \(sauce)"
    }
}

일반적인 Model 역할을 한다

// Builder
class HamburgerBuilder {
    private let meat: String
    private var toppings: [String] = []
    private var sauce: String = ""
    init(meat: String) {
        self.meat = meat
    }
    
    func addTopping(topping: String) -> Self {
        toppings.append(topping)
        return self
    }
    
    func addSauce(sauce: String) -> Self {
        self.sauce = sauce
        return self
    }
    
    func build() -> Hamburger {
        return Hamburger(meat: meat,
                         toppings: toppings,
                         sauce: sauce)
    }
}

meat의 경우 이니셜라이저로 초기값을 결정하고 변경할 수 없도록 let으로 선언

나머지 프로퍼티의 경우 변경을 허용할 생각이기 때문에 var로 선언하였다

각 메소드의 경우 옵셔널체이닝 처럼 사용할 수 있도록 하기 위해 Self를 반환하였음

// Director
class Employee {
    static func createCombo() -> Hamburger {
        let builder = HamburgerBuilder(meat: "beef")
        return builder.addTopping(topping: "양파")
            .addTopping(topping: "양상추")
            .addSauce(sauce: "칠리소스")
            .build()
    }
    
    static func createSpecial() -> Hamburger {
        let builder = HamburgerBuilder(meat: "돼지")
        return builder.addTopping(topping: "양파")
            .addSauce(sauce: "갈릭마요")
            .addTopping(topping: "양상추")
            .addTopping(topping: "토마토")
            .addTopping(topping: "치즈")
            .build()
    }
}

createCombo()createSpecial()을 보면 메소드 순서가 다른걸 볼 수 있다.

즉 입력의 순서는 상관이 없단 것!

let burger = Employee.createCombo()
print(burger.description)
// beef burger, toppings : ["양파", "양상추"], sauce : 칠리소스
let specialBurger = Employee.createSpecial()
print(specialBurger.description)
// 돼지 burger, toppings : ["양파", "양상추", "토마토", "치즈"], sauce : 갈릭마요

패턴의 사용 결과

  1. Builder가 제공하는 추상적인 인터페이스를 통해 Product객체를 만들기 때문에 어떻게 객체를 만들고 표현하는지에 대한 내부 구조를 숨길 수 있음
  2. Product객체가 많은 프로퍼티를 가지는 복잡한 객체일 경우, 이니셜라이저에 많은 파라미터를 정의해야 하는데 이럴 경우 각 파라미터가 어떤 값을 의미하는지 혼돈이 올 수 있지만 빌더 패턴을 사용할 경우 명확해짐
  3. 초기화 하지 않아도 되는 값이 있을 경우 굳이 생성자에 명시하지 않아도 됨
  4. 단계별로 객체를 생성하기 때문에 객체 생성을 세밀하게 제어할 수 있음

Swift 언어의 특성상 2,3번의 문제를 쉽게 해결할 수 있음

2번의 경우 파라미터에도 네이밍을 정해줄 수 있으며

3번의 경우 편의 이니셜라이저나 옵셔널 선언, default value지정등 여러 방법이 존재함

참고

https://brunch.co.kr/@yoonms/24

https://icksw.tistory.com/236

Raywenderich

댓글남기기