[DesignPattern] Abstract Factory Pattern

suhan0304·2024년 9월 13일

Design Pattern

목록 보기
7/16
post-thumbnail

Abstract Factory Pattern

추상 팩토리 패턴은 연관성이 있는 객체 군이 여러개 있을 경우 이들을 묶어 추상화하고, 어떤 구체적인 상황이 주어지면 팩토리 객체에서 집합으로 묶은 객체 군을 구현화하는 생성 패턴이다. 유저가 특정 객체를 사용할 때 팩토리 클래스만을 참조하여 특정 객체에 대한 구현부를 감추어 역할과 구현을 분리시킬 수 있다.

즉, 추상 팩토리의 핵심은 객체 '군'집합을 타입 별로 찍어낼 수 있다는 점이 포인트이다. 예를 들어 모니터, 마우스, 키보드를 묶은 전자 제품 군이 있는데 이들을 또 삼성 제품군이냐 애플 제품군이냐 로지텍 제품군이냐에 따라 집합이 브랜드 명으로 여러 갈래 나뉘게 될 때, 복잡하게 묶이는 이러한 제품군들을 관리와 확장하기 용이하게 패턴화 한것이 추상 팩토리이다.


Structure

  1. AbstractFactory : 최상위 공장 클래스. 여러개의 제품들을 생성하는 여러 메소드들을 추상화 한다.
  2. ConcreteFactory : 서브 공장 클래스들은 타입에 맞는 제품 객체를 반환하도록 메소드들을 재정의한다.
  3. AbstractProduct : 각 타입의 제품들을 추상화한 인터페이스
  4. ConcreteProduct (ProductA ~ ProductB) : 각 타입의 제품 구현체들. 이들은 팩토리 객체로부터 생성된다. 
  5. Client : Client는 추상화된 인터페이스만을 이용하여 제품을 받기 때문에, 구체적인 제품, 공장에 대해서는 모른다.

How

Product Class (instance)

// Product A 제품군
interface AbstractProductA {
}

// Product A - 1
class ConcreteProductA1 implements AbstractProductA {
}

// Product A - 2
class ConcreteProductA2 implements AbstractProductA {
}
// Product B 제품군
interface AbstractProductB {
}

// Product B - 1
class ConcreteProductB1 implements AbstractProductB {
}

// Product B - 2
class ConcreteProductB2 implements AbstractProductB {
}

Factory Class

interface AbstractFactory {
    AbstractProductA createProductA();
    AbstractProductB createProductB();
}

// Product A1와 B1 제품군을 생산하는 공장군 1 
class ConcreteFactory1 implements AbstractFactory {
    public AbstractProductA createProductA() {
        return new ConcreteProductA1();
    }
    public AbstractProductB createProductB() {
        return new ConcreteProductB1();
    }
}

// Product A2와 B2 제품군을 생산하는 공장군 2
class ConcreteFactory2 implements AbstractFactory {
    public AbstractProductA createProductA() {
        return new ConcreteProductA2();
    }
    public AbstractProductB createProductB() {
        return new ConcreteProductB2();
    }
}

class Client {
    public static void main(String[] args) {
    	AbstractFactory factory = null;
        
        // 1. 공장군 1을 가동시킨다.
        factory = new ConcreteFactory1();

        // 2. 공장군 1을 통해 제품군 A1를 생성하도록 한다 (클라이언트는 구체적인 구현은 모르고 인터페이스에 의존한다)
        AbstractProductA product_A1 = factory.createProductA();
        System.out.println(product_A1.getClass().getName()); // ConcreteProductA1

        // 3. 공장군 2를 가동시킨다.
        factory = new ConcreteFactory2();

        // 4. 공장군 2를 통해 제품군 A2를 생성하도록 한다 (클라이언트는 구체적인 구현은 모르고 인터페이스에 의존한다)
        AbstractProductA product_A2 = factory.createProductA();
        System.out.println(product_A2.getClass().getName()); // ConcreteProductA2
    }
}

코드를 보면 똑같은 createProductA() 메서드를 호출하지만 어떤 팩토리 객체이냐에 따라 반환되는 제품군이 다르게 된다.


