[Swift 디자인 패턴] Builder Pattern (빌더) - 디자인 패턴 공부 3
안녕하세요 Pingu입니다.🐧
지난 글에서는 Abstract Factory 패턴에 대해 알아봤었는데요, 이번 글에서는 또 다른 Creational Pattern인 Builder Pattern에 대해 알아보려고 합니다.
빌더 패턴이란?
복잡하게 생성되어야 할 객체를 구현할 때 구성을 분리하여 다른 표현으로 만들 수 있게 하는 패턴입니다. 여기서 표현이라고 하니 좀 와 닿지 않는데요, 간단하게 동일한 역할을 하는 다른 코드로 만들 수 있다고 볼 수 있습니다.
즉 어떤 객체를 생성자로 만들 때 한 번에 모두 만들 수도 있지만 객체가 가지는 요소가 많다면 여러 단계로 나누어 객체를 만들 수 있게 하는 패턴입니다.
빌더 패턴은 위의 그림과 같이 3가지 요소로 나눌 수 있어요.
- Director
- input을 받고 builder와 이를 조정합니다. iOS 개발에서는 ViewController나 ViewController에서 사용하는 도우미 클래스라고 해요.
- Builder
- 단계별 입력을 받고 객체를 만듭니다. Builder는 클래스인 경우도 있어 참조하여 재사용 가능합니다.
- Product
- 만들어지는 복잡한 객체입니다. 구조체나 클래스 객체일 수 있습니다.
빌더 패턴 언제 쓰나요?
객체를 생성할 때 단계를 거쳐서 만들고 싶을 때 빌더 패턴을 사용할 수 있습니다. 특히 어떤 객체에 여러개의 입력이 필요할 때 잘 작동할 수 있는데요, Director가 입력을 받고 Builder가 입력을 사용하여 객체를 만드는 방법을 추상화하여 객체를 생성하게 됩니다. 당연한 말이겠지만 굳이 간단한 객체를 빌더 패턴으로 만들면 복잡성만 증가하게 됩니다.
예를 들어 빌더 패턴을 사용하여 제가 좋아하는 햄버거를 만든다고 할 때 최종적으로 만들 객체는 햄버거입니다. Director는 햄버거를 만드는 방법을 알고 있는 사람이라고 볼 수 있죠. Director는 빵, 고기, 토핑, 채소 등을 순서에 상관없이 받고 Builder는 이러한 재료를 가지고 햄버거를 만들게 됩니다.
빌더 패턴의 결과
- 객체의 내부 표현을 변경할 수 있습니다. Builder 객체는 Director에게 객체 구성을 위한 추상 인터페이스를 제공합니다. 인터페이스를 통해 Builder는 자신이 어떻게 객체를 만들고 표현하는지에 대한 내부 구조를 숨길 수 있죠. 객체는 추상 인터페이스를 통해 생성되기 때문에 만약 객체의 내부를 변경하고 싶다면 새로운 Builder를 만들기만 하면 됩니다.
- 객체를 구성하는 코드를 분리할 수 있습니다. 예를 들어 엄청나게 많은 프로퍼티를 갖는 객체가 존재할 때 Builder는 이를 구성하고 표현하는 방식을 캡슐화하여 모듈성을 향상합니다. 이를 사용하는 곳에서는 객체의 내부 구조를 정의하는 클래스에 대해 알 필요가 없고 Builder 인터페이스에도 나타나지 않습니다.
- 객체가 만들어지는 과정을 세밀하게 제어 할 수 있습니다. 한 번에 모든 프로퍼티를 만드는 방법과 다르게 Builder 패턴은 단계별로 객체를 만들기 때문에 이를 세밀하게 제어할 수 있어요.
예제
그럼 실제로 Swift로 빌더 패턴을 간단하게 구현해보겠습니다.
제가 빌더 패턴으로 만들어 볼 객체는 햄버거 객체입니다.
햄버거에는 다양한 재료가 들어가기 때문에 빌더 패턴으로 만들기 적합한데요, 그럼 만들 햄버거부터 정의해볼게요!
public struct Hamburger {
public let meat: Meat
public let sauce: Set<Sauce>
public let vegetable: Set<Vegetable>
public let bread: Bread
func getAllProperty() {
print("Meat = \(meat)")
print("Sauce = \(sauce)")
print("Vegetable = \(vegetable)")
print("Bread = \(bread)\n")
}
}
public enum Meat: String {
case beef
case chicken
case pork
}
public enum Sauce: String {
case mayonnaise
case mustard
case ketchup
case secret
}
public enum Vegetable: String {
case cabbage
case lettuce
case pickels
case tomatoes
}
public enum Bread: String {
case brownBread
case hotDogBun
case mealBread
case ryeBread
}
제가 만든 햄버거에는 빵, 고기, 소스, 채소가 들어갈 수 있고 소스와 채소는 여러 가지가 들어갈 수 있도록 Set 자료구조로 만들었습니다. 이렇게 구성될 햄버거를 만들기 위해 Builder도 만들어볼게요.
public class HamburgerBuilder {
public private(set) var meat: Meat = .beef
public private(set) var sauces: Set<Sauce> = []
public private(set) var vegetables: Set<Vegetable> = []
public private(set) var bread: Bread = .brownBread
public func addSauce(_ sauce: Sauce) {
sauces.insert(sauce)
}
public func removeSauce(_ sauce: Sauce) {
sauces.remove(sauce)
}
public func addVegetable(_ vegetable: Vegetable) {
vegetables.insert(vegetable)
}
public func removeVegetable(_ vegetable: Vegetable) {
vegetables.remove(vegetable)
}
public func setMeat(_ meat: Meat) {
self.meat = meat
}
public func setBread(_ bread: Bread) {
self.bread = bread
}
public func build() -> Hamburger {
return Hamburger(meat: meat, sauce: sauces, vegetable: vegetables, bread: bread)
}
}
위와 같이 HamburgerBuilder를 만들어서 Hamberger 객체를 원하는대로 만들 수 있게 됩니다.
위와 같이 builder에서 build() 메서드만 호출하면 새로운 햄버거 객체가 계속해서 생성됩니다.
또한 builder의 값들을 적절히 설정해서 다양한 유형의 햄버거 객체를 만들 수 있는 것을 볼 수 있어요.
그럼 이번에는 Director를 만들어보겠습니다.
class HamburgerDirector {
public func createBeefBurger() -> Hamburger {
let builder = HamburgerBuilder()
builder.setMeat(.beef)
builder.addSauce(.ketchup)
builder.addSauce(.mustard)
builder.addVegetable(.pickels)
builder.addVegetable(.lettuce)
builder.setBread(.ryeBread)
return builder.build()
}
public func createChickenBurger() -> Hamburger {
let builder = HamburgerBuilder()
builder.setMeat(.chicken)
builder.addSauce(.secret)
builder.addSauce(.mayonnaise)
builder.addVegetable(.cabbage)
builder.addVegetable(.tomatoes)
builder.setBread(.brownBread)
return builder.build()
}
}
위와 같이 Director를 만들어서 비프버거나 치킨버거를 고정된 모양으로 만들어낼 수 있습니다.
만약 빌더 패턴을 사용하지 않고 햄버거를 만든다면 아래와 같이 만들어야합니다.
var hamburgerWithoutBuilder = Hamburger(meat: .beef,
sauce: [.ketchup],
vegetable: [.cabbage],
bread: .brownBread)
어떤가요? 사실 제가 정의한 햄버거가 어떻게 보면 조금 단순하게 4개의 프로퍼티만 가지기 때문에 빌더 패턴이 크게 필요하진 않아 보일 수 있지만 100개의 프로퍼티를 갖는다고 하면 빌더 패턴이 편하다는 것을 느낄 수 있습니다.
그리고 많이 사용하는 햄버거는 Director에 정의하여 만들 수 있기 때문에 매번 직접 햄버거 객체를 만드는 거 보단 쉽게 햄버거를 만들 수 있습니다.
이렇게 빌더 패턴도 간단하게 직접 구현해봤는데요, 이해에 도움이 되셨으면 좋겠습니다.
전체 코드는 여기서 볼 수 있습니다.
감사합니다.