재우니의 블로그

https://unsplash.com/ko/%EC%82%AC%EC%A7%84/saRKnTHBEhU

 

Abstract Factory (추상팩토리) 에 대해 설명을 기재해 봤습니다.

 

더보기

Abstract Factory (추상팩토리) 는 구체적인 클래스를 지정하지 않고 전체 제품군을 생성하는 문제를 해결하는 크리에이티브 디자인 패턴입니다.

Abstract Factory 는 모든 개별 제품을 생성하기 위한 인터페이스를 정의하지만 실제 제품 생성은 구체적인 팩토리 클래스에 맡깁니다. 각 팩토리 유형은 특정 제품 종류에 해당합니다.

클라이언트 코드는 생성자 호출(새 연산자)을 사용하여 제품을 직접 생성하는 대신 팩토리 객체의 생성 메서드를 호출합니다. 팩토리는 단일 제품 이형 상품에 해당하므로 모든 제품이 호환됩니다.

클라이언트 코드는 팩토리 및 제품의 추상 인터페이스를 통해서만 작동합니다. 따라서 클라이언트 코드는 공장 개체에 의해 생성된 모든 제품 이형 상품과 함께 작동할 수 있습니다. 구체적인 팩토리 클래스를 새로 생성하여 클라이언트 코드에 전달하기만 하면 됩니다.

 

사용 예제: 추상 팩토리 패턴은 C# 코드에서 매우 일반적입니다. 많은 프레임워크와 라이브러리에서 표준 컴포넌트를 확장하고 사용자 지정하는 방법을 제공하기 위해 이 패턴을 사용합니다.

식별: 이 패턴은 팩토리 객체를 반환하는 메서드로 쉽게 알아볼 수 있습니다. 그런 다음 팩토리는 특정 하위 컴포넌트를 만드는 데 사용됩니다.

 

* 집중해야 할 부분 살펴보기

 

어떤 클래스로 구성되어 있는가?
이 클래스들은 어떤 역할을 수행하나요?
패턴의 요소들은 어떤 방식으로 관련되어 있는가?

 

 

 

아래는 C#에서 Abstract Factory 패턴을 활용하여 다른 종족 간에 싸우고 이기고 지는 게임을 만들기 위한 예제 코드입니다:

 

 

using System;

// Abstract Factory 인터페이스
interface IUnitFactory
{
    Unit CreateUnit();
}

// 유닛 모델
abstract class Unit
{
    public abstract string Name { get; }
    public int Health { get; set; }

    public void Attack(Unit target)
    {
        Console.WriteLine($"{Name}이(가) {target.Name}을(를) 공격합니다.");
        target.Health -= 10;
        if (target.Health <= 0)
        {
            Console.WriteLine($"{target.Name}이(가) 파괴되었습니다.");
        }
    }
}

// 테란 종족 유닛 Factory
class TerranUnitFactory : IUnitFactory
{
    public Unit CreateUnit()
    {
        Console.WriteLine("테란 종족 유닛 생성 중...");
        return new TerranUnit();
    }
}

// 저그 종족 유닛 Factory
class ZergUnitFactory : IUnitFactory
{
    public Unit CreateUnit()
    {
        Console.WriteLine("저그 종족 유닛 생성 중...");
        return new ZergUnit();
    }
}

// 프로토스 종족 유닛 Factory
class ProtossUnitFactory : IUnitFactory
{
    public Unit CreateUnit()
    {
        Console.WriteLine("프로토스 종족 유닛 생성 중...");
        return new ProtossUnit();
    }
}

// 테란 종족 유닛
class TerranUnit : Unit
{
    public override string Name => "테란 마린";

    public TerranUnit()
    {
        Health = 100;
    }
}

// 저그 종족 유닛
class ZergUnit : Unit
{
    public override string Name => "저그 저글링";

    public ZergUnit()
    {
        Health = 80;
    }
}

// 프로토스 종족 유닛
class ProtossUnit : Unit
{
    public override string Name => "프로토스 질럿";

    public ProtossUnit()
    {
        Health = 120;
    }
}

// 클라이언트 코드
class GameClient
{
    private IUnitFactory playerFactory;
    private IUnitFactory enemyFactory;

    public GameClient(IUnitFactory playerFactory, IUnitFactory enemyFactory)
    {
        this.playerFactory = playerFactory;
        this.enemyFactory = enemyFactory;
    }