When

  • 관련 객체가 다양한 객체 군과 함꼐 작동해야 할 때, 객체의 구체적인 클래스에 의존하고 싶지 않은 경우
  • 여러 객체군 중 하나를 선택해서 시스템을 설정해야 하고 한 번 구성한 객체를 다른 것으로 대체할 수도 있을 때
  • 객체에 대한 클래스 라이브러리를 제공하고, 그들의 구현이 아닌 인터페이스를 노출시키고 싶을 때

이러한 추상 팩토리 패턴을 사용하게 되면..

  • 객체를 생성하는 코드를 분리하여 유저 코드와 결합도를 낮출 수 있다.
  • 객체 군을 쉽게 대체할 수 있다.
  • 단일 책임 원칙 준수 + 개방-폐쇄 원칙 준수

But

  • 각 구현체마다 팩토리 객체들을 모두 구현해주어야 하기 때문에 객체가 늘어날 때 마다 클래스가 증가하여 코드의 복잡성이 증가한다. (팩토리 메서드와 공통적인 문제)
  • 기존 추상 팩토리의 세부사항이 변경되면 모든 팩토리에 대한 수정이 필요해진다. 즉, 추상 팩토리와 모든 서브 클래스의 수정이 필요하게 된다.
  • 새로운 종류의 객체를 지원하는 것이 어렵다. 새로운 객체가 추가되면 팩토리 구현 로직 자체를 변경해야 한다.

Example

많은 사람들이 추상 팩토리와 팩토리 메서드 패턴 차이에 대해 어려워한다. 왜냐하면 둘 모두 팩토리 객체가 대신 인스턴스 객체를 생성해준다는 점에서 구조가 거의 동일하기 때문이다. 어떤 디자인 패턴 서적에서는 추상 팩토리와 팩토리 메서드를 하나의 팩토리 패턴 카테고리로 다루기도 한다.

하지만 이 둘은 완전히 별개의 패턴이다. 따라서 예제를 통해 팩토리 메서드와 추상 팩토리 메서드를 각각 구현해보고 이 둘을 합쳐볼 것이다.

팩토리 메서드에 대한 자세한 설명에 대해서는 해당 문서를 참고하자.

실제 게임에서 있을듯한 상황을 가정해보자. 특정 맵에서 맵에 환경 오브젝트를 추가하는 로직을 구현한다고 하자. 나무(Tree), 바위(Rock), 풀(Grass)를 각각 맵에 잘 배치되기 위해 각기 객체로 지정한다. 그런데 맵 종류가 (Forest)와 사막(Desert)으로 종류가 나뉘는데 어떤 종류냐에 따라 생성될 환경 오브젝트의 모습이 달라질 수 있기 때문에 각기 생성하여 구현하고자 한다.

정리하면 이 3가지의 오브젝트들은 하나의 환경 오브젝트(EnvObj) 군으로 묶을 수 있으면 또한 Map별 군으로 나뉘게 된다.

using UnityEngine;

interface EnvObj
{
    public void Create(); //환경 오브젝트 생성
}

public abstract class Tree : EnvObj { 
    public abstract void Create();
}

class ForestTree : Tree
{
    public override void Create()
    {
        Debug.Log("Forest Tree 생성 완료");
    }
}


class DesertTree : Tree
{
    public override void Create()
    {
        Debug.Log("Desert Tree 생성 완료");
    }
}

/*-------------------------------------------------------*/

public abstract class Rock : EnvObj { 
    public abstract void Create();
}

class ForestRock : Rock
{
    public override void Create()
    {
        Debug.Log("Forest Rock 생성 완료");
    }
}


class DesertRock : Rock
{
    public override void Create()
    {
        Debug.Log("Desert Rock 생성 완료");
    }
}

/*-------------------------------------------------------*/

public abstract class Grass : EnvObj { 
    public abstract void Create();
}

class ForestGrass : Grass
{
    public override void Create()
    {
        Debug.Log("Forest Grass 생성 완료");
    }
}


class DesertGrass : Grass
{
    public override void Create()
    {
        Debug.Log("Desert Grass 생성 완료");
    }
}

Factory Method

