개요
생성 패턴에는 다음의 패턴이 포함된다.
- 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) 패턴
빌더 패턴은 오브젝트의 생성과정이 복잡할 때(특히 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) 패턴
싱글턴 패턴은 다음의 특징을 갖는다.
- 오직 한 개의 인스턴스만 갖도록 보장한다.
- 전역 접근점을 제공한다.
이 외에도 게으른 초기화를 한다는 특징이 있다.
문제
- 전역 접근점을 제공하는 만큼 전역 변수가 가지는 단점도 모두 가진다. 싱글턴 패턴은 어떻게 보면 클래스로 캡슐화된 전역 상태라고 볼 수 있다. 이게 싱글턴 패턴의 최대 단점이다.
- 싱글턴 패턴은 '한 개의 인스턴스 보장'과 '전역 접근'을 제공하는데 만약 '전역 접근'만을 위해 싱글턴 패턴을 사용한다면 의도치 않게 '한 개의 인스턴스'가 강제될 수 있다.
- 싱글턴 패턴의 게으른 초기화는 제어할 수 없다.
대안
- 클래스가 꼭 필요한지 생각해본다. 예를 들어, ~~ 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 |