Priv's Blog
4. Factory pattern 본문
1. 팩토리 패턴
때때로 다른 오브젝트들을 생성하기 위한 특수한 오브젝트를 만드는 것이 유용할 수 있습니다. 많은 게임이 게임이 플레이되는 동안 수많은 오브젝트를 생성(spawn)합니다. 또한 런타임 상에서 실제로 언제 어떤 오브젝트가 필요한지 모르는 일도 종종 발생합니다.
팩토리 패턴은 여러분이 예측하신 것처럼 필요에 따라 ‘공장(factory)’ 역할을 해주는 특별한 오브젝트를 디자인하는 디자인 패턴입니다. 한 레벨 상에 배치된 공장은 생성할 “제품(products)”과 관련된 세부 사항들을 캡슐화합니다. 이때 바로 얻을 수 있는 이점은 여러분의 코드를 깔끔하게 정리할 수 있다는 것입니다.
그러나, 만약 각각의 제품이 일반적인 인터페이스 또는 부모 클래스와 같은 형태를 띠고 있다면, 여러분은 한 단계 더 나아가 그 제품 자체에 고유한 생산 로직을 더 많이 포함하도록 만들고, 그 로직을 공장으로부터 숨길 수 있습니다. 그렇게 하면 더욱 높아진 확장성 덕분에 새로운 오브젝트를 생산하는 것이 가능해지죠.
또한 여러분은 특정한 제품들을 생산하기 위한 다수의 공장을 자식 클래스로 둘 수도 있습니다. 이렇게 하면 런타임 상에서 적을 생산하거나, 장애물을 생산하는 등의 다양한 기능을 쉽게 구현할 수 있습니다.
2. 예제: 간단한 팩토리 패턴 실습
팩토리 패턴을 통해 게임 레벨 상에서 아이템을 생성하는 기능을 만들고 싶다고 가정해 봅시다. 프리팹(Prefab)을 사용해 게임 오브젝트를 생성하는 방법도 있지만, 여러분은 각각의 인스턴스를 생성할 때 몇 가지 커스텀 기능들도 실행되게 만들고 싶습니다.
if 조건문 또는 switch 조건문을 사용하여 로직을 관리하는 것보다 IProduct라는 이름의 인터페이스를 생성하고 Factory라는 이름의 추상 클래스를 생성하여 코드를 작성해 봅시다:
public interface IProduct
{
public string ProductName { get; set; }
public void Initialize();
}
public abstract class Factory : MonoBehaviour
{
public abstract IProduct GetProduct(Vector3 position);
// shared method with all factories
…
}
제품들은 메서드를 구현하기 위한 특정한 양식 지켜야 합니다. 하지만 그 외에는 어떠한 기능도 공유하지 않죠. 그러므로 위와 같이 IProduct 인터페이스를 선언합니다.
공장들은 공통된 기능들을 공유해야 할 수도 있습니다. 그러므로 이 예시에서는 추상 클래스를 사용했습니다. 자식 클래스를 사용할 때는 항상 SOLID 원칙 중 하나인 리스코프 치환 원칙을 기억해 주세요.
그럼, 이제 다음과 같은 구조가 만들어집니다:
IProduct 인터페이스는 게임 내에서 생산되는 제품들의 일반적인 기능들이 무엇인지를 정의하는 인터페이스입니다. 이 경우에는 간단하게 ProductName이라는 속성과 제품이 Initialize 메서드에서 실행하는 로직들이 포함되어 있습니다.
이제 여러분은 필요에 따라 IProduct 인터페이스에서 정의한 양식에 따르는 다양한 제품들(ProductA, ProductB 등)을 정의할 수 있습니다.
Factory 부모 클래스는 IProduct 타입의 객체를 반환하는 GetProduct 메서드를 가지고 있습니다. 이 클래스는 추상 클래스이기 때문에 Factory 타입의 인스턴스를 직접적으로 생성하는 것은 불가능합니다. 그러므로 여러분은 서로 다른 제품을 생산하는 한 쌍의 자식 클래스(ConcreteFactoryA와 ConcreteFactoryB)를 파생해야 합니다.
아래 예시 코드에서 볼 수 있는 GetProduct 메서드는 Vector3 position 매개변수를 받으므로, 더욱 쉽게 특정한 위치에 프리팹 게임 오브젝트를 인스턴스화할 수 있습니다. 또한 각각의 ConcreteFactory 클래스에도 동일한 양식의 프리팹이 저장됩니다.
ProductA 클래스와 ConcreteFactoryA 클래스의 예시 코드는 다음과 같습니다:
public class ProductA : MonoBehaviour, IProduct
{
[SerializeField] private string productName = “ProductA”;
public string ProductName = { get => productName; set => productName = value; }
private ParticleSystem pariticleSystem;
public void Initialize()
{
// any unique logic to this product
gameObject.name = productName;
particleSystem = GetComponentInChildren<ParticleSystem>();
particleSystem?.Stop();
particleSystem?.Play();
}
}
public class ConcreteFactoryA : Factory
{
[SerializeField] private ProductA producPrefab;
public override IProduct GetProduct(Vector3 position)
{
// create a Prefab instance and get the product component
GameObject instance = Instantiate(productPrefab.gameObject, position, Quaternion.identity);
ProductA newProduct = instance.GetComponent<ProductA>();
// each product contains its own logic
newProduct.Initialize();
return newProduct;
}
}
자, 여기서 여러분은 IProduct 인터페이스를 구현하는 제품 클래스들을(ProductA, ProductB) 만들었습니다. 이를 통해 공장 내의 프리팹들이 지니는 장점들을 취할 수 있게 되었습니다.
어떻게 각각의 제품들이 고유한 형태의 Initialize 메서드를 가질 수 있을지 염두에 두세요. 예시로 나온 ProductA 프리팹은 ConcreteFactoryA가 복사본을 인스턴스화할 때 재생되는 ParticleSystem을 포함하고 있습니다. Factory 클래스 그 자체에는 파티클을 제어하는 특정한 로직을 포함하고 있지 않습니다. 그저 모든 Product 클래스 내에 존재하는 일반적인 Initialize 메서드를 깨울(Invoke) 뿐이죠.
예제 프로젝트를 통해 ClickToCreate 컴포넌트가 서로 다른 동작을 보여주는 ProductA와 ProductB를 생성하기 위해 Factory 클래스들을 어떻게 전환하는지 살펴보세요. ProductA는 파티클 효과를 재생하고, ProductB는 생성될 때 소리를 재생합니다.
3. 팩토리 패턴의 장단점
팩토리 패턴은 수많은 제품을 설정해야 할 때 효과적일 수 있습니다. 애플리케이션 내에 존재하는 새로운 제품의 타입을 정의하는 것은 이미 생성된 다른 요소를 변경하거나 기존의 코드를 수정하는 작업을 요구하지 않습니다.
각 제품이 가지고 있는 클래스 내부의 로직을 분리하면 공장의 역할을 하는 코드를 비교적 짧게 유지할 수 있습니다. 각 공장은 각 제품이 가지고 있는 Initialize 메서드를 깨우는 것만 할 수 있을 뿐, 해당 메서드의 세부적인 부분까지는 알지 못합니다.
팩토리 패턴의 단점이라면, 패턴을 구현하기 위해서 여러 개의 클래스와 자식 클래스를 만들어야 한다는 겁니다. 만약 여러분이 만드시는 게임에 수많은 종류의 제품들이 필요하지 않다면, 이는 약간의 불필요한 오버헤드(overhead)를 발생시킵니다.
4. 팩토리 패턴 개량하기
팩토리 패턴은 이 책에서 다루는 것보다 더 넓고 다양한 방법으로 구현될 수 있습니다. 아래의 요소들을 참고하여 여러분만의 팩토리 패턴을 만들어 보세요.
- 제품을 탐색할 때 딕셔너리(dictionary) 자료형 사용하기: 여러분의 제품들을 키-값의 쌍으로 데이터를 관리하는 딕셔너리 자료형을 이용해 관리하고 싶을 수 있습니다. 고유한 문자열 형식의 식별자(제품의 이름 또는 특정한 ID 값 등)를 키와 값의 타입으로 사용해 보세요. 이는 제품이나 적절한 공장들을 더욱 쉽게 검색할 수 있도록 만들어 줍니다.
- 공장 (또는 공장 관리자) 클래스를 정적 클래스로 생성하기: 이 방법은 더 쉽게 팩토리 패턴을 사용할 수 있게 해주지만, 별도의 추가 작업이 요구됩니다. 정적 클래스는 Unity 엔진 상의 Inspector 창에 표시되지 않습니다. 그렇기 때문에 여러분이 사용하려는 제품들의 컬렉션도 정적 타입으로 만들어 주어야 합니다.
- 오브젝트 풀 패턴과 함께 사용하기: 제품 생산 역할을 수행하는 공장들은 인스턴스화 또는 새로운 오브젝트 생성을 할 필요가 없습니다. 또한 이들은 Hierarchy 창에서 검색도 가능합니다. 만약 여러분이 수많은 오브젝트(무기의 발사체 등)를 동시다발적으로 생성해야 한다면, 좀 더 최적화된 메모리 관리를 위해 오브젝트 풀 패턴을 함께 사용해 보세요.
공장들은 게임 플레이에 필요한 요소들을 필요에 따라서 어느 때나 생성할 수 있습니다. 그러나, 기억해야 할 것은, 제품들을 생성하는 것이 유일한 목적이 아닌 경우가 종종 있다는 것입니다. 여러분은 팩토리 패턴을 더 거대한 다른 작업(게임 레벨의 일부분인 대화 상자 내의 UI 요소들을 설정하는 것 등)의 일부분으로써 사용할 수 있다는 거죠.
수고하셨습니다!
'Unity Learn > Game Programming Patterns' 카테고리의 다른 글
6. Singleton pattern (0) | 2023.10.31 |
---|---|
5. Object pool (0) | 2023.10.23 |
3. Design patterns for game development (0) | 2023.10.05 |
2. The SOLID principles (0) | 2023.10.01 |
1. Introducing design patterns (0) | 2023.09.07 |