위의 구성을 팩토리 메서드 패턴으로 구현해보자. 팩토리 메서드 패턴의 공장 객체는 한가지 종류의 EnvObj만 생성하는 구조이다. 팩토리 메서드의 초점은 추상화된 팩토리 메서드를 각 서브 공장 클래스가 재정의하여 걸맞는 객체를 생성하는 것이기 때문이다. 그렇기 때문에 Tree를 생성한다고 해도 어느 Map에서 생성한 것인지는 메서드 내에서 분기문을 통해 구분해 주어야한다.

interface EnvObjFactoryMethod 
{
    EnvObj CreateOperation(string type); // 템플릿
    EnvObj CreateEnvObj(string type); // 팩토리 메서드
}

class TreeFactory : EnvObjFactoryMethod
{
    public EnvObj CreateOperation(string type)
    {
        EnvObj tree = CreateEnvObj(type);
        //tree.후처리();
        return tree;
    }

    public EnvObj CreateEnvObj(string type)
    {
        Tree tree = null;

        switch (type)
        {
            case "Forest" :
                tree = new ForestTree();
                break;
            case "Desert" :
                tree = new DesertTree();
                break;
        }
        return tree;
    }
}

class RockFactory : EnvObjFactoryMethod
{
    public EnvObj CreateOperation(string type)
    {
        EnvObj rock = CreateEnvObj(type);
        //rock.후처리();
        return rock;
    }

    public EnvObj CreateEnvObj(string type)
    {
        Rock rock = null;

        switch (type)
        {
            case "Forest" :
                rock = new ForestRock();
                break;
            case "Desert" :
                rock = new DesertRock();
                break;
        }
        return rock;
    }
}

class GrassFactory : EnvObjFactoryMethod
{
    public EnvObj CreateOperation(string type)
    {
        EnvObj grass = CreateEnvObj(type);
        //tree.후처리();
        return grass;
    }

    public EnvObj CreateEnvObj(string type)
    {
        Grass grass = null;

        switch (type)
        {
            case "Forest" :
                grass = new ForestGrass();
                break;
            case "Desert" :
                grass = new DesertGrass();
                break;
        }
        return grass;
    }
}
using UnityEngine;

public class Env : MonoBehaviour
{
    private void Start()
    {
        EnvObjFactoryMethod factory = null;
        Tree tree = null;
        Rock rock = null;
        
        // Forest Tree 생성
        factory = new TreeFactory();
        tree = (Tree)factory.CreateOperation("Forest");
        tree.Create();
        
        // Desert Tree 생성
        factory = new TreeFactory();
        tree = (Tree)factory.CreateOperation("Desert");
        tree.Create();
        
        // Forest Rock 생성
        factory = new RockFactory();
        rock = (Rock)factory.CreateOperation("Forest");
        rock.Create();
        
        // Desert Rock 생성
        factory = new RockFactory();
        rock = (Rock)factory.CreateOperation("Desert");
        rock.Create();
    }
}

여기까지가 저번에 다뤘던 팩토리 메서드 패턴을 통해 구현한 예제이다. 실제 코드 실행 자체는 문제가 없지만 만일 기능을 확장할 필요가 있으면 문제가 생긴다. 예를 들어 맵 종류에 (Swamp)를 추가한다고 해보자. 그러면 각 메서드마다 있는 분기문 로직을 모두 수정해야 하는데, 그러면 OCP 원칙에 위배된다.

switch (type)
{
    case "Forest" :
        tree = new ForestTree();
        break;
    case "Desert" :
        tree = new DesertTree();
        break;
    case "Swamp" : // 다른 rock과 grass도 모두 수정해야됨
        tree = new SwampTree();
        break;
}

Abstract Factory

그럼 이것을 추상 팩토리 패턴으로 구현하면 어떨까?

팩토리 메서드의 공장 객체는 한 종류의 EnvObj만 생성하지만, 추상 팩토리의 공장 객체는 하나의 객체에서 여러 종류의 컴포넌트들을 골라 생산할 수 있도록 구성한다.

interface EnvObjAbstractFactory
{
    Tree createTree();
    Rock createRock();
    Grass createGrass();
}

class ForestFactory : EnvObjAbstractFactory
{
    public Tree createTree() {
        return new ForestTree();
    }
    public Rock createRock() {
        return new ForestRock();
    }
    public Grass createGrass() {
        return new ForestGrass();
    }
}
class DesertFactory : EnvObjAbstractFactory
{
    public Tree createTree() {
        return new DesertTree();
    }
    public Rock createRock() {
        return new DesertRock();
    }
    public Grass createGrass() {
        return new DesertGrass();
    }
}
using UnityEngine;

