이론/디자인패턴

디자인 패턴 - 생성 패턴

tsyang 2021. 11. 27. 12:48

개요

생성 패턴에는 다음의 패턴이 포함된다.

  • Factory
  • Factory Method
  • Abstract Factory
  • Builder
  • Singleton
  • Prototype

이 글에서는 위 패턴들을 '간단하게' 다룬다.


팩토리(Factory) 패턴

 

팩토리는 오브젝트를 찍어내는 공장 같은 것이다.

//타입을 받아 클래스를 반환한다.
public class AnimalFactory
{
    public Animal Create(AnimalType animalType)
    {
        switch (animalType)
        {
            case AnimalType.Cat:
                return new Cat();
            case AnimalType.Dog:
                return new Dog();
        }
        return null;
    }
}

위 방식처럼 ~~ Factory 클래스를 만들어 사용할 수 있다.

 

이 방식을 단독으로 사용하는 경우는 잘 없고 팩토리 메서드 패턴이나 추상 팩토리와 연관되어 사용된다.

 

클라이언트가 (복잡할 수 있는) 생성과정을 알 필요가 없다는 장점이 있다.

 

 


팩토리 메서드(Factory Method) 패턴

만약 팩토리 패턴을 사용하는데 지금까지 고양이를 몇 마리 만들어냈는지 알고 싶다거나, 예방 접종된 고양이를 만들고 싶다면 한계에 부딪힌다.

 

이를 해결하기 위해 팩토리 메서드 패턴을 사용한다.

 

팩토리 메서드 패턴은 팩토리 인터페이스를 만든 뒤 구체화된 팩토리가 이를 상속하도록 한다.

 

//팩토리에 기능을 부여함
public sealed class CatFactory : AnimalFactory
{
    public int CatCount { get { return _catCount; } }
    private int _catCount = 0;

    public override Animal Create()
    {
        ++_catCount;
        return new Cat();
    }

    public Animal CreateVacinatedCat()
    {
        ++_catCount;
        return new Cat().Vacinate();
    }
}

 

실무에서는 간혹 팩토리이지만 팩토리보다 더 많은 기능을 가진 클래스가 존재하기도 한다. 이런 경우 보통 클래스 이름이 CatCreator, CatManager처럼 된다. 만약 팩토리를 상속하고 있다면 그냥 팩토리 메서드구나~ 하고 알아먹으면 된다.

 


추상 팩토리(Abstract Factory) 패턴

추상 팩토리 패턴은 아래와 같이 매트릭스 형태를 가진 오브젝트들을 생성할 때 유리하다. 즉, 하나의 팩토리에서 특정 테마에 속하는 여러 오브젝트를 만들 때 유용하다.

 

  Light Mode Dark Mode
Button LightButton DarkButton
CheckBox LightCheckBox DarkCheckBox
ScrollBar LightScrollBar DarkScrollBar

 

예를 들어, 웹페이지 중에는 다크 모드를 추가로 지원하는 경우가 있다. 이 경우에 다크 모드에 들어갈 UI 컴포넌트들의 테마를 모두 바꿔줘야 한다. 이 경우 버튼, 체크박스, 스크롤 바를 다크 모드에 맞게 만들어주는 팩토리가 있으면 편할 것이다.

//Dark, Light 테마별 버튼
public abstract class Button { }
public sealed class DarkButton : Button { }
public sealed class LightButton : Button { }

//여러 개의 UI 오브젝트를 생성하는 팩토리 인터페이스
public interface UIFactory
{
    Button GetButton();
    CheckBox GetCheckBox();
    ScrollBar GetScrollBar();
}

//Dark Mode 테마에 맞는 UI 오브젝트들을 만들어내는 구체 클래스
public sealed class DarkModeUIFactory : UIFactory
{
    public Button GetButton()
    {
        return new DarkButton();
    }

    public CheckBox GetCheckBox()
    {
        return new DarkCheckBox();
    }

    public ScrollBar GetScrollBar()
    {
        return new DarkScrollBar();
    }
}
var darkmodeUIFactory = new DarkModeUIFactory();
var button = darkmodeUIFactory.GetButton(); //DarkButton
var checkBox = darkmodeUIFactory.GetCheckBox(); //DarkCheckBox
var scrollBar = darkmodeUIFactory.GetScrollBar(); //DarkScrollBar

 

팩토리나 팩토리 메서드 패턴과 다른 점은, 각각의 테마를 가진 여러 가지 오브젝트들을 하나의 팩토리에서 만들고자 할 때, 이러한 오브젝트들을 만드는 팩토리 자체를 추상화시킨다는 것.

 


빌더(Builder) 패턴

 

출처 : https://en.wikipedia.org/wiki/Builder_pattern

 

빌더 패턴은 오브젝트의 생성과정이 복잡할 때(특히 argument) 이를 간단하게 만드는 것을 도와주는 패턴이다.

 

