디자인패턴

Abstract Factory Pattern(추상 팩토리)

빈코더 2024. 12. 17. 20:25
728x90

Abstract Factory Pattern

소프트웨어 디자인 패턴중 하나로, 객체 생성을 위한 디자인 패턴입니다.
관련된 객체들의 팩토리를 묶어 추상화하는 역할을 합니다.
팩토리 메서드 패턴의 확장된 형태이며, 상호 관련되거나 의존적인 객체군(제품군)을 생성할 때 유용합니다.

추상 팩토리는 인터페이스를 제공하고, 이 인터페이스를 구현하는 구체적인 팩토리가 제품군을 생성합니다.
클라이언트는 구체적인 팩토리 클래스를 알 필요 없이 인터페이스를 통해 객체를 생성합니다.  

팩토리 메서드 패턴과 추상 팩토리 패턴의 차이점과 확장된 점

  1. 팩토리 메서드 패턴
    • 객체를 생성할 때 단일 제품에 대해 팩토리 메서드를 제공합니다.
    • 하나의 팩토리 메서드가 특정 제품의 객체를 생성합니다.
  2. 추상 팩토리 패턴
    • 여러 제품군에 대한 객체를 생성할 수 있는 팩토리 제공합니다.
    • 여러 개의 팩토리 메서드를 묶어서 제품군을 생성합니다.  

즉, 추상 팩토리 패턴은 팩토리 메서드 패턴을 확장하여 여러 관련된 객체(제품군)을 생성할 수 있도록 만든 패턴입니다.

예제로 확인해 봅시다.

팩토리 메서드 패턴과 추상 팩토리 패턴 비교 예제

1. 팩토리 메서드 패턴 예제
  // 1. Product Interface
  interface Chair {
      fun sitOn()
  }

  // 2. Concrete Products
  class ModernChair : Chair {
      override fun sitOn() = println("Sitting on a modern chair.")
  }

  class VictorianChair : Chair {
      override fun sitOn() = println("Sitting on a Victorian chair.")
  }

  // 3. Factory
  abstract class ChairFactory {
      abstract fun createChair(): Chair
  }

  class ModernChairFactory : ChairFactory() {
      override fun createChair(): Chair = ModernChair()
  }

  class VictorianChairFactory : ChairFactory() {
      override fun createChair(): Chair = VictorianChair()
  }

  // 4. Client
  fun main() {
      var factory: ChairFactory = ModernChairFactory()
      var chair: Chair = factory.createChair()
      chair.sitOn()

      factory = VictorianChairFactory()
      chair = factory.createChair()
      chair.sitOn()
  }

2. 추상 팩토리 패턴 예제 
  // 1. Abstract Products
  interface Chair {
      fun sitOn()
  }

  interface Table {
      fun putOn()
  }

  // 2. Concrete Products
  class ModernChair : Chair {
      override fun sitOn() = println("Sitting on a modern chair.")
  }

  class ModernTable : Table {
      override fun putOn() = println("Putting items on a modern table.")
  }

  class VictorianChair : Chair {
      override fun sitOn() = println("Sitting on a Victorian chair.")
  }

  class VictorianTable : Table {
      override fun putOn() = println("Putting items on a Victorian table.")
  }

  // 3. Abstract Factory
  interface FurnitureFactory {
      fun createChair(): Chair
      fun createTable(): Table
  }

  // 4. Concrete Factories
  class ModernFurnitureFactory : FurnitureFactory {
      override fun createChair(): Chair = ModernChair()
      override fun createTable(): Table = ModernTable()
  }

  class VictorianFurnitureFactory : FurnitureFactory {
      override fun createChair(): Chair = VictorianChair()
      override fun createTable(): Table = VictorianTable()
  }

  // 5. Client
  fun main() {
      var factory: FurnitureFactory = ModernFurnitureFactory()
      val chair = factory.createChair()
      val table = factory.createTable()

      chair.sitOn()
      table.putOn()

      factory = VictorianFurnitureFactory()
      val victorianChair = factory.createChair()
      val victorianTable = factory.createTable()

      victorianChair.sitOn()
      victorianTable.putOn()
  }

팩토리 메서드 추상 팩토리
하나의 제품군(단일 객체)을 생성하는 패턴 여러 제품군(서로 관련된 객체들)을 생성하는 패턴
하나의 팩토리 메서드만 필요 여러 팩토리 메서드를 묶어 제공
확장성이 비교적 낮음 제품군이 추가되어도 확장성이 높음

그럼 이제 다시 추상 팩토리 패턴에 대해서 알아봅시다.