public class Env : MonoBehaviour
{
    // 추상 팩토리에서 객체를 생성하는 부분 코드는 같기 때문에 따로 메서드로 묶음 분리
    public static Tree CreateTree(EnvObjAbstractFactory fac)
    {
        return fac.CreateTree();
    }
    
    private void Start()
    {
        EnvObjAbstractFactory factory = null;
        
        // Forest Tree 생성
        factory = new ForestFactory();
        Tree forestTree = CreateTree(factory);
        forestTree.Create();
        
        // Desert Tree 생성
        factory = new DesertFactory();
        Tree desertTree = CreateTree(factory);
        desertTree.Create();
    }
}

기존 팩토리 메서드에서는 다른 맵의 환경 오브젝트를 생성하기 위해선 문자열을 인자로 주어 메서드 내에서 분기문으로 객체 생성을 처리하였지만, 추상 팩토리에선 어떠한 팩토리 객체를 생성하느냐에 따라 같은 메서드를 호출해도 반환되는 결과가 다르게 된다.

하지만 추상 팩토리가 팩토리 메서드보다 무조건 좋다는 것은 아니다. 이 예제처럼 어떠한 환경 오브젝트를 '맵'에 따라 묶어 생성해야 할 때 추상 팩토리로 구성하는 것이 유지보수와 확장에 있어 더 유리하다는 것을 보여주는 것이다.

예를 들어 (Swamp)라는 새로운 맵이 추가된다고 해도, 기존의 코드 수정 없이 늪 오브젝트 구현체 클래스와 늪 팩토리 클래스만 적절하게 추가하면 확장이 완료되게 된다.

기존 팩토리 메서드로 설계 했을때는 메서드의 분기문을 일일이 뜯어 고치는 것에 비하면 확실히 OCP 원칙의 수정에는 닫혀있고 확장에는 열려있다는 말이 무슨 의미인지 체감이 될 것이다.

class SwampTree : Tree
{
    public override void Create()
    {
        Debug.Log("Swamp Tree 생성 완료");
    }
}

class SwampRock : Rock
{
    public override void Create()
    {
        Debug.Log("Swamp Rock 생성 완료");
    }
}

class SwampGrass : Grass
{
    public override void Create()
    {
        Debug.Log("Swamp Grass 생성 완료");
    }
}
class SwampFactory : EnvObjAbstractFactory
{
    public Tree CreateTree() {
        return new SwampTree();
    }
    public Rock CreateRock() {
        return new SwampRock();
    }
    public Grass CreateGrass() {
        return new SwampGrass();
    }
}

그러나 모든 확장에 대해 유연하게 대처할 수 있는 것은 아니다. 새로운 맵이 아니라 새로운 환경 오브젝트인 을 추가한다고 해보자. 그러면 모든 서브 팩토리 클래스마다 꽃을 생성하는 CreateFlower() 메서드를 추가해야되니 이때는 오히려 문제점으로 작용한다.

public interface EnvObjAbstractFactory
{
    Tree CreateTree();
    Rock CreateRock();
    Grass CreateGrass();
    Flower CreateFlower();
}

class ForestFactory : EnvObjAbstractFactory
{
    public Tree CreateTree() {
        return new ForestTree();
    }
    public Rock CreateRock() {
        return new ForestRock();
    }
    public Grass CreateGrass() {
        return new ForestGrass();
    }
    public Flower CreateFlower() {
        return new ForestFlower();
    }
}

Singleton

기본적으로 팩토리 클래스는 호출되면 객체를 생성하기만 하면 되기 때문에 메모리 최적화를 위해 각 팩토리 클래스마다 싱글톤을 적용하는 것이 옳다.

// Forest Tree 생성
factory = new ForestFactory();
Tree forestTree = CreateTree(factory);
forestTree.Create();

// Desert Tree 생성
factory = new DesertFactory();
Tree desertTree = CreateTree(factory);
desertTree.Create();

물론 GC가 가종으로 지워주긴 하지만, 결국에 객체 제거 과정에서 Stop-the-world가 일어나게 된다. 가비지 컬렉션 = 렉! 꼭 기억하고 있자. 따라서 각 팩토리 클래스를 싱글톤화 시켜 메모리 적으로 최적화 시킨다.

