
======= 추상 팩토리 패턴, 팩토리 패턴의 장점, 언제 사용하면 좋은지는 추가할 예정입니다!========
1. Factory Pattern?
객체 생성을 전담하는 struct/class를 만들어 구체적인 생성과정을 그 안에서 구현하는 패턴입니다.
왜 사용할까요?
크게 두가지 장점을 가지고 있는데요!
1. 객체의 생성을 겉으로 드러나지 않도록 캡슐화해서 사용하기 위해서
2. 객체 생성의 구체적인 구현을 generic하게 객체를 사용해 분리하기 위해서
이렇게 두가지가 있는데 잘 와닿지 않으니, 예시를 통해서 알아봅시다.
2. 햄버거로 예시를 들어보겠습니다!
아래 코드를 보면 겉으로는 큰 문제를 가지고 있지 않습니다.
하지만 예시와 함께 코드를 설명해보면 굉장히 이질감이 드는 상황이라는 것을 느끼실 수 있을거에요 같이 알아봅시다!
import Foundation
protocol Burger {
func prepare()
}
class 통새우와퍼: Burger {
var 치즈: String
var 통새우개수: Int
var 패티: String
var 양상추: Bool
var 토마토: Bool
init(치즈: String, 통새우개수: Int, 패티: String, 양상추: Bool, 토마토: Bool) {
self.치즈 = 치즈
self.통새우개수 = 통새우개수
self.패티 = 패티
self.양상추 = 양상추
self.토마토 = 토마토
}
func prepare() {
print("통새우와퍼를 만들라면 \(치즈)치즈, 통새우 \(통새우개수)개, \(패티)패티, \(양상추 ? "넣어" : "넣지마"), and \(토마토 ? "넣어" : "넣지마")가 필요해!.")
}
}
// 햄버거 주문 함수 정의
func orderBurger(type: String) {
if type == "통새우와퍼" {
let burger = 통새우와퍼(치즈: "체다", 통새우개수: 4, 패티: "소고기", 양상추: true, 토마토: true)
burger.prepare()
} else {
print("다른 종류는 없어..")
}
}
// 손님이 햄버거 주문
orderBurger(type: "통새우버거")
자 어떤 상황이냐면..
여기 햄버거를 먹으러 온 손님이 있습니다. 해당 손님은 어떤 햄버거를 먹을지 고민을 하던중 통새우와퍼를 먹어야겠다고 생각했습니다. 그리고 주문을 하려고 하는데 난데없이 직접 재료를 준비해서 알아서 만들어 먹으라고 하고 있습니다. 이게 무슨 어이없는 상황일까요?

손님은 단지 햄버거를 “주문”해서 맛있게 먹고 싶었을 뿐인데,, 직접 햄버거를 만들어야 되는 상황이 되버린거에요!
이 예시는 위에 코드처럼 팩토리 패턴을 사용하지 않은 상황과 같습니다!
뭐 그래 어찌저찌 통새우와퍼는 만들었다고 치는데 만약 여기서 더 다양한 종류의 햄버거가 늘어난다면 어떻게 될까요?

func orderBurger(type: String) {
if type == "통새우와퍼" {
let burger = CheeseBurger(cheese: "Cheddar", patty: "Beef", lettuce: true, tomato: true)
burger.prepare()
} else if type == "채소버거" {
let burger = VeggieBurger(patty: "Black Bean", lettuce: true, tomato: true)
burger.prepare()
} else if type == "치킨버거" {
let burger = ChickenBurger(patty: "Grilled Chicken", lettuce: true, tomato: true)
burger.prepare()
}
...
}
이 상황을 개발적으로 한번 해석을 해봅시다!
Factory 패턴을 활용하지 않으면 객체의 생성을 직접 관여하게 됩니다. 즉, 자신이 수행하고자 하는 목적과 맞지 않는 일을 해야되는 것이고, 객체지향의 시점에서 관심사를 분리하지 못했다고 볼 수 있습니다. 마치 소비자는 그저 햄버거를 주문해서 먹고 싶었을 뿐인데 직접 재료를 찾고 만들어야하는것처럼요.
그리고 해당 예시에서는 단순히 햄버거를 만들어서 힘들다고 표현을 했지만 캡슐화가 되어있지 않기 때문에 외부에서 해당 객체로 불필요한 접근으로 인한 문제가 발생할 수 있고, 다양한 종류가 추가될때마다 확장성이 떨어져 유지보수성도 좋지 않게 됩니다.
그럼 문제를 해결해봅시다!
그럼 이 문제를 해결하려면 어케 해야될까요?
너무나 당연하게 햄버거 가게에 햄버거를 만드는 사람이 있으면 됩니다.