추상 팩토리의 핵심은 제품 '군'집합을 타입 별로 찍어낼 수 있다는 점이 포인트입니다.
예를 들어 식빵, 단팥빵, 소보루빵 등을 묶은 빵에 대한 제품군이 있는데 이들을 또 A 빵집이냐 B 빵집이냐
빵집에 따라 여러 갈래로 나뉘게 될때, 복잡하게 묶이는 이러한 제품군들을 관리와 확장하기 용이하게 패턴화 한 것
그것이 추상 팩토리(Abstract Factory)입니다.

추상 팩토리는 아래와 같은 구성으로 구현이 됩니다.

추상 팩토리의 구성요소

위에 팩토리 메서드와 비교 했던 예제로 같이 확인해봅시다.  

1. 추상 제품(Abstract Product)

생성되는 제품들의 인터페이스 또는 추상 클래스를 정의합니다.

  // 1. Abstract Products
  interface Chair {
      fun sitOn()
  }

  interface Table {
      fun putOn()
  }

2. 구체 제품(Concrete Product)

추상 제품을 구현한 실제 제품 클래스입니다.

  class ModernChair : Chair {
      override fun sitOn() = println("Sitting on a modern chair.")
  }

  class ModernTable : Table {
      override fun putOn() = println("Putting items on a modern table.")
  }

  class VictorianChair : Chair {
      override fun sitOn() = println("Sitting on a Victorian chair.")
  }

  class VictorianTable : Table {
      override fun putOn() = println("Putting items on a Victorian table.")
  }

3. 추상 팩토리(Abstract Factory)

객체를 생성하는 인터페이스를 정의합니다.

  interface FurnitureFactory {
      fun createChair(): Chair
      fun createTable(): Table
  }

4. 구체 팩토리(Concrete Factory)

추상 팩토리를 구현하여 구체적인 제품을 생성합니다.

  class ModernFurnitureFactory : FurnitureFactory {
      override fun createChair(): Chair = ModernChair()
      override fun createTable(): Table = ModernTable()
  }

  class VictorianFurnitureFactory : FurnitureFactory {
      override fun createChair(): Chair = VictorianChair()
      override fun createTable(): Table = VictorianTable()
  }

5. 클라이언트(Client)

추상 팩토리를 사용하여 객체를 생성합니다.

  fun main() {
      var factory: FurnitureFactory = ModernFurnitureFactory()
      val chair = factory.createChair()
      val table = factory.createTable()

      chair.sitOn()
      table.putOn()

      factory = VictorianFurnitureFactory()
      val victorianChair = factory.createChair()
      val victorianTable = factory.createTable()

      victorianChair.sitOn()
      victorianTable.putOn()
  }

추상 팩토리 사용하면 좋은 상황

1. 제품군이 여러 개 존재하고, 해당 제품군이 서로 연관되어 있을때
  • 추상 팩토리를 사용하면 하나의 팩토리에서 연관된 제품들을 일관성 있게 생성할 수 있습니다.
  • 예를 들어 UI 컴포넌트를 테마에 따라 다르게 제공해야 할 때:
    1. Ligth Theme: LightButton, LightTextField
    2. Dark Theme: DarkButton, DarkTextField
2. 클라이언트 코드가 구체적인 클래스에 의존하지 않아야 할 때
  • 클라이언트 코드가 구체적인 제품 클래스 대신 추상 인터페이스만 알도록 해야 할 경우.
  • 제품 생성 로직이 변경되더라도 클라이언트 코드를 수정할 필요가 없습니다.
3. 플랫폼이나 환경에 따라 서로 다른 구현체를 제공해야 할 때
  • 예를 들어 서로 다른 운영체제(Windows, MacOS)에서 동작해야 하는 애플리케이션
    • Windows 제품군: WindowsButton, WindowsCheckbox
    • Mac 제품군: MacButton, MacCheckbox
4. 여러 제품이 함께 사용될 때 일관성을 유지해야 할 때
  • 제품군이 섞이지 않고 동일한 스타일이나 유형의 제품으로 일관되게 사용되어야 할 경우.
  • 예를 들어 한 팩토리에서 Modern 스타일 가구(ModernChair, ModernTable)를 만들고, Victorian 스타일의 가구(VictorianChair, VictorianTable)를 만드는 상황입니다.
5. 새로운 제품군을 추가해야 할 때 확장성이 중요할 때
  • 새로운 제품군을 추가할 때 기존의 코드를 변경하지 않고도 확장할 수 있습니다.
  • 예를 들어, 새로운 테마를 추가하거나 새로운 플랫폼에 대한 지원을 추가해야 할 때 추상 팩토리 패턴이 유용합니다.
6. 다양한 데이터베이스에 대해 드라이버와 쿼리 객체를 제공해야 할 때
  • 이때 추상 팩토리를 사용하면 데이터베이스를 교체할 때 클라이언트 코드 변경이 최소화 됩니다.
    • MySQL 팩토리 → MySQLDriver, MySQLQuery 생성
    • PostgreSQL 팩토리 → PostgreSQLDriver, PostgreSQLQuery 생성

