[디자인 패턴] Abstract Factory Pattern

전성훈·2023년 11월 2일
0

DesignPattern

목록 보기
7/18
post-thumbnail

주제: 추상화하여 생성


추상 팩토리 패턴은 생성 패턴의 한 종류로 객체의 집합을 생성할 때 유리한 패턴이다. 추상 팩토리 패턴은 앞 페이지에서 다룬 팩토리 메서드 패턴과 혼동할 수 있지만, 다른 패턴이다. 추상 팩토리 패턴은 기존에 팩토리를 한번 더 추상화 하여 서로 관련이 있는 제품군을 생성 하게 해준다.

추상 팩토리 패턴이란

  • 관련된 객체들 또는 종속적인 객체들을 생성할 때 사용하는 디자인 패턴이다. 이 패턴에서는 인터페이스를 이용해 연관된 객체들을 생성하지만, 객체 생성을 위한 구체적인 클래스 코드를 지정하지는 않는다.
  • 추상 팩토리 패턴은 실제 구현 클래스를 추상화된 인터페이스 뒤에 숨겨 놓아, 코드의 결합도를 낮추는 데 도움을 준다. 이로 인해 코드의 유연성과 재사용성이 향상되며, 시스템의 독립성과 포터빌리티를 높일 수 있다.

언제 사용할까?

  • 생성을 책임지는 구체적인 클래스를 분리시키고 싶을 때
    • 추상 팩토리 패턴은 인스턴스를 생성하는 과정을 캡슐화 한 것이다. 따라서 생성에 관한 구체적인 내용이 사용자와 분리된다.
  • 여러 제품군 중 선택을 통하여 시스템을 구성하고 제품군을 대체하고 싶을 때
    • 구체 팩토리를 추가하거나 변경을 통해 서로 다른 제품을 사용할 수 있게 할 수 있다. 추상 팩토리는 필요한 모든 것을 생성하기 때문에 전체 제품군은 한 번에 변경이 가능하다.

Factory Method vs Abstract Factory Method

언제 Factory Method 대신 Abstract Factory Method 패턴을 사용할까?

  • 제품군중 하나를 선택하여 시스템을 설정해야 하고 구성한 제품군을 다른 것으로 대체 할 수 있을 때
  • 연관되어 있는 다수의 인스턴스가 함께 사용하도록 설계하고, 이 부분에 대한 제약이 외부에서도 지켜지도록 하고 싶을 때

제품군이 필요할 때 Factory Method를 사용 했을 때 문제점

Factory Method

  • iPhone, iPad 각 기기 별로 UI를 구성하려는 상황을 가정하였을 때
    • 사용자(Factory를 사용하는 타입인 ContentUI) 입장에서는 분기 처리를 위한 ContentType을 이용하여 처리한다.
    • 사용자는 모든 UI 요소들의 Factory를 가지고 있어야 한다.
    • 결국 부품이 많아지면(Button, Label 같은 요소들이 계속 추가 된다면) 사용자 입장에서 관리하기 까다로워 진다.
    • 여기서 만약 새로운 기기 Apple Watch가 추가 된다면 어떻게 될까? 먼저 모든 Factor에 Apple Watch에 관한 분기 처리를 추가해줘야 한다. 그리고 Factory 쪽 코드 뿐만 아니라 사용자도 Apple Watch를 생성하는 경우 내부 코드 변경이 필요하다.
    • 따라서 연관이 있는 여러 객체를 묶어서 생성하는 경우에 Abstract Factory 패턴이 Factory 패턴에 비해 유리할 수 있다.

Abstract Factory Method