    public void PlayGame()
    {
        Unit playerUnit = playerFactory.CreateUnit();
        Unit enemyUnit = enemyFactory.CreateUnit();

        Console.WriteLine($"플레이어 유닛: {playerUnit.Name} (체력: {playerUnit.Health})");
        Console.WriteLine($"적 유닛: {enemyUnit.Name} (체력: {enemyUnit.Health})");
        Console.WriteLine();

        // 플레이어 유닛이 적 유닛을 공격
        playerUnit.Attack(enemyUnit);
        Console.WriteLine($"플레이어 유닛: {playerUnit.Name} (체력: {playerUnit.Health})");
        Console.WriteLine($"적 유닛유닛: {enemyUnit.Name} (체력: {enemyUnit.Health})");
        Console.WriteLine();

        // 적 유닛이 플레이어 유닛을 공격
        enemyUnit.Attack(playerUnit);
        Console.WriteLine($"플레이어 유닛: {playerUnit.Name} (체력: {playerUnit.Health})");
        Console.WriteLine($"적 유닛: {enemyUnit.Name} (체력: {enemyUnit.Health})");
        Console.WriteLine();

        // 게임 결과 출력
        if (playerUnit.Health <= 0 && enemyUnit.Health <= 0)
        {
            Console.WriteLine("무승부!");
        }
        else if (playerUnit.Health <= 0)
        {
            Console.WriteLine("적 유닛이 승리했습니다!");
        }
        else if (enemyUnit.Health <= 0)
        {
            Console.WriteLine("플레이어 유닛이 승리했습니다!");
        }
    }
}

class Program
{
    static void Main()
    {
        Console.WriteLine("StarCraft 게임을 시작합니다.");

        IUnitFactory terranFactory = new TerranUnitFactory();
        IUnitFactory zergFactory = new ZergUnitFactory();
        IUnitFactory protossFactory = new ProtossUnitFactory();

        // 테란 vs 저그
        GameClient terranVsZergClient = new GameClient(terranFactory, zergFactory);
        terranVsZergClient.PlayGame();

        // 저그 vs 프로토스
        GameClient zergVsProtossClient = new GameClient(zergFactory, protossFactory);
        zergVsProtossClient.PlayGame();

        // 프로토스 vs 테란
        GameClient protossVsTerranClient = new GameClient(protossFactory, terranFactory);
        protossVsTerranClient.PlayGame();
    }
}

 

 

위의 코드에서는 GameClient 클래스에서 플레이어 Factory와 적 Factory를 각각 주입받아 다른 종족 간의 전투를 구현합니다. PlayGame() 메서드에서는 플레이어 유닛과 적 유닛을 생성하고 초기 체력을 출력한 후, 플레이어 유닛이 적 유닛을 공격하고, 적 유닛이 플레이어 유닛을 공격합니다. 마지막으로 게임의 승리 조건에 따라 결과를 출력합니다.

 

Main() 메서드에서는 테란 vs 저그, 저그 vs 프로토스, 프로토스 vs 테란의 세 가지 전투를 각각 생성하고 GameClient를 생성하여 게임을 플레이합니다.

 

 

 

장점:

  1. 관련된 객체들의 일관된 생성: Abstract Factory 패턴은 관련된 객체들의 생성을 일관되게 처리할 수 있습니다. 서로 다른 종류의 객체들을 생성하는 Factory들이 일관된 방식으로 동작하도록 구성할 수 있습니다.
  2. 클라이언트 코드와의 낮은 결합도: Abstract Factory는 구체적인 클래스 대신 인터페이스를 사용하기 때문에, 클라이언트 코드는 추상화된 Factory 인터페이스에 의존하게 됩니다. 이를 통해 클라이언트 코드와 구체적인 클래스 사이의 결합도를 낮출 수 있습니다.
  3. 유연성과 확장성: 새로운 종류의 객체를 추가하기 위해서는 해당 객체의 Factory를 구현하고 Abstract Factory의 인터페이스를 따르도록 만들면 됩니다. 이를 통해 시스템에 새로운 기능을 쉽게 추가하고 확장할 수 있습니다.

 

 

 

단점:

  1. 복잡성: Abstract Factory 패턴은 객체들의 생성을 추상화하기 때문에 추가적인 추상화 계층을 도입하게 됩니다. 이로 인해 시스템의 구조가 복잡해지고, 코드의 양이 증가할 수 있습니다.
  2. 유연성 제한: Abstract Factory 패턴은 미리 정의된 Factory들을 사용하여 객체들을 생성하기 때문에 동적인 객체 생성에는 제한이 있습니다. 즉, 실행 중에 Factory를 변경하여 다른 종류의 객체를 생성하는 것은 어렵습니다.

 

 

 

Abstract Factory 패턴은 관련된 객체들을 일관성 있게 생성하고, 클라이언트 코드와의 결합도를 낮추며, 확장성을 갖출 수 있는 장점을 가지고 있습니다. 그러나 복잡성과 유연성 제한을 고려하여 패턴을 적용해야 합니다. 프로젝트의 요구사항과 구조에 맞게 적절한 패턴을 선택하고 사용하는 것이 중요합니다.

 

 


 