햄버거를 만드는 스폰지밥을 고용함으로써 소비자는 직접 만들 필요 없이 또 어떤 재료가 들어가는지 신경쓰지 않고 원하는 햄버거를 주문해서 맛있게 먹기만 하면 됩니다. 그게 치즈버거든 불고기버거든 통새우와퍼든 상관없이요!
코드로 같이 알아볼까요?
BurgerShopAssitant라는 클래스를 두어서 각 종류에 맞게 재료를 준비하고 햄버거를 만들어주는 역할만 수행할 수 있도록 하는 것. 이것이 바로 객체 생성을 전담하는 struct/class를 만들어 구체적인 생성과정을 그 안에서 구현하는 패턴인 팩토리 패턴입니다!
class BurgerShopAssistant {
enum BurgerType {
case 통새우와퍼
case 치즈버거
case 블고기버거
}
func makeBurger(type: BurgerType) -> Burger {
switch type {
case .통새우와퍼:
return 통새우와퍼(cheese: "Cheddar", patty: "Beef", lettuce: true, tomato: true)
case .치즈버거:
return 치즈버거(patty: "Black Bean", lettuce: true, tomato: true)
case .블고기버거:
return 블고기버거(patty: "Grilled Chicken", lettuce: true, tomato: true)
}
}
}
그러면 손님은 이제 단순히 assitant(우리에겐 스폰지밥이겠죠?)에게 햄버거를 주문을 해서 맛있게 먹기만 하면 됩니다! 어때유 깔끔하죠?
// 햄버거 주문 함수 정의
func orderBurger(assistant: BurgerShopAssistant, type: BurgerShopAssistant.BurgerType) {
let burger = assistant.makeBurger(type: type)
burger.prepare()
}
// 손님이 햄버거를 주문
let 스폰지밥 = BurgerShopAssistant()
orderBurger(assistant: assistant, type: .통새우와퍼)
orderBurger(assistant: assistant, type: .치즈버거)
orderBurger(assistant: assistant, type: .불고기버거)
3. Factory Pattern 톺아보기
위에 내용들을 통해서 팩토리 패턴을 왜 사용해야되고, 어떤 패턴인지 알아보았으니 이제 자세히 개념적으로 정리를 해봅시다!
Factory Method Pattern
실제로 인스턴스를 생성하는 팩토리들에 대해 인터페이스를 제공하고,
이 인터페이스를 구현하는 하위 클래스들에 의해 인스턴스가 생성되는 디자인 패턴

- Product: 팩토리 메서드 패턴에서 생성될 객체의 공통 인터페이스 또는 추상 클래스
- Concreate Product: Product 인터페이스를 구현한 구체적인 클래스들입니다.
- Creator: 팩토리 메서드를 선언하는 클래스입니다. 보통 이 클래스는 추상 클래스로 정의되며, 하나 이상의 팩토리 메서드를 포함합니다.
- Concrete Creator: Creator 클래스를 상속받아 팩토리 메서드를 구현하는 클래스입니다. Concrete Creator는 Product 객체를 생성하는 구체적인 로직을 제공합니다.
우리가 위에서 적었던 햄버거 코드를 분리해서 살펴봅시다!
1. Product에 해당하는 인터페이스 만든다
Product는 팩토리 메서드가 생성할 객체의 종류를 정의합니다. 따라서 해당 예제에서는 Product는 Burger 프로토콜이 됩니다.
protocol Burger {
func prepare()
}
2. Product을 채택하여 Concrete Product class를 만든다.
Concrete Product 의 구체적인 인스턴스를 생성하며, 실제로 팩토리 메서드가 반환하는 객체들입니다. 이 클래스들은 Product가 제공하는 인터페이스를 구현합니다. 통새우와퍼, 치즈버거, 불고기버거 등 Burger 프로토콜을 채택해서 구체화한 클래스들이 여기에 해당되겠죠!
class 통새우와퍼: Burger {
var 치즈: String
var 통새우개수: Int
var 패티: String
var 양상추: Bool
var 토마토: Bool
init(치즈: String, 통새우개수: Int, 패티: String, 양상추: Bool, 토마토: Bool) {
self.치즈 = 치즈
self.통새우개수 = 통새우개수
self.패티 = 패티
self.양상추 = 양상추
self.토마토 = 토마토
}
func prepare() {
print("통새우와퍼를 만들라면 \(치즈)치즈, 통새우 \(통새우개수)개, \(패티)패티, \(양상추 ? "넣어" : "넣지마"), and \(토마토 ? "넣어" : "넣지마")가 필요해!.")
}
}
3. 특정 프로토콜을 채택한 객체만 반환할 수 있는 Creator 인터페이스를 만든다
Product 객체를 반환하는 팩토리 메서드를 정의합니다.
Creator 클래스 자체는 팩토리 메서드의 기본 구현을 제공할 수도 있지만, 일반적으로 구체적인 생성은
Concrete Creator클래스에서 이루어집니다.
기존 예제 코드와 다르게 Creator 부분을 분리를 해서 활용해봤습니다!
protocol BurgerCreator {
func makeBurger(type: BurgerType) -> Burger
}
enum BurgerType {
case 통새우와퍼
case 치즈버거
case 블고기버거
}
4. Creator를 채택하는 Factory class를 만든다.
팩토리 메서드를 오버라이드하여 특정 Concrete Product객체를 생성하는 책임을 갖습니다. 새로운
Concrete Product가 추가되면 새로운 Concrete Creator클래스를 추가하거나 기존 Concrete Creator 클래스를 수정할 수 있습니다.
BurgerFactory의 makeBurger 메소드를 통해서 비로소 햄버거 객체를 만들 수 있게 되었습니다!
class BurgerFactory: BurgerCreator {
func makeBurger(type: BurgerType) -> Burger {
switch type {
case .통새우와퍼:
return 통새우와퍼(cheese: "Cheddar", patty: "Beef", lettuce: true, tomato: true)
case .치즈버거:
return 치즈버거(patty: "Black Bean", lettuce: true, tomato: true)
case .블고기버거:
return 블고기버거(patty: "Grilled Chicken", lettuce: true, tomato: true)
}
}
}'🔗 Architecture & Design Pattern' 카테고리의 다른 글
| [Design Pattern] Singleton Pattern (1) | 2024.09.03 |
|---|---|
| [Design Pattern] Why Design Pattern? (0) | 2024.09.02 |