스탠다드 특강 5 - 디자인 패턴 1

김정환·2024년 10월 28일
0

디자인 패턴

  • 디자인 패턴은 기출문제집이다.
    • 어떤 문제, 유형에서 어떻게 풀면 되는가?
    • 이걸 쓸려면 어떤 문제, 유형인지 알아야 한다.
    • 디자인 패턴을 통해서 어떤 걸 해결하고자 하는지 알아야 쓸 수 있다.
  • Gof 외에도 다양한 디자인 패턴이 있다.

왜 쓸까

  • 복잡하고 난해한 문제를 패턴화해서 풀 수 있다. (유형화)
  • 다른 개발자와 소통할 때 간단하게 설명할 수 있다.
    • 복잡하게 설명하기 vs 싱글톤으로 풀었습니다.
    • 단, 이것도 혼자만 아는 것을 이야기하는 것을 주의

문제를 푸는데 쓰는 것이다

  • 디자인 패턴을 쓰기 위해 문제를 만드는 경우가 있음
  • 디자인 패턴을 무지성으로 활용하는 것은 매우 지양
  • 포폴에서 이 디자인 패턴을 왜 썼는지 어필하는 것이 매우 중요

디자인 패턴의 특징

  • 비유가 많다.

1. 싱글톤 Singleton

왜 쓸까?

  • 중요하고 유일하게 존재하는 대상을 쉽게 접근하고 싶어서
  • 접근이 잦은 핵심 기능에 대한 전역적 접근을 허용한다.

구현

  • 유일하다 : 추가적으로 생기는 것을 제한
  • 전역적으로 접근 가능 : publick static을 통해 구현
  • instance - 개별 객체
    • new 키워드로 생성
    ClassExample instance = new ClassExample();
  • static : 메모리에 상주하게 됨.

구현 예시

public class ClassExample : MonoBehaviour
{
    public static ClassExample instance;

    void Awake()
    {
        if (instance != null)
        {
            Destroy(gameObject); //이미 있는 경우 새로운 인스턴스는 파괴
            return;
        }

        instance = this;        
    }
}

확장 : Generic 연계 - 제너릭 싱글톤

왜 쓸까?

  • 싱글톤 객체들 간의 유사성이 매우 높음
  • 그런데 단순하게 상속으로 상위 클래스로 단순하게 접근하긴 싫음
    (각 싱글톤의 고유 기능 캐스팅 없이 바로 쓰고 싶다는 것)
    • 추상클래스로 구현하기 싫다는 것
    • 상속으로 중복은 최대한 줄이고 그 외적인 부분은 동적으로 처리하고 싶음

제너릭

= 일반화 프로그래밍

  • 성능에 영향이 없는 것은 아니나 그것 때문에 쓰지 않을 이유는 없다.

제너릭 구현 예시

public class Singleton<T> : MonoBehaviour where T : MonoBehaviour
{
    private static T instance;
    public static T Instance
    {
        get 
        {
            if (instance == null)
            {
                instance = FindObjectOfType<T>();

                if(instance == null)
                {
                    instance = new GameObject(typeof(T).Name).AddComponent<T>();
                    DontDestroyOnLoad(instance.gameObject); // 필요에 따라 선택적으로 사용
                }
            }

            return instance;
        }
    }
    
}

사용 팁

  1. 라이프 사이클 주의
    • Awake, Start 사용에서 유의해야함
    • Awake는 나와 관련된 작업
    • Start는 다른 컴포넌트 관련 작업
      • GetComponent 등
    • 별도로 Initialize()를 명시하는 경우.
  2. 모든 싱글톤을 DontDestroyOnLoad를 할 필요가 없다.
  • 추천은 Singleton<T> / SingletonDontDestroy<T> 구분 구현을 선호
  • DontDestroy했는데 참조하는 객체들이 null 되어 있는 것이 더 피곤

매니저 vs 일반 객체

  • 유일성 : 매니저는 유일해야 함
  • 참조가 많은 객체 : 매니저는 참조가 최소한 활발한 객체여야함.
    • 전역적으로 참조가 된다면 매니저로 해도 되지만
      지역적으로 작은 인스턴스들에 참조된다면 일반 객체로 남겨두는 것이 좋은 방향.

2. 오브젝트 풀 패턴

왜 쓸까

  • 할당/해제에 걸리는 성능 낭비메모리 낭비를 줄이고 싶어서
    • GC 호출 유도, 메모리 할당,삭제들이 성능에 지대한 영향을 줌
  • 생성, 파괴가 반복되는 오브젝트를 재활용함. (오브젝트를 재사용하기 위한 것)
    • 일반적으로 로딩이 1초 더 걸리는 것이 게임 중 프레임 드랍이 발생하는 것보다 나은 경험을 제공.
    • 그러므로 오브젝트 풀링을 통해서 사전에 미리 만들어두고 사용
  • 풀 Pool : 뽑힐 수 있는 대상을 묶는 단위(수학 : 전체 집합)라고 생각하면 됨.

