본문 바로가기
Engineering/SW Design

[디자인 패턴] 생성패턴 - 추상 팩토리

by 쿨쥰 2023. 5. 24.

생성패턴 - 추상 팩토리


이전 글 : 생성패턴 - 팩토리 메서드

2023.05.22 - [Engineering/SW Design] - [디자인 패턴] 생성패턴 - 팩토리 메서드

 

[디자인 패턴] 생성패턴 - 팩토리 메서드

생성패턴 - 팩토리 메서드 이전 글 : 디자인 패턴 개요 2023.05.19 - [Engineering/SW Design] - 디자인 패턴 개요 디자인 패턴 개요 디자인 패턴 이란? 디자인 패턴 SW 설계 과정에서 자주 발생하는 문제들에

skidrow6122.tistory.com

 

 

요약

구체적인 클래스에 의존하지 않고 서로 연관되거나 의존적인 객체의 조합을 만드는 인터페이스를 제공하는 생성패턴이다.

즉, 관련성 있는 여러 종류의 객체를 일관된 방식으로 생성하는 경우에 유용하다.

내부적으로 싱글턴 패턴과, 팩토리 메서드 패턴을 사용한다.

이름만 봐서는 팩토리 메서드 패턴과 비슷해보이지만, 명확한 차이점이 있다.

  • 팩토리 메서드 패턴
    • 조건에 따른 객체 생성을 팩토리 클래스로 위임, 팩토리 클래스에서 객체를 생성
  • 추상 팩토리 패턴
    • 서로 관련있는 객체들을 통째로 묶어서 팩토리 클래스로 만들고, 이들 팩토리를 조건에 따라 생성하도록 다시 팩토리를 만들어서 객체를 생성

따라서, 추상 팩토리 패턴은 어떻게 보면, 팩토리 메서드 패턴을 좀 더 캡슐화한 개념이라고 보면 된다.

 

키워드 : 제품 패밀리 / 제품 / 객체 조합 묶음 / abstract factory / concrete factory / abstract product / concrete product

 

 

 

문제

가구 판매 프로그램을 만들고 있다고 가정해보자.

판매 제품들은 몇가지 제품들이 모여서 하나의 제품 패밀리로서 판매 된다.

하나의 제품 패밀리는 chair + sofa + table 총 3가지의 제품들이 모여 있는 그룹이며,

제품 패밀리는 각 구성 제품들의 스타일에 따라 현대식, 빅토리안, 아르데코 같은 변형 양식으로 제공된다.

따라서 개별 가구 객체 생성시에는 이 객체들이 기존의 같은 패밀리 내에 있는 다른 가구 객체들과 일치하는 스타일을 가지도록 할 방법이 필요하다.

또한, 가구 공급 업체들은 카탈로그를 매우 자주 변경하므로, 새로운 제품 또는 제품 패밀리를 추가 할 때마다 기존 코드를 많이 변경하는 번거로움을 피해야 한다.

제품 패밀리와 그들의 변형들

 

 

해결책

[첫번째 인터페이스 그룹]

첫째로 각 제품에 대한 개별적인 인터페이스를 명시적으로 선언한다.

예를들어, chair 라는 제품이라면, chair 가 가질 수 있는 모든 변형이 해당 인터페이스를 따르도록 하며, 이는 모든 의자의 변형들은 곧 chair 인터페이스를 구현해야 한다는 것을 의미한다.

같은 객체의 모든 변형은 단일 클래스 계층 구조로 옮겨짐

[두번째 인터페이스 그룹]

다음 단계는 바로 본 포스팅의 핵심인 추상 팩토리 패턴을 선언 하는 것이다.

추상 팩토리 패턴은 제품 패밀리 내의 모든 개별 제품들의 생성 메서드들이 목록화 되어있는 인터페이스이다.

예를들어, 제품 패밀리 안에 chair, sofa, table 3종의 제품이 속한다면, createChair():Chair , createSofa():Sofa, createTable():Table 따위와 같이 클래스를 생성해주는 메서드들을 포함한다.

