[Swift] 디자인 패턴 (Design Patterns) - 생성 패턴 (빌더 패턴, Builder pattern)

Fezravien·2021년 10월 14일
0
post-thumbnail

이번 포스트는 디자인 패턴의 생성 패턴(Creational Patterns) 중 빌더 패턴(Builder pattern)입니다 😗

생성 패턴 (Creational patters)

생성 패턴은 객체를 어떻게 생성하는지 다루는 디자인 패턴이다.

생성 패턴에는 두 가지 기본 개념이 있다.

  • 어떤 구체적인 타입이 생성돼야 하는지에 대한 정보를 캡슐화하기
  • 이러한 타입의 인스턴스가 어떻게 생성되는지를 숨기기

생성 패턴 범주의 한 부분으로 널리 알려진 패턴으로는 다섯 가지가 있으며, 이는 다음과 같다.

  • 싱글턴 패턴 (Singleton pattern)
    애플리케이션 주기 동안 하나의 클래스 인스턴스를 허용한다.

  • 빌더 패턴 (Builder pattern)
    복잡한 객체의 생성과 표현을 서로 분리해 유사한 타입을 생성하기 위해 동일한 프로세스가 사용될 수 있게 한다.

  • 팩토리 메소드 패턴 (Factory method pattern)
    객체(또는 객체의 타입)를 어떻게 생성하는지에 대한 근본적인 로직을 노출하지 않으면서 객체를 생성한다.

  • 추상 팩토리 패턴 (Abstract factory pattern)
    구체적인 타입을 명시하지 않으면서 관련된 객체를 생성하기 위한 인터페이스를 제공한다.

  • 프로토타입 패턴 (Prototype pattern)
    이미 존재하는 객체를 복사하는 방식으로 객체를 생성한다


빌더 패턴 (Builder pattern)

빌더 패턴은 복잡한 객체의 생성을 도우면서 어떻게 객체들을 생성하는지에 대한 프로세스를 강제한다.
일반적으로 빌더 패턴에서는 복잡한 타입으로부터 생성로직을 분리하며, 다른 타입을 추가한다.

빌더 패턴은 타입의 서로 다른 결과물을 생성하는 데 동일한 생성 프로세스를 사용하게 해준다.

빌더 패턴은 언제 사용할까?

빌더 패턴은 타입의 인스턴스가 설정 가능한 여러 값을 요구하는 문제를 해결하기 위해 설계됐다.

클래스의 인스턴스를 생성할 때 설정 옵션을 추가할 수도 있지만,
옵션이 올바르게 설정되지 않았거나 모든 옵션에 대한 적절한 값을 알지 못하는 경우에는 문제가 발생할 수 있다.
또 다른 문제는 타입의 인스턴스를 생성할 때마다 모든 설정 가능한 옵션을 설정하는 데 만은 양의 코드가 필요하다.

빌더 패턴은 builder 타입으로 알려진 중개자를 이용해 이러한 문제를 해결한다.
빌더 타입은 원래의 복잡한 타입의 인스턴스를 생성하는 데 필요한 대부분의 정보를 보유하고 있다.


빌더 패턴을 구현하는 데 사용하는 방법

  1. 구체적인 방법으로, 원래의 복잡한 객체를 설정하는 정보를 가진 여러 가지의 빌더 타입을 갖는 방식
  2. 모든 설정 가능한 옵션을 기본 값으로 설정하는 단일 빌더 타입을 사용해 빌더 패턴을 구현하며,
    필요하다면 옵션 값을 변경하는 방식이다.

빌더 패턴 구현하기

기존의 복잡한 구조체 이용하기

우선 빌더 패턴을 설계해 해결하고자 했던 문제를 살펴보기 위해 빌더 패턴을 사용하지 않고
복잡한 구조체를 만드는 방법을 알아보는 것부터 시작해보자.

struct SampleBurger { 
    var name: String
    var patties: Int
    var bacon: Bool
    var cheese: Bool
    var pickles: Bool
    var ketchup: Bool
    var mustard: Bool
    var lettuce: Bool
    var tomato: Bool
    
    init(name: String, patties: Int, bacon: Bool, cheese: Bool, pickles: Bool,
         ketchup: Bool, mustard: Bool, lettuce: Bool, tomato: Bool) {
        self.name = name
        self.patties = patties
        self.bacon = bacon
        self.cheese = cheese
        self.pickles = pickles
        self.ketchup = ketchup
        self.mustard = mustard
        self.lettuce = lettuce
        self.tomato = tomato
    }
}

SampleBurger 구조체는 어떠한 양념이 버거에 들어가는지와 버거의 이름을 정의한 프로퍼티를 여러 개 갖고 있다.
이러한 프로퍼티는 SampleBurger 구조체의 인스턴스를 생성할 때 반드시 알고 있어야 하므로,
이니셜라이저는 사용자에게 각 아이템을 정의할 것을 요구한다.

