2024. 2. 26. 22:12ㆍArchitecture, Design Pattern
1️⃣ 빌더 패턴 (Builder Pattern)이 왜 필요하게 된 거지?
빌더 패턴 (Bulder Pattern)은 복잡한 객체를 생성하는 방법을 정의한 디자인 패턴이다.
"복잡한 객체를 생성한다"는 말이 잘 와닿지 않을 수도 있어, 내가 주문했던 메뉴 중 가장 복잡했던 서브웨이 샌드위치를 예시로 들어보겠다.
서브웨이 샌드위치를 하나 시키기 위해서는 사소하게 주문해야 할 게 참 많았다.
빵은 어떤 것을 고를 거고, 치즈는 어떻게 할 거고, 야채는 어떤 것은 많이 넣고, 어떤 것은 빼고, 소스는 어떻게 하고... 등을 매번 일일이 주문하기 힘들었던 경험이 서브웨이에 한 번이라도 가본 적 있다면, 공감할 것이다.
이 상황을 iOS 개발자의 입장에서 <서브웨이 샌드위치>를 객체로 바라보고 코드로 표현하면 아래와 같이 작성할 수 있겠다.
class SubwaySandwich {
var bread: String
var cheeze: String
var isHeat: Bool
var lettuce: Bool
var tomato: Bool
var olive: Bool
var onion: Bool
var sauce: [String]
init(bread: String, cheeze: String, isHeat: Bool, lettuce: Bool, tomato: Bool, olive: Bool, onion: Bool, sauce: [String]) {
self.bread = bread
self.cheeze = cheeze
self.isHeat = isHeat
self.lettuce = lettuce
self.tomato = tomato
self.olive = olive
self.onion = onion
self.sauce = sauce
}
}
그럼 매번 해당 클래스에서 받아와 샌드위치 주문(인스턴스를 선언)을 하는 상황에서,
저 많은 프로퍼티의 값들을 매번 일일이 정해줘야 하는 비효율적인 상황이 발생하는 셈인 것이다.
빌더 패턴(Bulder Pattern)은 이러한 비효율적인 상황을 조금 더 효율적으로 바꾸기 위해 등장한 디자인 패턴이다.
2️⃣ 빌더 패턴 (Builder Pattern) 예시 코드로 살펴보기
빌더 패턴의 핵심은 객체를 만드는 과정(내부 프로퍼티와 메서드를 활용)을 하나의 클래스 안에 미리 담아놓는다는 것이다.
아래 구현한 SubwaySandwichBuilder 클래스의 경우,
내부에 SubwaySandwich 타입의 인스턴스를 하나 만들어 사용자가 호출하는 메서드에 따라 인스턴스의 프로퍼티가 변경되는 로직을 갖고 있다.
클래스 내에 있는 인스턴스를 만들기 위해 사용되는 것이 build() 메서드이다. (빌더로 만들기 위해 꼭 호출해야 하는 메서드)
인스턴스의 속성(프로퍼티)을 지정하는 메서드는 with~의 이름으로 만들어져 있으며,
이는 클래스 내부에 있는 인스턴스의 프로퍼티를, 인자로 받은 값으로 대입하고, 빌더 클래스 자체를 반환하는 형태로 구성되어 있는 것을 확인할 수 있다.
class SubwaySandwichBuilder {
private var sandwich = SubwaySandwich()
func withMenu(_ menu: String) -> SubwaySandwichBuilder {
self.sandwich.menu = menu
return self
}
func withBread(_ bread: String) -> SubwaySandwichBuilder {
self.sandwich.bread = bread
return self
}
func withCheese(_ cheese: String) -> SubwaySandwichBuilder {
self.sandwich.cheese = cheese
return self
}
func withLettuce(_ lettuce: Bool) -> SubwaySandwichBuilder {
self.sandwich.lettuce = lettuce
return self
}
func withTomato(_ tomato: Bool) -> SubwaySandwichBuilder {
self.sandwich.tomato = tomato
return self
}
func withOlive(_ olive: Bool) -> SubwaySandwichBuilder {
self.sandwich.olive = olive
return self
}
func withOnion(_ onion: Bool) -> SubwaySandwichBuilder {
self.sandwich.onion = onion
return self
}
func withSauce(_ sauce: [String]) -> SubwaySandwichBuilder {
self.sandwich.sauce = sauce
return self
}
func build() -> SubwaySandwich {
return self.sandwich
}
}
위와 같은 형태로 빌더 클래스를 만들어놓게 되면 아래 왼쪽 코드와 같이 사용할 수 있게 된다.
SwiftUI를 가지고 먼저 iOS 개발을 배운 사람은 딱 체감할 수 있는 사실.
UIKit에서 Builder Pattern으로 개발을 하게 되면, SwiftUI처럼 수정자(Modifier)를 활용한 선언적 구문의 형태로 코드를 짤 수 있게 된다는 것이다.
이와 같이 .메서드(속성)의 형태로 짜여진 코드를 줄줄이 이어진 "Chaining(체이닝)의 형태로 짜여졌다"라고 표현하기도 한다.
사실, 코드가 위에 비해 조금 짧아지긴 했는데, "굳이 이 패턴을 써야 할 필요성"에 대해서는 의문이 생기는 것은 사실이다.
복잡한 빌더 클래스(Builder Class)를 정의할만큼 "유의미하게 효율성이 증가했다"라고 보기 어렵기 때문이다.
그냥 SwiftUI 스타일의 개발 방식이 익숙한 iOS 개발자들이 UIKit 방식에서도 편하게 코딩하기 위해 사용하는 건가?라고 생각이 들 무렵.
실전 코드에서는 어떻게 사용하는지를 알아보며, 해당 디자인 패턴의 유용도를 확인해보고자 한다!
3️⃣ 빌더 패턴 (Builder Pattern) 실전 코드로 적용해보기 : UIKit를 SwiftUI처럼 사용할 수 있게 된다!
아래는 UIButton 객체를 만들 때, Builder 클래스를 활용해서 생성하는 예제이다.
만약 한 프로젝트에서 사용(지정)하는 버튼의 속성이 모두 동일한 경우에는 사용하는 프로퍼티를 빌더 파일에 정의해서,
프로젝트 내에 사용하기 편하도록 아래와 같이 선언해 줄 수 있겠다.
class UIButtonBuilder {
private var title: String?
private var tintColor: UIColor?
private var backgroundColor: UIColor?
private var isEnabled: Bool?
func withTitle(_ title: String) -> UIButtonBuilder {
self.title = title
return self
}
func withTintColor(_ color: UIColor) -> UIButtonBuilder {
self.tintColor = color
return self
}
func withBackgroundColor(_ color: UIColor) -> UIButtonBuilder {
self.backgroundColor = color
return self
}
func withEnabled(_ isEnabled: Bool) -> UIButtonBuilder {
self.isEnabled = isEnabled
return self
}
func build() -> UIButton {
let button = UIButton()
button.setTitle(self.title, for: .normal)
button.tintColor = self.tintColor
button.backgroundColor = self.backgroundColor
button.isEnabled = self.isEnabled ?? false
return button
}
}
위와 같은 빌더 클래스로 버튼을 정의할 때,
매번 생성한 버튼 컴포넌트에 대해 속성을 지정하기 위해 인스턴스에 반복적으로 접근하지 않고 빌더 내에 있는 메서드로 접근할 수 있게 되는 것이다. 차이가 조금 보이는가?
4️⃣ Director를 사용해서 더 편하게 Build 해보자!
여기까지 왔는데도 불편함이 느껴진다면, Director의 사용을 추천해 본다.
*Swift에서 일반적인 Builder Pattern의 경우 Director의 사용을 권장하지 않는 것으로 보인다. -> Director의 사용이 오히려 객체를 생성하는 프로세스에 있어 더 복잡함을 유도하고, 유연성을 감소시킨다는 이유!
Director에 대한 이해를 위해 다시 서브웨이 샌드위치 예시로 돌아가보겠다.
매번 샌드위치의 메뉴를 일일이 지정하는 데 있어, 처음보다는 조금 간결해졌지만 여전히 불편함을 호소하는 사람들이 많았다고 치자.
이를 해결하기 위해 서브웨이에서는 특정한 메뉴 조합을 가진 샌드위치를 출시했다 (실제로).
사용자가 선택해야 했던 빵, 치즈, 야채, 소스 종류까지 아예 인기 있는 조합을 가지고 만들 수 있도록 구현해 놓은 것이다.
이를 어렵게 말하면, "Director는 Builder를 한번 더 감싸서 객체의 생성 과정을 추상화하는 용도로 사용된다"라고 표현한다.
코드로 살펴보면, 정말 별거 없다.
그저 비엠티 썹픽이나 스테이크&치즈 썹픽 같은 메뉴를 쉽게 사용자가 빌드(인스턴스 생성)할 수 있도록 또 다른 메서드로 묶어주는 것..?
그게 Swift에서 사용하는 Builder Pattern의 전부이다!
class Director {
private var subwaySandwichBuilder = SubwaySandwichBuilder()
func buildBMTSubPick() -> SubwaySandwich {
return subwaySandwichBuilder
.withMenu("Italian BMT")
.withBread("Pamasan Oregano")
.withCheese("Mozzarella")
.withLettuce(true)
.withTomato(true)
.withOnion(true)
.withOlive(true)
.withSauce(["Sweat Onion", "Ranch"])
.build()
}
func buildSteakCheeseSubPick() -> SubwaySandwich {
return subwaySandwichBuilder
.withMenu("Steak and Cheese")
.withBread("White")
.withCheese("American cheese")
.withLettuce(true)
.withTomato(true)
.withOnion(true)
.withOlive(true)
.withSauce(["Mayo", "SouthWest"])
.build()
}
}
그럼 아래와 같이 단순한 호출로 어느 정도 구현되어 있는 샌드위치를 만들 수 있다는 것!
let mySandwich2 = Director().buildBMTSubPick()
다시 한번 말하지만,
Swift에서의 빌더 패턴(Builder Pattern)의 사용이 또 다른 생성 과정에 있어 복잡도를 증가시키는 역효과를 가져올 수 있기에!
그 사용에 있어 (빌더 패턴이 아니더라도 다루는 모든 디자인 패턴들에게 해당!) 신중한 고민을 할 것을 꼭 당부해 본다!