추상 팩토리를 구현한 각 구상 팩토리는 특정 제품 패밀리의 변형에 해당

 

 

구조

 

 

 

예제 클래스 다이어그램

요즘 스마트 디바이스 시장을 보면 다수업체가 정리되고, 삼성제품을 즐겨 쓰는 사람들과 애플제품을 즐겨쓰는 사람으로 양분화 된듯하다.

스마트 디바이스 총판 커머스 몰을 가정해보자.

고객의 선호 제조사만 알고 있다면, 몰에서 자동으로 스마트폰 + 워치 + 태블릿 까지 3종 세트로 패밀리 제품을 제공해준다고 치자.

 

  • PackageFactory 클래스는 Creator로서 동작하며, 스마트폰, 태블릿 웨어러벌 객체를 생성하는 팩토리 클래스를 추상화 한다.
  • PackageServce 라는 클라이언트 코드에서는 필요한 객체를 직접 생성하지 않고 팩토리를 통해서 생성한다.
  • 스마트폰, 태블릿, 웨어러블 각 제품은 각각의 인터페이스를 가지고 있고, 각 제품 별 변형은 구현체에서 구체화 된다.

 

 

Hands on

interface PackageFactory {
    fun getSmartphone(): Smartphone
    fun getTablet(): Tablet
    fun getWearable(): Wearable
}
// 팩토리 클래스를 싱글톤으로
object SamsungUserFactory : PackageFactory {
    override fun getSmartphone(): Smartphone {
        val phone = Galaxy()
        return phone
    }
    override fun getTablet(): Tablet {
        val tab = GalaxyTab()
        return tab
    }
    override fun getWearable(): Wearable {
        val watch = GalaxyWatch()
        return watch
    }
}

object AppleUserFactory : PackageFactory {
    override fun getSmartphone(): Smartphone {
        val phone = Iphone()
        return phone
    }
    override fun getTablet(): Tablet {
        val tab = Ipad()
        return tab
    }
    override fun getWearable(): Wearable {
        val watch = AppleWatch()
        return watch
    }
}
  • PackageFactory 클래스를 AbstractFactory 인 추상 팩토리라고 하고 여기가 실제 팩토리 클래스의 공통 인터페이스이다.
  • xxxxxUserFactory 클래스들이 ConcreteFactory 이며 구체적인 팩토리 클래스이다. 추상 메서드를 오버라이드함으로서 구체적인 제품을 생성한다.
  • 구체 팩토리 클래스는 주로 싱글턴 패턴이 적용된다.
interface Smartphone {
    fun getName(id: String): String
    fun getModelCode(id: String): String
}

class Galaxy : Smartphone {
    override fun getName(id: String): String {
        TODO("Not yet implemented")
        return "samsung galaxy"
    }
    override fun getModelCode(id: String): String {
        var modelCode = "SM-793G_XDkdsnc32ds"
        TODO("Not yet implemented")
        return modelCode
    }

    fun getSamsungSpec(): String {
        return ""
    }
}

class Iphone : Smartphone {
    override fun getName(id: String): String {
        TODO("Not yet implemented")
        return "apple iphone"
    }
    override fun getModelCode(id: String): String {
        var modelCode = "MT9MEKH/A"
        TODO("Not yet implemented")
        return modelCode
    }
}
  • AbstractProduct 는 Smartphones.kt 로서 제품의 공통 인터페이스이다.
  • ConcreteProduct 는 Galaxy.kt 와 Iphone.kt 로서 구체적인 팩토리 클래스에서 생성되는 구체적인 제품이다.

 

interface Tablet {
    fun getName(id: String): String
    fun getCloud(id: String): String
}

class GalaxyTab : Tablet {
    override fun getName(id: String): String {
        TODO("Not yet implemented")
        return "samsung galaxy tab series"
    }
    override fun getCloud(id: String): String {
        var cloudPlatform = "S-cloud"
        TODO("Not yet implemented")
        return cloudPlatform
    }
}