이는 애플리케이션 내에서 복잡한 초기화로 이어지게 되며,
기본적인 버거(베이컨 치즈버거, 치즈버거, 햄버거 등)를 한 가지 이상 갖게되는 경우에는
각각의 버거가 올바르게 정의됐는지를 확인해야만 한다.

// 햄버거 생성
var hamBurger = Burger(name: "Hamburger", 
                    patties: 1, 
                    bacon: false, 
                    cheese: false, 
                    pickles: false,
                    ketchup: false, 
                    mustard: false, 
                    lettuce: false, 
                    tomato: false)
                    
 // 치즈버거 생성
var cheeseBurger = Burger(name: "Cheeseburger", 
                    patties: 1, 
                    bacon: false, 
                    cheese: true, 
                    pickles: true,
                    ketchup: true, 
                    mustard: trie, 
                    lettuce: false, 
                    tomato: false)

Burger 타입의 인스턴스를 생성하는 데에는 많은 코드가 필요하다.

빌더 패턴을 사용해 개선하기 - 1

빌더 패턴을 사용해 이러한 타입의 생성 방법을 어떻게 향상시킬 수 있는지 살펴보자

protocol BugerBuilder {
    var name: String { get }
    var patties: Int { get }
    var bacon: Bool { get }
    var cheese: Bool { get }
    var pickles: Bool { get }
    var ketchup: Bool { get }
    var lettuce: Bool { get }
    var tomato: Bool { get }
}

struct HamBurgerBuilder: BugerBuilder {
    let name = "HamBurger"
    let patties = 1
    let bacon = false
    let cheese = false
    let pickles = true
    let ketchup = true
    let mustard = true
    let lettuce = false
    let tomato = false
}

struct CheeseBurgerBuilder: BugerBuilder {
    let name = "CheeseBurger"
    let patties = 1
    let bacon = false
    let cheese = true
    let pickles = true
    let ketchup = true
    let mustard = true
    let lettuce = false
    let tomato = false
}

struct Burger {
    var name: String
    var patties: Int
    var bacon: Bool
    var cheese: Bool
    var pickles: Bool
    var ketchup: Bool
    var mustard: Bool
    var lettuce: Bool
    var tomato: Bool
    
    init(builder: BurgerBuilder) {
        self.name = builder.name
        self.patties = builder.patties
        self.bacon = builder.bacon
        self.cheese = builder.cheese
        self.pickles = builder.pickles
        self.ketchup = builder.ketchup
        self.mustard = builder.mustard
        self.lettuce = builder.lettuce
        self.tomato = builder.tomato
    }
    
    func showBurger() {
        print("Name: \(name)")
        print("Patties: \(patties)")
        print("Bacon: \(bacon)")
        print("Cheese: \(cheese)")
        print("Pickles: \(pickles)")
        print("Ketchup: \(ketchup)")
        print("Mustard: \(mustard)")
        print("Lettuce: \(lettuce)")
        print("Tomato: \(tomato)")
    }
}

기존에 사용하던 SampleBuger의 생성자는 매개변수로 9가지의 프로퍼티 내용을 전부 가지고 있었다.
새로운 Burger 구조체에서 생성자는 매개변수를 한 개만 가지며,
이 매개션수는 BurgerBuilder 프로토콜을 따르는 타입의 인스턴스다.

// 햄버거를 생성한다.
var fezzBuger = Buger(builder: HamBurgerBuilder())
fezzBuger.showBuger()

// 토마토가 들어간 치즈버거를 생성한다.
var cheeseBurgerBuilder = CheeseBurgerBuilder()
var fezzCheeseBurger = Buger(builder: cheeseBurgerBuilder)

// 토마토를 뺀다.
fezzCheeseBurger.tomato = false
fezzCheeseBurger.showBurger()

새로운 Burger 구조체를 어떻게 생성하는지 앞이 SampleBurger 구조체와 비교하면
Burger 구조체의 인스턴스 생성이 훨씬 더 간단했다는 것을 확인할 수 있다.


빌더 패턴을 사용해 개선하기 - 2

두 번째 방법에서는 여러 빌더 형태를 보이는 것과는 달리 모든 설정 가능한 값을 기본 값으로 설정한 단일 빌더 타입을 갖는다.
그러면 해당 값은 필요에 따라 변경할 수 있다.

이 방법은 기존 코드와 통합하기 쉽기 때문에 오래된 코드를 업데이트하는 경우 이런 구현 방법을 활용할 수 있다.

먼저 단일 BurgerBuilder 구조체를 생성한다.
BurgerBuilder 구조체는 SampleBurger 구조체를 생성하는 데 사용될 것이며,
기본적으로 모든 재료를 기본 값으로 설정할 것이다.

