팩토리 패턴(Factroy Pattern)
팩토리 패턴은 어떤 객체를 생성할 때, 그 객체를 사용하는 객체에서 직접 객체의 인스턴스를 생성하는게 아니라, 팩토리라는 객체에게 그 작업을 맡겨 의존성을 주입받는 방법이다.
팩토리패턴의 장점은 객체의 생성을 한 번 캡슐화 할 수 있다는 것이다.
그 효과로 첫 번째는 인스턴스의 생성에 비즈니스 로직이 끼어있을 때, 이 로직을 의존성을 가지는 객체로부터 분리할 수 있다는 것이고, 두 번째 효과는 다형성을 통해 언제든지 인스턴스를 생성하는 팩토리를 유연하게 변경할 수 있다는 것이다.
구체적인 객체의 생성방식에 대해 알 필요가 없어지며 객체 생성 로직의 중복을 제거하고 유지보수성을 향상시킨다.
만약 어떤 인스턴스를 만드는 과정이 바뀐다 하더라도 의존성을 가지는 개체는 수정하지 않은 채로 팩토리의 시나리오만 수정해줄 수 있다. 만약 의존성의 인스턴스를 생성하는 방법이 여러가지라고 한다면 여러종류의 팩토리를 구현해두고 필요한 팩토리를 선택해 사용할 수 있다.
Head First Design Pattern에 따르면 팩토리 패턴 자체는 디자인 패턴으로 볼 수 없고 팩토리라는객체를 사용해 코드를 정리하는 하나의 방법이라고한다.
팩토리 메서드 패턴과 추상 팩토리 패턴은 팩토리를 사용해 정의된 디자인 패턴이라고 한다.
팩토리 메서드 패턴(Factory Method Pattern)
팩토리 메서드 패턴의 핵심은 실제로 인스턴스를 생성하는 팩토리들에 대해 인터페이스(프로토콜)을 제공하고, 이 인터페이스(프로토콜)을 구현하는 하위 클래스들에 의해 인스턴스가 생성되는 디자인 패턴이다.
팩토리 메서드는 팩토리의 인터페이스에서 정의되는 인스턴스를 생성하는 메서드를 의미한다. 실제로 인스턴스를 생성하는 구체적인 팩토리들은 인터페이스의 팩토리 메서드를 구현해서 생성한다.
만약 상황에 따라 여러 타입, ConcreteProduct의 인스턴스를 생성해내고 싶다면, Product로 타입을 추상화하고, 이 인스턴스의 생성을 팩토리인 Creator에게 맡긴다.
Creator 역시도 구체 타입이 존재해 실제로 인스턴스의 생성은 Creator를 따르는 하위 계층의 클래스인 ConcreteCreator에서 진행된다. "팩토리 메서드"라는 이름이 붙은 이유는 여러 팩토리들을 하나로 추상화하고 인스턴스의 생성을 담당하는 메서드를 추상화 하는것을 주요한 전략으로 사용하기 때문이다.
예제
protocol Animal {
var name: String { get set }
func sound()
}
class Dog: Animal{
var name: String
init(name: String){
self.name = name
}
func sound() {
print("\(name) Bark🐶")
}
}
class Cat: Animal{
var name: String
init(name: String){
self.name = name
}
func sound() {
print("\(name) Meow! 😻")
}
}
이 예제에서 사용될 Concrete Product는 Dog와 Cat이다. 이 둘을 추상화하는 Product인 프로토콜은 Animal프로토콜이다.
이제 Dog나 Cat인스턴스가 필요할 때 직접 인터페이스를 만들지 않고 팩토리를 통해 인스턴스를 생성한다. 따라서 Concrete Creator는 어떤 인스턴스를 생성할지 결정하기 때문에 Dog나 Cat을 생성해서 반환하는 팩토리가 된다.
protocol AnimalFactory {
func make(with name: String) -> Animal
}
class RandomAnimalFactory: AnimalFactory{
func make(with name: String) -> Animal {
return Int.random(in: 0...1) == 0 ? Dog(name: name) : Cat(name: name)
}
}
class EvenAnimalFactory: AnimalFactory{
var previousState: Animal.Type?
func make(with name: String) -> Animal {
if previousState == Cat.self {
self.previousState = Dog.self
return Dog(name: name)
} else {
self.previousState = Cat.self
return Cat(name: name)
}
}
}
두 종류의 팩토리이다.
그냥 인스턴스를 생성하는 것이 아니라 두 팩토리 모두 인스턴스를 생성하기 위한 비즈니스 로직을 가지고있다.
이렇게 로직을 분리해낼 수 있는 것이 팩토리 메서드 패턴의 큰 장점이다. RandomAnimalFactory는 랜덤하게 강아지, 고양이를 생성하고, EvenAnimalFactroy는 이전에 생성한 타입을 기억해서 똑같은 동물이 연달아 나오지 않도록 하고있다.
class AnimalCafe{
private var animals = [Animal]()
private var factroy: AnimalFactory
init(factory: AnimalFactory){
self.factroy = factory
}
func addAnimal(with name: String){
self.animals.append(self.factroy.make(with: name))
}
func printAnimals(){
self.animals.forEach{
$0.sound()
}
}
func change(factory: AnimalFactory){
self.factroy
}
func clear(){
self.animals = []
}
}
동물카페 클래스이다. 여기서 factroy를 AnimalFactory인 프로토콜타입으로 받고있다는 점을 잘 봐야한다.
let animalCafe = AnimalCafe(factory: EvenAnimalFactory())
animalCafe.addAnimal(with: "A")
animalCafe.addAnimal(with: "B")
animalCafe.addAnimal(with: "C")
animalCafe.addAnimal(with: "D")
animalCafe.addAnimal(with: "E")
animalCafe.addAnimal(with: "F")
animalCafe.printAnimals()
animalCafe.clear()
print("\n## Change Factory ##\n")
animalCafe.change(factory: RandomAnimalFactory())
animalCafe.addAnimal(with: "A")
animalCafe.addAnimal(with: "B")
animalCafe.addAnimal(with: "C")
animalCafe.addAnimal(with: "D")
animalCafe.addAnimal(with: "E")
animalCafe.addAnimal(with: "F")
animalCafe.printAnimals()
Factory를 프로토콜 타입으로 받았기 때문에 다양한 종류의 팩토리를 주입할 수 있다.
인스턴스 생성에 대한 의존성이 없어졌기 때문에 런타임에 새로운 팩토리를 주입해서 인스턴스 생성을 위한 다른 전략을 선택할 수 있고, 만약 동물 카페에 새로운 동물이 추가되더라도 동물카페는 수정할 필요 없이 팩토리 메서드만 수정하면 동일하게 동물카페 객체를 사용할 수 있게 된다.