Abstract Factory Method

  • Abstract Factory
    • UIFactoryable: Factory 추상화 타입
    • 이 인터페이스는 제품을 생성하는 데 필요한 메서드들을 선언한다. 일반적으로 이 인터페이스는 여러 종류의 제품을 생성하는 메서드들로 이루어져 있다.
  • Concrete Factory
    • IPhoneFactory, IPadFactory: 각 연관이 있는 인스턴스 집합을 생성할 구체 Factory 타입
    • AbstractFactory 인터페이스를 구현하는 클래스들이다. 이 클래스들은 실제 제품 객체를 생성하는 작업을 수행한다. 각 Concrete Factory는 특정 제품군을 만드는 데 특화되어 있다.
  • Abstract Product
    • Buttonable, Labelable: 생성되는 인스터스를 추상화한 타입
    • 제품군을 속하는 개별 제품들의 공통 특성을 정의하는 인터페이스이다.
  • Concrete Product
    • IPhoneButton, IPadLabel...: 최종적으로 생성되는 구체적인 타입
    • AbstractProduct 인터페이스를 사용하여 필요한 제품을 생성하는 클래스이다. Client는 ConcreteFactory나 ConcreteProduct 클래스를 직접 참조하지 않고, 대신 AbstractFactory를 통해 제품을 생성하므로 시스템이 유연해진다.
  • Client

장단점 및 사용 예시

장점 및 단점

  • Abstract Factory Method 패턴은 Factor가 추가되고 기존에 존재하는 Product로 Factory를 구성 할때는 매우 효과적인 패턴이 될 수 있다.
  • 하지만 새로운 종류의 Product가 추가되면 각각의 Factory에도 추가해줘야 하는 경우가 생긴다. Product의 추가나 변동이 잦아진다면 모든 Factory에 변동이 생길 위험이 있다.
  1. 복잡성 증가
    • 팩토리 클래스, 추상 제품 클래스, 구체적인 제품 클래스 등 다양한 클래스들을 관리해야하므로, 코드가 복잡해질 수 있다. 이는 코드의 이해와 유지 관리를 어렵게 만든다.
  2. 확장성 제한
    • 새로운 종류의 제품을 추가하려면 모든 팩토리에 새로운 생성 메서드를 추가해야한다. 이는 Open-Closed Principle(OCP, 개방-폐쇄 원칙)을 위반하게 된다. OCP는 "클래스는 확장에는 열려 있어야 하고, 변경에는 닫혀 있어야 한다"는 원칙이다.
  3. 제품 패밀리 강제
    • 추상 팩토리 패턴을 사용하면, 일관성이 보장되지 않은 제품의 조합을 사용하는 것을 방지할 수 있지만, 반대로 항상 특정 제품 패밀리를 사용하도록 강제하게 된다. 이는 때때로 불필요한 제한을 가할 수 있다.
  • 추상 팩토리 패턴은 이러한 문제점들에도 불구하고, 특정 문제에 대한 효과적인 해결책이 될 수 있다. 따라서 이 패턴을 사용하는 것은 특정 상황과 요구 사항에 따라 결정해야 한다.

추상 팩토리 패턴 적용 상황

  1. UI 테마 변경
    • 사용자가 앱의 테마를 변경할 수 있게 하려는 경우, 각 테마별로 다른 팩토리를 제공하고, 이 팩토리를 통해 UI 컴포넌트를 생성할 수 있다.
  2. 플랫폼별 처리
    • 앱이 여러 플랫폼에서 동작해야 하는 경우, 각 플랫폼별로 다른 팩토리를 제공하고, 이 팩토리를 통해 플랫폼 특정 기능을 생성 및 처리할 수 있다.
  3. 데이터 소스 변경
    • 앱이 다양한 데이터 소스에서 데이터를 가져오는 경우, 각 데이터 소스별로 다른 팩토리를 제공하고, 이 팩토리를 통해 데이터를 가져오는 방법을 변경할 수 있다.
    • 데이터 소스가 변경되었다는 말은, 앱이 데이터를 가져오는 방법이나 위치가 바뀌었다는 것을 의미하며, 앱이 처음에는 네트워크 API를 통해 데이터를 가져왔는데, 나중에는 로컬 데이터베이스에서 데이터를 가져오기로 변경되었다면, 이는 앱의 데이터 소스가 변경되었다고 말할 수 있다.
    • 이렇게 데이터 소스가 변경될 때마다, 앱의 모든 부분을 수정해야 하는 것은 매우 번거로울 수 있다. 그러나 추상 팩토리 패턴을 사용하면, 앱의 데이터 가져오기 로직을 캡슐화하여 이러한 변경을 더 쉽게 관리할 수 있다. 따라서 데이터 소스가 변경되더라도 앱의 나머지 부분에는 영향을 미치지 않는다.