public interface EnvObjAbstractFactory
{
    Tree CreateTree();
    Rock CreateRock();
    Grass CreateGrass();
}

class ForestFactory : EnvObjAbstractFactory
{
    private ForestFactory() { }

    private static class SingleInstanceHolder
    {
        public static readonly ForestFactory INSTANCE = new ForestFactory();
    }

    public static ForestFactory GetInstance()
    {
        return SingleInstanceHolder.INSTANCE;
    }

    public Tree CreateTree() {
        return new ForestTree();
    }
    public Rock CreateRock() {
        return new ForestRock();
    }
    public Grass CreateGrass() {
        return new ForestGrass();
    }
}

class DesertFactory : EnvObjAbstractFactory
{ 
    private DesertFactory() { }

    private static class SingleInstanceHolder
    {
        public static readonly DesertFactory INSTANCE = new DesertFactory();
    }

    public static DesertFactory GetInstance()
    {
        return SingleInstanceHolder.INSTANCE;
    }

    public Tree CreateTree() {
        return new DesertTree();
    }
    public Rock CreateRock() {
        return new DesertRock();
    }
    public Grass CreateGrass() {
        return new DesertGrass();
    }
}

class SwampFactory : EnvObjAbstractFactory
{ 
    private SwampFactory() { }

    private static class SingleInstanceHolder
    {
        public static readonly SwampFactory INSTANCE = new SwampFactory();
    }

    public static SwampFactory GetInstance()
    {
        return SingleInstanceHolder.INSTANCE;
    }
    
    public Tree CreateTree() {
        return new SwampTree();
    }
    public Rock CreateRock() {
        return new SwampRock();
    }
    public Grass CreateGrass() {
        return new SwampGrass();
    }
}
// Forest Tree 생성
factory = ForestFactory.GetInstance();
Tree forestTree = CreateTree(factory);
forestTree.Create();

// Desert Tree 생성
factory = DesertFactory.GetInstance();
Tree desertTree = CreateTree(factory);
desertTree.Create();

Abstract Factory + Factory Method

사람들이 많이 착각하는게 추상 팩토리와 팩토리 메서드를 병행해서 사용할 수 없다고 알고 있다는 점이다. 이 둘은 엄연히 별개의 코드 패턴이고 그렇기에 동시에 사용할 수 있다.

팩토리 메서드는 추상 메서드를 통한 제품 구현과 더불어 객체 생성에 관한 전처리, 후처리를 해주는 로직이 핵심이며, 추상 팩토리여러 타입의 객체 군을 생성할 수 있는 것이 핵심이다. 따라서 필요하다면 둘을 적절히 조합해서 사용할 수도 있어야 한다. 추상 팩토리와 팩토리 메서드 패턴 둘을 조합하게 된다면, 여러 타입의 객체 군을 생성하면서 동시에 팩토리 메서드를 통해 전처리, 후처리 작업을 해주는 것이 가능해진다.

아래의 코드는 맵 군 별로 추상 팩토리를 구성하며, 각 객체 생성 메서드에 대해서 팩토리 메서드로 구성한 예제이다. 팩토리 메서드의 템플릿은 한꺼번에 환경 오브젝트들을 생성하고 추가 세팅한다는 컨셉으로써 하나의 리스트로 묶어 반환한다.

using System;
using System.Collections;
using System.Collections.Generic;

public interface EnvObjAbstractFactoryMethod
{
    Tree CreateTree();
    Rock CreateRock();
    Grass CreateGrass();

    public List<EnvObj> createOperation()
    {
        Tree tree = CreateTree();
        Rock rock = CreateRock();
        Grass grass = CreateGrass();
        
        // tree.추가세팅(); //후처리
        // rock.추가세팅(); //후처리
        // grass.추가세팅(); //후처리
        
        return new List<EnvObj> { tree, rock, grass };
    }
}

class ForestFactoryMethod : EnvObjAbstractFactoryMethod
{
    private ForestFactoryMethod() { }

    private static class SingleInstanceHolder
    {
        public static readonly ForestFactoryMethod INSTANCE = new ForestFactoryMethod();
    }