구현

표준이 따로 없어서 자유롭게 만들어도 됨.
단, 파괴 대신 비활성화한다는 것은 필수사항.

  • 필요한만큼 미리 생성하고 비활성화
    • 필요할때 생성할 수도 있음.
  • 생성 대신 비활성화된 오브젝트를 찾아 활성화
    • 큐 등을 통해서 오래된 오브젝트를 쓸 수 있음.
  • 미리 생성해둔 양 이상을 요구한다면 추가로 생성
    • 제일 오래된 걸 쓸 수도 있음.
  • 파괴 대신 오브젝트를 비활성화 (필수)

구현 예시

using System.Collections.Generic;
using UnityEngine;

public class ObjectPool : MonoBehaviour
{
    [SerializeField] GameObject prefab;
    [SerializeField] int initialSize = 10;
    [SerializeField] int maxSize = 300;

    List<GameObject> pool = new List<GameObject>();

    void Awake()
    {
        /// 초기화
        /// 초기에 필요한 갯수만큼 오브젝트를 생성
        for (int i = 0; i < initialSize; i++)
        {
            CreateObject();
        }
    }

    GameObject CreateObject()
    {
        GameObject obj = Instantiate(prefab);
        obj.SetActive(false); // 초기 생성 시 비활성화로 생성

        pool.Add(obj); // 풀에 넣기
        return obj;
    }

    public GameObject GetObject()
    {
        foreach (GameObject obj in pool)
        {
            if(!obj.activeInHierarchy)
            {
                obj.SetActive(true);
                return obj;
            }
        }

        // 사용 가능한 것이 없는 경우 - 새로 생성하기
        GameObject newObject = CreateObject();
        newObject.SetActive(true); // 바로 사용할 것이므로 활성화하기
        
        if (pool.Count < maxSize) // 1. 만들어진 갯수가 최대 크기보다 작은 경우 - 새로 생성한 것을 pool에 추가
        {
            pool.Add(newObject);
        }
        else // 2. 만들어진 갯수가 최대 크기에 도달한 경우 - 임시 오브젝트를 생성하고 반납 시 파괴할 것 
        {
            newObject.name = "Temp";
        }

        return newObject;
    }

    public void ReturnObject(GameObject obj)
    {
        if(obj.name.Equals("Temp")) // 반환 시, 임시 오브젝트라면 파괴
            Destroy(obj);
        else  // 일반 오브젝트라면 비활성화
            obj.SetActive(false);
    }
}
  

UnityEngine.Pool

2021 버전이후부터 제공.
간단하게 델리게이트를 넣어서 커스터마이징 가능한 클래스를 제공.

  • maxSize는 널널하게, initialSize는 보수적으로 할당

공식 문서

using UnityEngine;
using UnityEngine.Pool;

public class ObjectPool : MonoBehaviour
{
    [SerializeField] GameObject prefab;
    
    const int SIZE = 100;
    bool collectionCheck = true;
    
    ObjectPool<GameObject> pool;

    void Start()
    {
        pool = new ObjectPool<GameObject>(
        	Create, 
            OnGet, 
            OnRelease, 
            OnPoolDestroy, 
            collectionCheck, 
            0, 
            SIZE
        );    
    }

    GameObject Create()
    {
        return Instantiate(prefab, transform);
    }
	
    // Pool에서 꺼낼 때 호출
    void OnGet(GameObject obj)
    {   
        // 꺼낼 때 활성화하기
        obj.SetActive(true);
    }
	
    // Pool에 반납 시 호출
    void OnRelease(GameObject obj)
    {
        // 반납 시 비활성화하기
        obj.SetActive(false);
    }

    void OnPoolDestroy(GameObject obj)
    {
        Destroy(obj);
    }
    
    // 외부에서 호출할 메서드
    public GameObject GetObject()
    {
        return pool.Get();
    }

    public void ReturnObject(GameObject obj)
    {
        pool.Release(obj);
    }

}

고려사항

  • 씬 로드 시 오브젝트 풀의 모든 오브젝트를 전부 다 생성해 두어야 함.
    • 씬 로드 시간을 매우 길게 만들 수 있음
    • 필요한 만큼만 생성할 것

3. 전략 패턴