예를 들어, 고블린 한 마리를 만든다고 하자. 고블린은 다음의 필드들을 가진다.

 

public class Goblin
{
    private int hp;
    private int spd;
    private int str;
    private int dex;
    private int con;
    private int height_cm;
}

 

그렇다면 고블린의 생성자는 어떻게 될까?

 

public Goblin(int hp, int spd, int str, int dex, int con, int height_cm)
{
    this.hp = hp;
    this.spd = spd;
    this.str = str;
    this.dex = dex;
    this.con = con;
    this.height_cm = height_cm;
}
var goblin = new Goblin(10, 5, 4, 5, 4, 150);

지정해줘야 할 필드가 많으니 생성자가 길고 복잡해지며, 몇 번째 argument가 무엇인지도 헷갈린다.

 

실제로 만들 고블린의 경우 회피율, 적중률과 같은 여러 속성을 추가할 수 있기 때문에 실제로 고블린 클래스는 더 복잡할 것이다.

 

그렇다면 빌더 패턴을 이용해보자.

 

public class GoblinBuilder
{
    private int hp;
    private int spd;
	//... 고블린이 갖는 필드를 똑같이 가진다.

    public GoblinBuilder()
    {
    }

    public Goblin Build()
    {
        return new Goblin(hp, spd, str, dex, con, height_cm);
    }

    public void SetHP(int hp)
    {
        this.hp = hp;
    }

    public void SetSpd(int spd)
    {
        this.spd = spd;
    }
    /// ... 나머지 필드에도 set함수를 정해준다.
}

 

그렇다면 고블린을 다음과 같이 만들 수 있다.

var goblinBuilder = new GoblinBuilder();
goblinBuilder.SetHP(10);
goblinBuilder.SetSpd(5);
goblinBuilder.SetStr(4);
goblinBuilder.SetDex(5);
goblinBuilder.SetCon(4);
goblinBuilder.SetHeight(150);

Goblin goblin = goblinBuilder.Build();

 

상속

상속을 통해 특정 필드를 고정시켜줄 수 있다. 

//강한 고블린은 힘이 10이다.
public class StrongGoblinBuilder : GoblinBuilder
{
    public StrongGoblinBuilder() : base()
    {
        SetStr(10);
    }
}

 

디렉터

그 외에도 디렉터라는 게 존재하는데

//거대한 고블린과 같은 특수 고블린들을 세팅해 준다.
public class GoblinDirector
{
    public void SetHugeGoblin(GoblinBuilder builder)
    {
        builder.SetHP(10);
        builder.SetStr(10);
        builder.SetCon(10);
        builder.SetDex(2);
        builder.SetSpd(2);
        builder.SetHeight(190);
    }
}
var goblinBuilder = new GoblinBuilder();
var goblinDirector = new GoblinDirector();

goblinDirector.SetHugeGoblin(goblinBuilder);
Goblin goblin = goblinBuilder.Build();

위 코드처럼 빌더를 매개변수로 받아 변수를 일괄적으로 세팅해주는 역할을 한다.

 


싱글톤(Singleton) 패턴

싱글턴 패턴은 다음의 특징을 갖는다.

  1. 오직 한 개의 인스턴스만 갖도록 보장한다.
  2. 전역 접근점을 제공한다.

이 외에도 게으른 초기화를 한다는 특징이 있다.

 

문제

  • 전역 접근점을 제공하는 만큼 전역 변수가 가지는 단점도 모두 가진다. 싱글턴 패턴은 어떻게 보면 클래스로 캡슐화된 전역 상태라고 볼 수 있다. 이게 싱글턴 패턴의 최대 단점이다.
  • 싱글턴 패턴은 '한 개의 인스턴스 보장'과 '전역 접근'을 제공하는데 만약 '전역 접근'만을 위해 싱글턴 패턴을 사용한다면 의도치 않게 '한 개의 인스턴스'가 강제될 수 있다. 
  • 싱글턴 패턴의 게으른 초기화는 제어할 수 없다.

 

대안

  • 클래스가 꼭 필요한지 생각해본다. 예를 들어, ~~ Manager와 같은 싱글턴 클래스들은 사실 OOP를 제대로 이해했다면 불필요한 경우가 있다. 오브젝트가 스스로 자기 일을 수행하도록 해라.
  • 한 개의 인스턴스 보장이 필요한 경우 정적 클래스를 쓰거나 런타임에 인스턴스 개수를 확인하는 방법을 쓸 수 있다. 
//2개 이상의 파일시스템이 만들어지면 예외를 던진다.
class FileSystem : IDisposable
{
    private static bool _isInstantiated = false;

    public FileSystem()
    {
        if (_isInstantiated)
            throw new Exception();
        _isInstantiated = true;
    }

    ~FileSystem()
    {
        OnRelease();
    }