Abstract Factory 패턴의 사용에 따른 장점과 단점을 비교하기 위해 좋은(Good) 코드와 나쁜(Bad) 코드를 비교해보겠습니다.

 

Bad Code (Abstract Factory를 사용하지 않은 경우):

 

 

class GameClient
{
    public void PlayGame(string faction)
    {
        if (faction == "Terran")
        {
            TerranUnit terranUnit = new TerranUnit();
            terranUnit.Attack();
        }
        else if (faction == "Zerg")
        {
            ZergUnit zergUnit = new ZergUnit();
            zergUnit.Attack();
        }
        else if (faction == "Protoss")
        {
            ProtossUnit protossUnit = new ProtossUnit();
            protossUnit.Attack();
        }
    }
}

 

 

위의 코드는 게임 클라이언트에서 Abstract Factory 패턴을 사용하지 않고 유닛을 생성하는 예시입니다. 코드에서는 플레이어의 선택에 따라 테란, 저그, 프로토스 종족의 유닛을 생성하고 공격하는 로직이 구현되어 있습니다. 이러한 방식은 다음과 같은 문제점을 가지고 있습니다:

 

  1. 클라이언트 코드가 구체적인 유닛 클래스에 직접 의존하고 있어, 변경이 어려움: 플레이어가 선택한 종족이 변경되면 클라이언트 코드를 수정해야 합니다. 새로운 종족이 추가되거나 기존 종족의 유닛이 변경되는 경우에도 클라이언트 코드를 수정해야 합니다.
  2. 유닛 생성과 관련된 로직이 중복: 각 종족의 유닛 생성 로직이 중복되어 있습니다. 유닛 생성 코드가 여러 곳에 분산되어 있어 유지보수가 어렵고 코드 중복이 발생할 수 있습니다.

 

Good Code (Abstract Factory 패턴을 사용한 경우):

 

interface IUnitFactory
{
    Unit CreateUnit();
}

class TerranUnitFactory : IUnitFactory
{
    public Unit CreateUnit()
    {
        return new TerranUnit();
    }
}

class ZergUnitFactory : IUnitFactory
{
    public Unit CreateUnit()
    {
        return new ZergUnit();
    }
}

class ProtossUnitFactory : IUnitFactory
{
    public Unit CreateUnit()
    {
        return new ProtossUnit();
    }
}

class GameClient
{
    private IUnitFactory unitFactory;

    public GameClient(IUnitFactory factory)
    {
        unitFactory = factory;
    }

    public void PlayGame()
    {
        Unit unit = unitFactory.CreateUnit();
        unit.Attack();
    }
}

 

위의 코드는 Abstract Factory 패턴을 사용하여 유닛을 생성하는 예시입니다. 클라이언트 코드는 추상화된 IUnitFactory 인터페이스에 의존하고 있으며, 특정 종족에 대한 유닛 생성 로직은 해당Factory 클래스에서 구현되어 있습니다. 이러한 방식은 다음과 같은 장점을 가지고 있습니다:

 

  1. 클라이언트 코드와 유닛 생성 로직 간의 낮은 결합도: 클라이언트 코드는 추상화된 IUnitFactory 인터페이스에 의존하고 있어, 구체적인 유닛 클래스에 직접 의존하지 않습니다. 이로 인해 플레이어가 선택한 종족이 변경되더라도 클라이언트 코드를 수정할 필요가 없습니다. 유닛 생성 로직은 해당 Factory 클래스 내에 캡슐화되어 있습니다.
  2. 유닛 생성 로직의 중복 제거: 각 종족의 유닛 생성 로직이 각각의 Factory 클래스에 구현되어 있습니다. 이로 인해 유닛 생성과 관련된 로직이 중복되지 않으며, 유지보수가 용이해집니다. 새로운 종족을 추가할 때는 해당 종족에 대한 Factory 클래스만 구현하면 되므로 코드 중복을 피할 수 있습니다.

 

 

Abstract Factory 패턴을 사용하면 클라이언트 코드와 유닛 생성 로직 사이의 결합도를 낮출 수 있으며, 유지보수성과 확장성을 개선할 수 있습니다. 클라이언트 코드는 추상화된 인터페이스에 의존하여 구체적인 클래스에 대한 의존성을 제거하고, 유닛 생성 로직은 적절한 Factory 클래스 내에서 캡슐화됩니다.

 

 


 

좀 더 복잡하게 구현해 볼까요?

 

서로 다른 종족끼리 서로 싸우고 에너지가 0이 되면 먼저 지는 게임을 만들었습니다.

마린과 질럿으로 싸움을 붙여서 결국은 마린이 졌다고 나올 겁니다. 🤗

 

 

using System;

public interface IUnit
{
    string Name { get; }
    int HP { get; }
    int AttackPower { get; }
    void Attack(IUnit enemy);
    void ReceiveDamage(int damage);
}

