애플사이다의 iOS 개발 일지

[디자인 패턴] Abstract Factory - 미리 정해둔 종류로만 객체를 생성할 때 본문

프로그래밍 철학

[디자인 패턴] Abstract Factory - 미리 정해둔 종류로만 객체를 생성할 때

Applecider 2023. 1. 16. 07:00

GoF 디자인 패턴 중 하나인 Abstract Factory 패턴을 정리했다.

이 패턴은 예시코드부터 보는 것을 추천한다.

- Ref : 도서 <Dive into Design Patterns>, Alexander Shvets 저


문제 상황

  • 가구 판매 앱 개발과정에서 세트 제품군 (a family of reloated products)이 있다. (ex. 의자, 소파, 커피테이블)
    또한 해당 세트 제품군에는 여러 스타일의 변형 (variants)이 있다. (ex. 아르데코, 빅토리안, 현대식)
  • 고객이 가구를 주문하면, 동일한 스타일로 가구세트를 통일해야 하는 상황이다.
    또한 새로운 제품 (새로운 스타일)이 자주 추가되므로 매번 기존 코드를 수정하는 번거로움을 피하고 싶다.

패턴 설명

 

이 패턴은 말로 설명하면 복잡한데, 예시를 보면 매우 쉽다. 😞

  • 요약 : Concrete 클래스가 없어도 특정 객체를 생성할 때 항상 미리 지정해 둔 종류로 생성하는 패턴이다.
  • 먼저 세트 제품군 종류별 프로토콜을 생성한다. (ex. 의자 프로토콜)
    이후 각 스타일의 제품이 위 프로토콜을 채택하도록 한다. (ex. 빅토리안 의자, 모던 의자 등)
  • 그 다음 Abstract Factory 프로토콜을 선언하고, (ex. FurnitureFactory 프로토콜)
    이후 각 스타일마다 위 프로토콜을 채택하도록 한다. (ex. 빅토리안 FurnitureFactory, 모던 FurnitureFactory 등)
  • 클라이언트는 의자 프로토콜을 사용하여 모든 의자를 항상 동일한 방식으로 주문하게 된다.
    (= 의자를 주문하기만 하면 항상 정해진 스타일로 온다는 뜻❗️
  • Abstract Factory 패턴은 Factory Method 패턴의 집합을 기반으로 한다.

Abstract Factory 구조도

예시 코드

핵심은 초기화 시 스타일을 지정하면

-> 해당 스타일로 모든 제품군을 생성한다는 것이다.

// MARK: - 1️⃣ Chair
protocol Chair {
    func hasLegs()
    func sitOn()
}
class VictoryianChair: Chair {
    func hasLegs() { }
    func sitOn() { }
}
class ModernChair: Chair {
    func hasLegs() { }
    func sitOn() { }
}

// MARK: - Sofa
protocol Sofa { }  // 생략
class VictoryianSofa: Sofa { }
class ModernSofa: Sofa { }

// MARK: - CoffeeTable
protocol CoffeeTable { }  // 생략
class VictoryianCoffeeTable: CoffeeTable { }
class ModernCoffeeTable: CoffeeTable { }

// MARK: - 2️⃣ FurnitureFactory
protocol FurnitureFactory {
    func createChair() -> Chair
    func createSofa() -> Sofa
    func createCoffeeTable() -> CoffeeTable
}
class VictorianFurnitureFactory: FurnitureFactory {
    func createChair() -> Chair {
        print("VictoryianChair")
        return VictoryianChair()
    }
    func createSofa() -> Sofa {
        print("VictoryianSofa")
        return VictoryianSofa()
    }
    func createCoffeeTable() -> CoffeeTable {
        print("VictoryianCoffeeTable")
        return VictoryianCoffeeTable()
    }
}
class ModernFurnitureFactory: FurnitureFactory {
    func createChair() -> Chair {
        print("ModernChair")
        return ModernChair()
    }
    func createSofa() -> Sofa {
        print("ModernSofa")
        return ModernSofa()
    }
    func createCoffeeTable() -> CoffeeTable {
        print("ModernCoffeeTable")
        return ModernCoffeeTable()
    }
}

// MARK: - ✅ Client
class House {
    let factory: FurnitureFactory
    
    init(factory: FurnitureFactory) {  // 1. 초기화 시 스타일을 결정하면
        self.factory = factory
    }
    
    func service() {  // 2. 해당 스타일로 모든 세트 제품을 생성함 ❗️
        let chair = factory.createChair()
        let sofa = factory.createSofa()
        let coffeeTable = factory.createCoffeeTable()
    }
}

let victorianFurnitureFactory = VictorianFurnitureFactory()
let victorianHouse = House(factory: victorianFurnitureFactory)
victorianHouse.service()
//VictoryianChair
//VictoryianSofa
//VictoryianCoffeeTable

let modernFurnitureFactory = ModernFurnitureFactory()
let modernHouse = House(factory: modernFurnitureFactory)
modernHouse.service()
//ModernChair
//ModernSofa
//ModernCoffeeTable

활용하는 상황을 상상해보면 아래와 같다.

switch style {
case .victorian:
    let victorianFurnitureFactory = VictorianFurnitureFactory()
    let victorianHouse = House(factory: victorianFurnitureFactory)
    victorianHouse.service()
case .modern:
    let modernFurnitureFactory = ModernFurnitureFactory()
    let modernHouse = House(factory: modernFurnitureFactory)
    modernHouse.service()
// ...
}

장점

  • 항상 특정한 종류의 제품만 생성하는 것을 보장할 수 있다.
  • Concrete Product와 클라이언트 간의 결합도를 낮춘다.
  • Product 초기화 코드를 특정 객체가 전담하므로 단일책임 원칙을 준수한다.
  • 새로운 제품 (스타일)이 추가할 때 기존 클라이언트를 훼손하지 않으므로 개방폐쇄 원칙을 준수한다.

단점

  • 새로운 프로토콜과 클래스를 추가해야 하므로 코드의 복잡도가 높아진다.

 

- Reference

 

🍎 포스트가 도움이 되었다면, 공감🤍 / 구독🍹 / 공유🔗 / 댓글✏️ 로 응원해주세요. 감사합니다.

 

 

Comments