예시 코드

생성을 분리할때

import Foundation

protocol Animal {
    func name() -> String
}


protocol Sound {
    func noise() -> String
}

class Dog: Animal {
    func name() -> String {
        return "Dog"
    }
}

class Cat: Animal {
    func name() -> String {
        return "Cat"
    }
}

class Bark: Sound {
    func noise() -> String {
        return "Bark"
    }
}

class Meow: Sound {
    func noise() -> String {
        return "Meow"
    }
}


protocol AnimalFactory {
    func createAnimal() -> Animal
    func createSound() -> Sound
}

class DogFactory: AnimalFactory {
    func createAnimal() -> Animal {
        return Dog()
    }
    
    func createSound() -> Sound {
        return Bark()
    }
}

class CatFactory: AnimalFactory {
    func createAnimal() -> Animal {
        return Cat()
    }
    
    func createSound() -> Sound {
        return Meow()
    }
}

let dogFactory = DogFactory()

let dog = dogFactory.createAnimal()
let bark = dogFactory.createSound()


let catFactory = CatFactory()
let cat = catFactory.createAnimal()
let meow = catFactory.createSound()

여러 제품군 중 선택을 할 때

  • 생성을 담당할 Factory 구현
// 추상화된 Factory
protocol UIFactoryable {
    func createButton() -> Buttonable
    func createLabel() -> Labelable
}

// 연관된 제품군을 실제로 생성하는 구체 Factory
final class IPadUIFactory: UIFactoryable {
    func createButton() -> Buttonable {
        return IPadButton()
    }
    
    func createLabel() -> Labelable {
        return IPadLabel()
    }
}

final class IPhoneUIFactory: UIFactoryable {
    func createButton() -> Buttonable {
        return IPhoneButton()
    }
    
    func createLabel() -> Labelable {
        return IPhoneLabel()
    }
}
  • 생설될 Product 구현
// 추상화된 Product
protocol Buttonable {
    func touchUp()
}

protocol Labelable {
    var title: String { get }
}
// 실제로 생성될 구체 Product, 객체가 가질 기능과 상태를 구현
final class IPhoneButton: Buttonable {
    func touchUp() {
        print("IPhone Button")
    }
}

final class IPadButton: Buttonable {
    func touchUp() {
        print("IPad Button")
    }
}

final class IPhoneLabel: Labelable {
    var title: String = "iPhoneLabel"
}

final class IPadLabel: Labelable {
    var title: String = "iPadLabel"
}
  • 사용 부분 ViewController, UIContent
class ViewController: UIViewController {

        //UI를 가지고 있는 인스턴스 기기별로 설정
    var iPadUIContent = UIContent(uiFactory: IPadUIFactory())
    var iPhoneUIContent = UIContent()

    override func viewDidLoad() {
        super.viewDidLoad()
        touchUpButton()
        printLabelTitle()
    }

    func touchUpButton() {
        iPadUIContent.button?.touchUp()
        iPhoneUIContent.button?.touchUp()
    }

    func printLabelTitle() {
        print(iPadUIContent.label?.title ?? "")
        print(iPhoneUIContent.label?.title ?? "")
    }
}

// Factory를 통해 UI를 만들고 가지고 있는 Class
class UIContent {
    var uiFactory: UIFactoryable
    var label: Labelable?
    var button: Buttonable?
    
    
    init(uiFactory: UIFactoryable = IPhoneUIFactory()) {
        self.uiFactory = uiFactory
        setUpUI()
    }
    
    func setUpUI() {
        label = uiFactory.createLabel()
        button = uiFactory.createButton()
    }
}

테마 변경 예시 코드

// 추상 팩토리 
protocol ThemeFactory { 
	func createBackgroundColor() -> UIColor
	func createTextColor() -> UIColor
}

// 라이트 테마 팩토리
class LightThemeFactory: ThemeFactory { 
	func createBackgroundColor() -> UIColor { 
		return UIColor.white
	}
	
	func createTextColor() -> UIColor {
		return UIColor.black
	}
}