public interface IUnitFactory
{
    IUnit CreateUnit(string unitType);
}

public class Terran : IUnit
{
    public Terran(string name, int hp, int attackPower)
    {
        Name = name;
        HP = hp;
        AttackPower = attackPower;
    }

    public string Name { get; }
    public int HP { get; private set; }
    public int AttackPower { get; }

    public void Attack(IUnit enemy)
    {
        enemy.ReceiveDamage(AttackPower);
    }

    public void ReceiveDamage(int damage)
    {
        HP -= damage;
    }
}

public class Protoss : IUnit
{
    public Protoss(string name, int hp, int attackPower)
    {
        Name = name;
        HP = hp;
        AttackPower = attackPower;
    }

    public string Name { get; }
    public int HP { get; private set; }
    public int AttackPower { get; }

    public void Attack(IUnit enemy)
    {
        enemy.ReceiveDamage(AttackPower);
    }

    public void ReceiveDamage(int damage)
    {
        HP -= damage;
    }
}

public class TerranFactory : IUnitFactory
{
    public IUnit CreateUnit(string unitType)
    {
        switch (unitType)
        {
            case "Marine":
                return new Terran("Marine", 40, 5);
            case "Marauder":
                return new Terran("Marauder", 100, 10);
            default:
                throw new ArgumentException("Invalid unit type specified");
        }
    }
}

public class ProtossFactory : IUnitFactory
{
    public IUnit CreateUnit(string unitType)
    {
        switch (unitType)
        {
            case "Zealot":
                return new Protoss("Zealot", 60, 8);
            case "Stalker":
                return new Protoss("Stalker", 80, 12);
            default:
                throw new ArgumentException("Invalid unit type specified");
        }
    }
}

public class Game
{
    public static void Main(string[] args)
    {
        IUnitFactory terranFactory = new TerranFactory();
        IUnitFactory protossFactory = new ProtossFactory();

        IUnit terranUnit = terranFactory.CreateUnit("Marine");
        IUnit protossUnit = protossFactory.CreateUnit("Zealot");

        // 유닛들이 서로 싸우기 시작합니다.
        while (true)
        {
            terranUnit.Attack(protossUnit);
            if (protossUnit.HP <= 0)
            {
                Console.WriteLine($"{protossUnit.Name} is defeated!");
                break;
            }

            protossUnit.Attack(terranUnit);
            if (terranUnit.HP <= 0)
            {
                Console.WriteLine($"{terranUnit.Name} is defeated!");
                break;
            }

            Console.WriteLine($"{protossUnit.HP} is protossUnit.HP!");
            Console.WriteLine($"{terranUnit.HP} is terranUnit.HP!");
        }
    }
}

 



Factory Method 패턴과 Abstract Factory 패턴의 차이점

 

Factory Method 패턴과 Abstract Factory 패턴은 서로 매우 유사한 패턴이지만, 각 패턴의 목적과 적용 방법에 약간의 차이가 있습니다. 이 차이점을 설명드리겠습니다.

더보기

* Factory Method 패턴:

객체 생성에 관련된 코드를 별도의 클래스로 분리하여, 생성할 객체와 해당 객체를 생성하기 위한 팩토리 메서드를 동일한 인터페이스에 정의합니다.
팩토리 메서드는 일반적으로 하나의 생성자를 가지고 있어, 추상화된 인터페이스를 구현하는 구체 클래스들 중 하나를 선택하여 생성합니다.
이 패턴은 새로운 종류의 객체를 추가하기 쉽게 만들어 기존의 코드에 최소한의 수정으로 새로운 유형의 객체를 지원할 수 있게 하려는 것이 목표입니다.


* Abstract Factory 패턴:

추상 팩토리는 서로 관련된 객체 집합을 생성하기 위한 인터페이스를 제공합니다. 각 객체 집합은 일반적으로 동일한 주제나 용도로 연결되어 있습니다.
구체 팩토리 클래스는 이 인터페이스를 구현하여 해당 주제 또는 용도에 대한 객체 집합을 생성하는 로직을 제공합니다.

 

* 결론


이 패턴은 서로 다른 객체 패밀리를 자유롭게 대체할 수 있는 유연성을 제공하려는 것이 목표입니다.
패턴에 어떤 클래스 구조와 코드가 포함되냐에 따라 Factory Method와 Abstract Factory를 구별할 수 있습니다. 많은 경우에서 이 두 패턴은 유사한 목적과 구조를 지니고 있기 때문에 상황에 맞게 선택하여 사용하시면 됩니다. 중요한 것은 패턴 자체의 이름이나 디자인보다는 클린하고 유연한 코드를 작성하는 데 도움이 되는 소프트웨어 디자인 원칙을 이해하는 것입니다.