이번 포스트는 디자인 패턴의 생성 패턴(Creational Patterns) 중 빌더 패턴(Builder pattern)입니다 😗
생성 패턴은 객체를 어떻게 생성하는지 다루는 디자인 패턴이다.
생성 패턴에는 두 가지 기본 개념이 있다.
생성 패턴 범주의 한 부분으로 널리 알려진 패턴으로는 다섯 가지가 있으며, 이는 다음과 같다.
싱글턴 패턴 (Singleton pattern)
애플리케이션 주기 동안 하나의 클래스 인스턴스를 허용한다.
빌더 패턴 (Builder pattern)
복잡한 객체의 생성과 표현을 서로 분리해 유사한 타입을 생성하기 위해 동일한 프로세스가 사용될 수 있게 한다.
팩토리 메소드 패턴 (Factory method pattern)
객체(또는 객체의 타입)를 어떻게 생성하는지에 대한 근본적인 로직을 노출하지 않으면서 객체를 생성한다.
추상 팩토리 패턴 (Abstract factory pattern)
구체적인 타입을 명시하지 않으면서 관련된 객체를 생성하기 위한 인터페이스를 제공한다.
프로토타입 패턴 (Prototype pattern)
이미 존재하는 객체를 복사하는 방식으로 객체를 생성한다
빌더 패턴은 복잡한 객체의 생성을 도우면서 어떻게 객체들을 생성하는지에 대한 프로세스를 강제한다.
일반적으로 빌더 패턴에서는 복잡한 타입으로부터 생성로직을 분리하며, 다른 타입을 추가한다.
빌더 패턴은 타입의 서로 다른 결과물을 생성하는 데 동일한 생성 프로세스를 사용하게 해준다.
빌더 패턴은 타입의 인스턴스가 설정 가능한 여러 값을 요구하는 문제를 해결하기 위해 설계됐다.
클래스의 인스턴스를 생성할 때 설정 옵션을 추가할 수도 있지만,
옵션이 올바르게 설정되지 않았거나 모든 옵션에 대한 적절한 값을 알지 못하는 경우에는 문제가 발생할 수 있다.
또 다른 문제는 타입의 인스턴스를 생성할 때마다 모든 설정 가능한 옵션을 설정하는 데 만은 양의 코드가 필요하다.
빌더 패턴은 builder
타입으로 알려진 중개자를 이용해 이러한 문제를 해결한다.
빌더 타입은 원래의 복잡한 타입의 인스턴스를 생성하는 데 필요한 대부분의 정보를 보유하고 있다.
우선 빌더 패턴을 설계해 해결하고자 했던 문제를 살펴보기 위해 빌더 패턴을 사용하지 않고
복잡한 구조체를 만드는 방법을 알아보는 것부터 시작해보자.
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
타입의 인스턴스를 생성하는 데에는 많은 코드가 필요하다.
빌더 패턴을 사용해 이러한 타입의 생성 방법을 어떻게 향상시킬 수 있는지 살펴보자
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
구조체의 인스턴스 생성이 훨씬 더 간단했다는 것을 확인할 수 있다.
두 번째 방법에서는 여러 빌더 형태를 보이는 것과는 달리 모든 설정 가능한 값을 기본 값으로 설정한 단일 빌더 타입을 갖는다.
그러면 해당 값은 필요에 따라 변경할 수 있다.
이 방법은 기존 코드와 통합하기 쉽기 때문에 오래된 코드를 업데이트하는 경우 이런 구현 방법을 활용할 수 있다.
먼저 단일 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를 이용한 방식 두 가지를 사용할 수 있습니다.