// 다크 테마 팩토리
class DarkThemeFactory: ThemeFactory { 
	func createBackgroundColor() -> UIColor { 
		return UIColor.black
	}
	
	func createTextColor() -> UIColor { 
		return UIColor.white
	}
}

// ViewController
class ViewController: UIViewController { 
	private var themeFactory: ThemeFactory!
	
	// viewController 초기화 시 테마 팩토리 전달 
	init(themeFactory: ThemeFactory) { 
		super.init(nibName: nil, bundle: nil)
		self.themeFactory = themeFactory
	}
	
	required init?(coder: NSCoder) {
		fatalError("init(coder:) has not been implemented")
	 }
	 
	 override func viewDidLoad() {
		 super.viewDidLoad()
		 
		 self.view.backgroundColor = themeFactory.createBackgroundColor()
		 
		 let label = UILabel()
		 label.textColor = themeFactory.createTextColor()
		 self.view.addSubView(label)
	 }
}

데이터 소스 변경 예시 코드

  • 아래 예제는 앱이 두 가지 타입의 데이터 소스, 즉 NetworkDatabase에서 데이터를 가져오는 경우를 보여진다.
  • 먼저, 데이터 소스에서 데이터를 가져오는 방법을 정의해야 한다. 이를 위해 DataSourceFactory 라는 프로토콜을 정의하고, createProductListDataSource()createProductDetailSource() 두 가지 메소드를 추가한다.
protocol DataSourceFactory { 
	func createProductListDataSource() -> ProductListDataSource
	func createProductDetailDataSource() -> ProductDetailDataSource
}

protocol ProductListDataSource { 
	func fetchProductList(completion: @escaping ([Product])-> Void)
}

protocol ProductDetailDataSource { 
	func fetchProductDetail(for productId: String, completion: @escaping (ProductDetail) -> Void)
}

// 네트워크와 데이터베이스에 대한 구체적인 팩토리
class NetworkDataSourceFactory: DataSourceFactory { 
	func createProductListDataSource() -> ProductListDataSource { 
		return NetworkProductListDataSource()
	}
	func createProductDetailDataSource() -> ProductDetailDataSource { 
		return NetworkProductDetailDataSource()
	}
}

class NetworkProductListDataSource: ProductListDataSource { 
	func fetchProductList(completion: @escaping ([Product]) -> Void) {
		 // Network request and parse data
	 }
}

class NetworkProductDetailDataSource: ProductDetailDataSource {
    func fetchProductDetail(for productId: String, completion: @escaping (ProductDetail) -> Void) {
        // Network request and parse data
    }
}

class DatabaseDataSourceFactory: DataSourceFactory {
    func createProductListDataSource() -> ProductListDataSource {
        return DatabaseProductListDataSource()
    }
    
    func createProductDetailDataSource() -> ProductDetailDataSource {
        return DatabaseProductDetailDataSource()
    }
}

class DatabaseProductListDataSource: ProductListDataSource {
    func fetchProductList(completion: @escaping ([Product]) -> Void) {
        // Fetch data from database
    }
}

class DatabaseProductDetailDataSource: ProductDetailDataSource {
    func fetchProductDetail(for productId: String, completion: @escaping (ProductDetail) -> Void) {
        // Fetch data from database
    }
}

class ViewController: UIViewController {
    private var dataSourceFactory: DataSourceFactory!
    
    // ViewController 초기화 시 DataSourceFactory 전달
    init(dataSourceFactory: DataSourceFactory) {
        super.init(nibName: nil, bundle: nil)
        self.dataSourceFactory = dataSourceFactory
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        
        let productListDataSource = dataSourceFactory.createProductListDataSource()
        productListDataSource.fetchProductList { productList in
            // Update UI
        }
        
        let productDetailDataSource = dataSourceFactory.createProductDetailDataSource()
        productDetailDataSource.fetchProductDetail(for: "productId") { productDetail in
            // Update UI
        }
    }
}

출처(참고문헌)

제가 학습한 내용을 요약하여 정리한 것입니다. 내용에 오류가 있을 수 있으며, 어떠한 피드백도 감사히 받겠습니다.

감사합니다.

0개의 댓글