struct BurgerBuilder {
    let name = "Burger"
    let patties = 1
    let bacon = false
    let cheese = false
    let pickles = true
    let ketchup = true
    let mustard = true
    let lettuce = false
    let tomato = false
    
    mutating func setPatties(choice: Int) { self.patties = choice }
    mutating func setBacon(choice: Bool) { self.bacon = choice }
    mutating func setCheese(choice: Bool) { self.cheese = choice }
    mutating func setPickles(choice: Bool) { self.pickles = choice }
    mutating func setKetchup(choice: Bool) { self.ketchup = choice }
    mutating func setMustard(choice: Bool) { self.mustard = choice }
    mutating func setLettuce(choice: Bool) { self.lettuce = choice }
    mutating func setTomato(choice: Bool) { self.tomato = choice }
    
    func buildSampleBurger(name: String) -> SampleBurger {
        return SampleBurger(name: name, 
                    		patties: self.patties, 
                    		bacon: self.bacon, 
                    		cheese: self.cheese, 
                    		pickles: self.pickles,
                    		ketchup: self.ketchup, 
                    		mustard: self.mustard, 
                    		lettuce: self.lettuce, 
                    		tomato: self.tomato)
    }
}

BurgerBuilder 구조체에서는 버거를 위한 9개의 프로퍼티를 정의하고 있으며,
name 프로퍼티를 제외한 각각의 프로퍼티에 대해 세터 메소드를 생성하고 있다.

또한, buildSampleBurger 메소드를 생성하는데,
이 메소드는 BurgerBuilder 인스턴스에 있는 프로퍼티의 값을 기반으로 하는 SampleBurger 구조체의 인스턴스를 생성한다.

var burgerBuilder = BurgerBuilder()
burgerBuilder.setCheese(choice: true)
burgerBuilder.setBacon(choice: true)
burgerBuilder.setMustard(choice: false)
var fezzBurger = burgerBuilder.buildSampleBurger(name: "Fezz's Burger")

이 코드는 BurgerBuilder 구조체의 인스턴스를 생성하고,
버거에 치즈와 베이컨은 추가하고 머스타드는 빼기위해 set~ 메소드를 사용했다.
마지막으로 SampleBurger 구조체의 인스턴스를 생성하기 위해 buildSampleBurger 메소드를 호출한다.

빌더 패턴을 구현하는 데 사용하는 두 방법 모두 복잡한 타입을 생성하기 간단해졌다.
또한, 두 방법 모두 인스턴스가 기본 값으로 적절하게 설정됨을 보장한다.

빌더 패턴은 iOS를 개발할때 Storyboard가 아닌 코드로 UI를 작성할 때 활용할 수 있다.
라벨을 생성할때 응집도를 높히는 방법으로 이렇게 클로저를 이용하기도 하는데

let itemLabel: UILabel = {
   let label = UILabel()
   label.text = "MacBook Pro 2021년도 M1X !"
   label.textColor = .black
   label.font = .systemFont(sizeOf: 20)
   return label    
}()

이렇게 여러개의 라벨을 만들게 된다면 공통된 부분을 계속 작성해줘야겠죠?
빌드 패턴을 이용해 공통된 부분을 빼서 생성하는 것을 해보도록 할게요

protocol Builder {
    var label: UILabel { get }
    func setText(with text: String) -> Builder
    func setTextColor(with textColor: UIColor) -> Builder
    func setFontSize(with textFontSize: CGFloat) -> Builder
}

class ConcreateBuilder: Builder {
    var label: UILabel = UILabel()
   
    func setText(with text: String) -> Builder {
        label.text = text
        return self
    }
   
    func setTextColor(with textColor: UIColor) -> Builder {
        label.textColor = textColor
        return self
    }
   
    func setFontSize(with textFontSize: CGFloat) -> Builder {
        label.font = .systemFont(ofSize: textFontSize)
        return self
    }
}

class Director {
    func makeLabel(builder: Builder) -> UILabel {
        let build = builder
        build.setText(with: "MacBook pro 2021 M1X !")
        build.setTextColor(with: .red)
        build.setFontSize(with: 20)
        return build.label
    }
}

class ViewController: UIViewController {
    private let director: Director = Director()
    
    private let itemLabel: UILabel = ConCreateBuilder()
        .setText(with: "MacBook pro 2021 M1X !")
        .setTextColor(with: .blue)
        .setFontSize(with: 25)
        .label
        
    override func viewDidLoad() {
        super.viewDidLoad()
        let label = director.makeLabel(builder: ConcreateBuilder())
		...
       
    }
}

이렇게 체이닝 방식, Director를 이용한 방식 두 가지를 사용할 수 있습니다.

profile
꺼진 뷰도 다시보자.

0개의 댓글