    private void OnRelease()
    {
        _isInstantiated = false;
    }

    public void Dispose()
    {
        OnRelease();
        GC.SuppressFinalize(this);
    }
}

 

전역 접근점

전역 접근점의 경우 그냥 함수의 인자로 데이터를 넘겨줘도 된다.

 

혹은 상위 클래스를 만들어 여기에 정적 변수를 둔 뒤 상위 클래스를 상속하는 방법으로 접근을 좀 더 제한할 수 있다. 

//상속을 통해 Log 클래스에 쉽게 접근할 수 있다.

public class Log
{
    public void Write(string str) { }
}

public class GameObject
{
    private static Log _log = new Log();

    protected Log GetLog()
    {
        return _log;
    }
}

public class Enemy : GameObject
{
    void SomeMethod()
    {
        GetLog().Write("Get Log From Parent");
    }
}

 

서비스 중개자 패턴이나 하위 클래스 샌드박스 패턴을 이용할 수 있다.

 

상속

싱글턴 상속 기법은 굉장히 유용할 수 있다. 예를 들어, 파일 시스템 래퍼가 PS5와 닌텐도 스위치를 지원해야 한다고 하자.

 

//상속을 통해 플랫폼에 맞는 파일 시스템 인스턴스를 생성한다.

public abstract class FileSystem
{
    public static FileSystem Instance
    {
        get
        {
            if (_instance != null)
            {
#if PLATFORM == PS5
                _instance = new PS5FileSystem();
#elif PLATFORM == NSW
                _instance = new NSWFileSystem();
#endif
            }
            return _instance;
        }
    }

    private static FileSystem _instance;

    public abstract byte[] ReadFile(string path);
    public abstract void WriteFile(string path, byte[] contents);
}

 

이 외에도 싱글턴 상속 기법은 단위 테스트용 모의 객체를 만들 때도 유용하게 사용할 수 있다.


프로토타입(Prototype) 패턴

프로토타입 패턴은 원형이 되는 오브젝트를 가지고 있다가 이를 Clone 하여 (deep copy) 여러 오브젝트를 만들어 내는 패턴이다. 이때 원형이 되는 오브젝트를 프로토타입이라고 한다.

 

//프로토타입 몬스터를 매개로 받아 Clone을 통해 몬스터를 생성하는 Spawner
//그러나! 이런 식의 사용은 비추

public class Monster : ICloneable
{
    public object Clone()
    {
        return MemberwiseClone();
    }
}

public class Spawner
{
    private Monster _prototype;

    public Spawner(Monster prototype)
    {
        _prototype = prototype;
    }

    public Monster SpawnMonster()
    {
        return _prototype.Clone() as Monster;
    }
}

프로토타입은 위처럼 몬스터 스포너 등의 클래스를 만드는 데 사용될 수 있다.

 

그러나 클래스 상속 구조가 복잡해지면 유지보수가 힘들기 때문에 웬만해서는 몬스터 별로 클래스를 만드는 게임은 없다. 주로 타입 객체나 컴포넌트 패턴을 이용해서 모델링한다. 

 

 

데이터 모델링

프로토타입 패턴은 데이터 모델링에서 유용하게 쓰일 수 있다. 예를 들어, 아이템을 다음과 같은 json 데이터로 관리한다고 하자.

{
  "이름" : "롱소드"
  "타입" : "검"
  "공격력" : 30
  "내구력" : 30
  "무게" : 5
}

이 상황에서 만약 "품질 좋은 롱소드", "마법의 롱소드", "냉기의 롱소드"를 만들고 싶다면?

 

{
  "이름" : "품질 좋은 롱소드"
  "프로토타입" : "롱소드"
  "공격력" : 35
}

{
  "이름" : "마법의 롱소드"
  "프로토타입" : "롱소드"
  "속성" : "마법"
}

{
  "이름" : "냉기의 롱소드"
  "프로토타입" : "롱소드"
  "특수능력" : [
  	{ 
      "이름" :"냉기부여"
      "레벨" : 1
    }]
}

이런 식으로 중복을 많이 제거할 수 있다. 

 

이런 개념을 정확히는 위임(delegation)이라고 하는데 해당 객체에 속성이 없으면 상위 객체의 속성을 사용하는 것이다.

 

이러한 위임 개념은 일회성 특수 객체가 많이 나오는 게임에 적합하다.

 


참고 : 로버트 나이스트롬, 게임 프로그래밍 패턴, 한빛미디어, [87~119p] (싱글턴, 프로토타입)

 

'이론 > 디자인패턴' 카테고리의 다른 글

서비스 중개자  (0) 2022.01.16
이벤트 큐 패턴  (1) 2022.01.03
컴포넌트 패턴  (1) 2021.12.25
디자인 패턴 - 행동패턴  (0) 2021.12.12
디자인 패턴 - 구조  (1) 2021.12.03