추상 팩토리의 장단점

추상 팩토리의 장점
  1. 여러 제품군의 일관성을 유지
    • 관련된 객체(제품군)를 함께 생성하므로 제품 간 일관된 조합을 유지할 수 있다.
  2. 구체적인 클래스에 대한 의존성 제거
    • 클라이언트는 구체적인 클래스 대신 인터페이스나 추상 클래스를 사용합니다.
    • 새로운 구현체를 추가하거나 교체해도 클라이언트 코드는 변경되지 않습니다.
  3. 코드 확장성이 뛰어남
    • 추상 팩토리 인터페이스를 따르는 새로운 팩토리를 추가할 때 기존 코드를 수정하지 않아도 됩니다.
    • 개방-폐쇄 원칙(OCP)을 따르는 설계를 가능하게 합니다.
  4. 플랫폼 독립적인 설계
    • 다양한 플랫폼이나 환경에 맞는 객체를 제공할 때 유용합니다.
  5. 유연한 변경
    • 제품군을 선택하는 로직만 변경하면 전체 객체 생성 방식을 바꿀수 있다.
추상 팩토리의 단점
  1. 코드 복잡성 증가
    • 추상 팩토리, 구체 팩토리, 제품군 등 많은 클래스와 인터페이스가 필요합니다.
    • 작은 프로젝트나 단일 제품만 필요한 경우네는 오히려 과도한 설계과 될 수 있습니다.
    • 팩토리 클래스와 제품 클래스가 늘어나므로 코드가 방대해질수 있습니다.
    • 예: 각 제품마다 새로운 팩토리와 제품 클래스를 만들어야 함
  2. 새로운 제품군 추가 시 수정 필요
    • 새로운 제품군을 추가하려면 추상 팩토리 인터페이스에 메서드를 추가해야 합니다.
    • 이는 기존의 모든 팩토리 구현체를 수정해야 하므로 번거로울수 있습니다.
  3. 유연성이 제한됨
    • 추상 팩토리 패턴은 제품군이 고정되어 있는 경우에 적합합니다.
    • 개별 제품군에 새로운 제품만 추가하고 싶을 때는 구조가 복잡해질 수 있습니다.
    • 위 와 같은 경우 팩토리 메서드 패턴이 더 적합할 수 있습니다.
추가 설명

장점에서 코드의 확장성이 뛰어남 부분에서 '추상 팩토리는 인터페이스를 따르는 새로운 팩토리를 추가할 때 기존 코드를 수정하지 않아도 됩니다.' 라고 설명을 드렸습니다. 근데 단점에서는 '새로운 제품군을 추가시 번거롭다' 라고 적었는데 이 부분에 대해서 추가 설명을 드리려고 합니다. 위 2개의 설명을 확연히 다릅니다.
장점에서 적은 코드의 확장성이 뛰어나다는 부분을 예제로 설명해드리면 예를 들어, ModernFurnitureFactory와 VictorianFurnitureFactory가 이미 존재하는 상황에서 새로운 팩토리 ArtDecoFurnitureFactory를 추가할 경우 추상 팩토리 인터페이스는 그대로고 새로운 구체 팩토리만 추가하면 되므로 기존 코드는 수정하지 않아도 됩니다.

단점의 새로운 제품군 추가 시 수정이 필요하다는 부분은 예를 들어 Table과 Chair가 기존 제품으로 있을때 Sofa를 추가하면 전체적인 수정이 필요하다는 설명입니다.

저도 적으면서 헷갈릴거 같아서 추가적인 부가설명을 적어봤습니다.

추상 팩토리 패턴에 대해 공부하면서 느낀점

추상 팩토리 패턴이 3번째 공부하는 디자인 패턴인데 추상 팩토리 패턴을 공부하면서 느낀점은 어떤 디자인 패턴이건 상황에 맞게 사용하면 코드를 굉장히 효율적으로 작성할 수 있지만 상황이 맞지 않는데 사용하면 독이되는 것 같습니다. 예를 들어 추상팩토리 패턴으로 모든 상황을 개발하는 것보다는 추상 팩토리 패턴과 팩토리 메서드 패턴을 상황에 맞게 골고루 사용하면 더 효율적인 코드가 될 것 같다는 생각이 듭니다. 이런 상황에 맞게 코드를 작성할 수 있도록 더 많은 디자인 패턴을 공부하고 숙지하고 있어여 상황에 맞게 디자인 패턴을 적용하여 코드를 작성할수 있기 열심히 공부해야 될 것 같습니다.

728x90