왜 쓸까?

  • 원본 클래스를 건드리지 않고 다양하게 추가되는 방식을 일관적으로 대응하고 싶어서
  • 마치 골프에서 상황에 맞게 채를 바꾸는 것처럼 상황에 맞게 로직군(SW)을 바꿔주는 방식
    • 공을 친다는 행위는 동일하게 하되, 용도에 맞는 채를 바꿔 사용함으로써
      공을 멀리 보내거나 가까운 목표로 세밀하게 조절해서 치는 것.

전략이란?

  • 전략이란 로직들의 군을 정의하고 로직군들간에서 교체가능하게 함.
  • 로봇에는 일반적인 기능들을 포함하고, 청소 SW를 구동해서 청소 로봇, 경비 SW를 구동해서 경비 로봇을 작동하는 것

구현

전략 인터페이스를 정의.
전략 인터페이스에는 세부 동작을 구현.

구현 예시

using UnityEngine;

// 전략 인터페이스
public interface IRideStrategy
{
    void Ride();
}

public class Bicycle : IRideStrategy
{
    public void Ride()
    {
        Debug.Log("Ride Bicycle");
    }
}
public class Car : IRideStrategy
{
    public void Ride()
    {
        Debug.Log("Ride Car");
    }
}
public class Helicopter : IRideStrategy
{
    public void Ride()
    {
        Debug.Log("Ride Helicopter");
    }
}

public class Person : MonoBehaviour
{
    IRideStrategy ridableStrategy;

    public void SetRideStrategy(IRideStrategy strategy)
    {
        ridableStrategy = strategy;
    }

    public void Ride()
    {
        ridableStrategy.Ride();
    }
}

활용 - UI 연출

연출에 대한 구현을 UIView 클래스에서 직접하지 않고,
UI를 보여주거나 숨기는 방법을 각 전략 인터페이스에서 가능하도록 구현


4. 상태 패턴

왜 쓸까?

  • 한 클래스에 여러 상태들이 있고 상태에 따라 다르게 실행되는 로직을 체계적으로 관리하고 싶을때 사용.
  • idle, angry, dead와 같은 여러 상태를 두고 그 상태에 맞는 행동을 하게한다.

대표적인 예

  • 애니메이터 : Unity Animator가 대표적인 사례.

FSM (Finite State Machine) 유한 상태 머신

  • N개의 가능한 상태, 현재 상태, 상태 간의 전환으로 구성

FSM 구현

  • 숙련 주차 강의의 적 구현, N개의 상태를 두고
    그 안에서 전이가 일어나는 형태의 구현에서 활용

구성요소

  • State Machine : 상태들을 전환하는 역할
  • State : 실행되어야할 로직을 정의
public interface IState
{
    // 상태들에 들어갈 때, 나올 때, 매 프레임이 중요
    void Enter(); // 상태에 들어갈 때 == OnEnable()
    void Excute(); // 매프레임 실행할 때 == Update()
    void Exit(); // 상태에서 나올 때 == OnDisable()
}

public class IdleState : IState
{
    public void Enter()
    {
        
    }

    public void Excute()
    {
        
    }

    public void Exit()
    {

    }
}

public class WanderState : IState
{
    public void Enter()
    {

    }

    public void Excute()
    {
        
    }

    public void Exit()
    {
        
    }
}

public class AttackState : IState
{
    public void Enter()
    {

    }

    public void Excute()
    {
        
    }

    public void Exit()
    {
        
    }
}

public class StateMachine
{
    IState curState;

    public void ChangeState(IState newState)
    {
        if(curState != null)
            curState.Exit();
        
        curState = newState;
        curState.Enter();
    }

    // MonoBehaviour의 Update가 아님.
    // 이 클래스 아무것도 상속받지 않고 있기에 별개의 메서드로 봐야 함.
    public void Update()
    {
        if(curState != null)
            curState.Excute();
    }
}

public class Enemy : MonoBehaviour
{
    StateMachine stateMachine;
    Transform target;

    void Start()
    {
        stateMachine = new StateMachine();
        
        stateMachine.ChangeState(new IdleState());
        // new로 원하는 상태를 만들어서 사용
        // 이 상태 클래스에 선언된 변수가 없기 때문에 비용적으로 큰 부담은 없음
    }

    // Update is called once per frame
    void Update()
    {
        DetectEnemy();
    }

    void DetectEnemy()
    {
        // 생략
        stateMachine.ChangeState(new AttackState());
    }
}

HFSM (Hierachial Finite State Machine): 계층 유한 상태 머신

  • 큰 개념의 상태와, 실제 행동을 결정하는 세부 상태를 구분하여 구현

Idle / Jump / Walk / Run
Idle / Jump / Move(Walk, Run)으로 나누는 것

profile
사파 개발자

0개의 댓글