포스트

Factory Pattern

위 글은 유니티에서 공식으로 제공하는 E Book을 기반으로 제가 번역, 공부하며 정리한 자료를 글로 남긴 것입니다.

image

다른 오브젝트들을 만드는 특별한 오브젝트가 있는 것이 가끔은 도움이 될 때가 있다. 많은 게임들은 게임 플레이 과정에서 다양한 것들을 생성하고, 정말로 필요해지기 전까지는 런타임에 정말 무엇이 필요한지 종종 모를 수 있다.

팩토리 패턴은 이 목적을 위한 특별한 객체, 즉 ‘팩토리’를 지정한다. 한 단계에서, 이 패턴은 ‘생산품’을 생성하는 데 관련된 많은 세부 사항을 캡슐화한다. 즉각적인 이점은 코드를 정리하는 것이다.

그러나, 각 제품이 공통 인터페이스나 기본 클래스를 따른다면, 이를 한 단계 더 발전시켜 그것의 생성 로직을 더 많이 포함시키고, 이를 팩토리 자체로부터 숨길 수 있다. 이렇게 함으로써 새로운 객체를 생성하는 것이 더 확장 가능해진다.

또한 팩토리를 서브 클래스화하여 특정 제품 전용의 팩토리를 만들 수 있다. 이렇게하면 런타임에 적, 장애물 또는 기타 어떤 것들도 생성하는 데 도움이 된다.

예제 : 단순한 팩토리

게임 레벨에 아이템을 인스턴스화하기 위한 팩토리 패턴을 만들고 싶다고 상상해 보자. 프리팹(Prefabs)을 사용하여 GameObject들을 생성할 수 있지만, 각 인스턴스를 생성할 때 몇 가지 사용자 정의 동작을 실행하고 싶을 수도 있다.

이 로직을 유지하기 위해 if문이나 switch문을 사용하는 대신, IProduct라는 인터페이스와 Factory라는 추상 클래스를 생성하라.

1
2
3
4
5
6
7
8
9
10
11
12
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 원칙 중 리스코프 치환 원칙을 염두에 두어라.

다음과 같은 구조가 그 결과일 수 있다.

image

IProduct는 생산품들 간의 공통점을 정의한다. 이 경우에는, 단순히 ProductName 프로퍼티와 Initialize에서 동작하는 로직이 있겠다.
그러면 IProduct 인터페이스를 따르는 한 (ProductA, ProductB 등) 필요한 만큼 많은 제품을 정의할 수 있다.

기본 클래스인 FactoryIProduct를 반환하는 GetProduct메소드를 가지고 있다.
이는 추상 클래스이기 때문에, Factory의 인스턴스를 직접 만들 수는 없다. 대신, 구체적인 서브클래스 (ConcreteFactoryAConcreteFactoryB)를 파생시켜, 실제로 다양한 생산품들을 관리할 수 있다.

이 예제에서 GetProductVector3 위치를 인수로 받아 특정 위치에 프리팹 GameObject를 더 쉽게 인스턴스화할 수 있다. 각 구체적인 팩토리의 필드에는 해당하는 템플릿 프리팹도 저장된다.

여기 ProductAConcreteFactoryA의 예시가 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class ProductA : MonoBehaviour, IProduct 
{ 
    [SerializeField] private string productName = "ProductA";
    public string ProductName { get => productName; set => productName = value ; } 
    private ParticleSystem particleSystem; 
    
    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 productPrefab; 
    
    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; 
    } 
}

여기, 생산품 클래스들이 MonoBehaviour를 상속받고 IProduct 인터페이스를 구현하게 함으로써, 팩토리 내에서 프리팹의 장점을 살릴 수 있다.

각 생산품이 Initialize 메소드의 자체 버전을 가질 수 있다는 점에 주목하라.
예제 ProductA 프리팹은 ParticleSystem을 포함하고 있으며, ConcreteFactoryA가 복사본을 인스턴스화할 때 재생된다. 팩토리 자체는 Particle을 트리거하는 구체적인 로직을 포함하고 있지 않다. 단지 모든 생산품에 공통인 Initialize 메소드만을 호출한다.

샘플 프로젝트를 탐색하여 ClickToCreate 컴포넌트가 팩토리 간에 전환하여 서로 다른 행동을 가진 ProductAProductB를 생성하는 방법을 확인하라. ProductB는 생성될 때 소리를 재생하는 반면, ProductA는 입자 효과를 발생시킨다.

장단점

팩토리 패턴은 많은 제품을 설정할 때 가장 큰 이점을 제공한다. 애플리케이션에서 새로운 제품 유형을 정의해도 기존 제품을 변경하거나 이전 코드를 수정할 필요가 없다.

각 제품의 내부 로직을 자체 클래스로 분리함으로써 팩토리 코드를 상대적으로 짧게 유지할 수 있다. 각 팩토리는 기본적인 세부 사항을 알 필요 없이 각 제품에 대해 Initialize를 호출하는 방법만 알고 있다.

단점은 패턴을 구현하기 위해 다수의 클래스와 서브클래스를 생성한다는 것. 다른 패턴들처럼, 이것은 약간의 오버헤드를 도입하는데, 이는 제품의 다양성이 크지 않은 경우 불필요할 수 있다.

개선 사항

팩토리의 구현은 여기에서 보여진 것과는 크게 다를 수 있다. 자신만의 팩토리 패턴을 구축할 때 다음과 같은 조정을 고려하라:

생산품 검색을 위한 딕셔너리 사용:

1
2
3
생산품을 딕셔너리의 키-값 쌍으로 저장하고자 할 수 있다. 
고유한 문자열 식별자(예: 이름 또는 일부 ID)를 키로 사용하고 타입을 값으로 사용하라. 
이는 제품 및/또는 해당 팩토리를 검색하는 것을 더 편리하게 만들 수 있다.

팩토리(또는 팩토리 매니저)를 정적(static)으로 만들기:

1
2
이는 사용하기 쉽게 만들지만 추가 설정이 필요하다. 
정적 클래스는 인스펙터에 나타나지 않으므로, 제품 컬렉션도 정적으로 만들어야 한다.

GameObject와 MonoBehaviour가 아닌 것에 적용하기:

1
2
프리팹이나 다른 Unity 특정 컴포넌트에만 제한되지 말 것. 
팩토리 패턴은 어떤 C# 객체와도 작동할 수 있다.

오브젝트 풀 패턴과 결합하기:

1
2
3
4
팩토리는 반드시 새 객체를 인스턴스화하거나 생성할 필요는 없다. 
계층구조에서 기존 객체를 검색할 수도 있다. 
한 번에 많은 객체를 인스턴스화해야 하는 경우(예: 무기에서 발사체), 
더 최적화된 메모리 관리를 위해 오브젝트 풀 패턴을 사용하라.

팩토리는 필요에 따라 어떤 게임플레이 요소든 생성할 수 있다. 그러나 제품 생성이 종종 그들의 유일한 목적은 아니라는 점에 유의하라. 팩토리 패턴을 더 큰 작업의 일부로 사용할 수도 있다(예: 대화 상자의 UI 요소 설정 또는 게임 레벨의 부분 설정).

참고한 자료

Unity_E-Book_LevelUpYourCodeWithGameProgrammingPatterns

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.