    public static ForestFactoryMethod GetInstance()
    {
        return SingleInstanceHolder.INSTANCE;
    }

    public Tree CreateTree() {
        return new ForestTree();
    }
    public Rock CreateRock() {
        return new ForestRock();
    }
    public Grass CreateGrass() {
        return new ForestGrass();
    }
}

class DesertFactoryMethod : EnvObjAbstractFactoryMethod
{ 
    private DesertFactoryMethod() { }

    private static class SingleInstanceHolder
    {
        public static readonly DesertFactoryMethod INSTANCE = new DesertFactoryMethod();
    }

    public static DesertFactoryMethod GetInstance()
    {
        return SingleInstanceHolder.INSTANCE;
    }

    public Tree CreateTree() {
        return new DesertTree();
    }
    public Rock CreateRock() {
        return new DesertRock();
    }
    public Grass CreateGrass() {
        return new DesertGrass();
    }
}

class SwampFactoryMethod : EnvObjAbstractFactoryMethod
{ 
    private SwampFactoryMethod() { }

    private static class SingleInstanceHolder
    {
        public static readonly SwampFactoryMethod INSTANCE = new SwampFactoryMethod();
    }

    public static SwampFactoryMethod GetInstance()
    {
        return SingleInstanceHolder.INSTANCE;
    }
    
    public Tree CreateTree() {
        return new SwampTree();
    }
    public Rock CreateRock() {
        return new SwampRock();
    }
    public Grass CreateGrass() {
        return new SwampGrass();
    }
}
using System.Collections.Generic;
using UnityEngine;

public class Env : MonoBehaviour
{
    private void Start()
    {
        EnvObjAbstractFactoryMethod factory = null;

        factory = ForestFactoryMethod.GetInstance();
        List<EnvObj> list = factory.createOperation();

        Debug.Log(list);

        foreach (EnvObj obj in list)
        {
            obj.Create();
        }
    }
}

만일 Tree 따로, Rock 따로 하고 싶다면 별도의 CreateTreeOperation()이나 CreateRockOperation() 메서드로 각기 구현해주면 된다.


Other

Abstract Factory vs Factory Method

둘 다 공장 객체를 통해 구체적인 타입을 감추고 객체 생성에 관여하는 패턴 임은 동일하다. 또한 공장 클래스와 인스턴스 클래스를 각각 나뉘어 느슨한 결합 구조를 구성하는 모습 역시 유사하다.

그러나 주의할 것은 추상 팩토리 패턴이 팩토리 메서드 패턴의 상위 호환이 아니라는 점이다. 두 패턴의 차이는 명확하기 때문에 적절한 패턴을 선택해서 사용해야 한다.

예를 들어 팩토리 메소드 패턴은 객체 생성 이후 해야 할 일의 공통점을 정의하는데 초점을 맞추는 반면, 추상 팩토리 패턴은 생성해야 할 객체 집합 군의 공통점에 초점을 맞춘다.

단, 이 둘을 유사점과 차이점을 조합해서 복합 패턴을 구성하는 것도 가능하다.

공통점

  • 객체 생성 과정을 추상화한 인터페이스를 제공
  • 객체 생성을 캡슐화함으로써 구체적인 타입을 감추고 느슨한 결합 구조를 표방

차이점

Factory Method

  • 구체적인 객체 생성과정을 하위 또는 구체적인 클래스로 옮기는 것
  • 한 Factory당 한 종류의 객체 생성 지원
  • 인자에 따라 생성되는 객체의 종류가 결정
  • 메소드 레벨에서 포커스를 맞춤으로써, 클라이언트의 ConcreteProduct 인스턴스의 생성 및 구성에 대한 의존을 감소

Abstract Factory

  • 관련 있는 여러 객체를 구체적인 클래스에 의존하지 않고 생성할 수 있게 하는 것
  • 한 Factory에서 서로 연관된 여러 종류의 객체 생성을 지원 (제품군 생성 지원)
  • 인자에 따라 객체들을 생성하는 Factory의 종류가 결정 (다수의 Factory 존재)
  • 클래스(Factory) 레벨에서 포커스를 맞춤으로써, 클라이언트의 ConcreteProduct 인스턴스 군의 생성 및 구성에 대한 의존을 감소

profile
Be Honest, Be Harder, Be Stronger

0개의 댓글