class Ipad : Tablet {
    override fun getName(id: String): String {
        TODO("Not yet implemented")
        return "apple Ipad PRO"
    }
    override fun getCloud(id: String): String {
        var cloudPlatform = "apple ICloud"
        TODO("Not yet implemented")
        return cloudPlatform
    }
}
  • 앞선 Smartphones 제품 예시와 동일하게 제품 패밀리를 구성하는 제품 인터페이스와 그에 대한 변형을 구체화 할 수 있는 구상체로 오버라이드 되었다.

 

class PackageService(private val packageFactory: PackageFactory) {
    fun combine(id: String): String {
        val smartphone = packageFactory.getSmartphone()
        val tablet = packageFactory.getTablet()
        val wearable = packageFactory.getWearable()
        return smartphone.getName(id) + tablet.getName(id) + wearable.getName(id)
    }
}

fun main() {
    val samsungUser = PackageService(SamsungUserFactory)
    val sId = "123"
    val appleUser = PackageService(AppleUserFactory)
    val appleId = "ABC"

    println("삼성유저는 " + samsungUser.combine(sId))
    println("애플유저는 " + appleUser.combine(appleId))
}
  • Client 코드에서는 필요한 객체를 직접 생성하지 않고, 팩토리를 통하여 생성한다.
  • 그 결과로, client는 package 타입별 객체 생성이 분기 처리 없이 일관성있게 사용 가능한 형태로 변경되었다.

 

 

핵심

  • 제품군의 다양한 패밀리들과 함께 조합으로 작동해야 하지만, 해당 제품들의 구상 클래스에 의존하고 싶지 않을때 사용함
  • 코드에 클래스가 있고, 이 클래스의 팩토리 메서드들의 집합의 기본 책임이 뚜렷하지 않을때 추상 팩토리 구현을 고려함
    • SRP 원칙에 의해 하나의 클래스는 하나의 책임만 가져야 하지만, 클래스가 여러 제품 유형을 상대할 경우 클래스의 팩토리 메서드들을 완전한 추상 팩토리 구현함
  • 구상 제품들과 클라이언트 코드 사이의 결합을 피할 수 있음
  • 추상 팩토리 클래스들은 팩토리 메서드들의 집합을 기반으로 하는 경우가 많으며, 하위 시스템 객체들이 클라이언트 코드에서 생성되는 방식만 숨기고 싶을때 퍼사드 대신 사용할 수 있음.

 

 

패턴 접근 과정

  1. 고유한 Product 유형들 과 이에 대한  변형 Product들을 나타내는 매트릭스를 매핑

  2. 모든 Product 변형들에 대한 추상 Product 인터페이스들을 선언

  3.  그 후 모든 Concrete Product 클래스들이 위 인터페이스들을 구현하도록 구현

  4. Abastract  Factory 인터페이스를 모든 abstract product들에 대한 생성 메서드들의 집합과 함께 선언

  5. 각 Product 변형에 대해 각각 하나의 Concrete Factory 클래스 집합을 구현

  6. 앱 어딘가에 factory 초기화 코드를 생성 
    - 초기화 코드는 현재 환경에 따라 Concrete Factory 클래스 중 하나를 인스턴스화 해야 함
    - 이 factory 객체를 product를 생성하는 모든 클래스들에 전달

  7. 코드를 검토해서 Product 생성자에 대한 모든 직접 호출을 탐색
    - 이 호출들을 factory 객체에 대한 적절한 생성 메서드에 대한 호출들로 교체

 


간단히 hands on 을 통해 추상 팩토리 패턴을 구현 해 보았다.

기본적으로 interface 를 활용하여 아키텍처 경계를 긋는 방식은 팩토리 메서드와 비슷하지만, 여러 제품군에 대한 조합이 발생할때 이 패턴을 쓰면 유용할 것 같다.

실제 상품과 상품 하위 옵션들의 묶음으로 서비스를 제공할 필요가 있을때 이 패턴을 뼈대로 잡으면 단단한 아키텍처를 달성할 수 있다.

 

댓글