Unity

유니티 생명주기 - 코루틴

우대비 2025. 5. 22. 09:30
반응형

생명주기 심화: 코루틴의 실행 흐름

코루틴(Coroutine)은 Unity의 생명주기와 밀접하게 연관된 강력한 비동기 프로그래밍 도구입니다. 코루틴을 사용하면 실행을 여러 프레임에 걸쳐 분산시킬 수 있으며, 이를 통해 무거운 작업을 수행하면서도 게임의 프레임 레이트를 유지할 수 있습니다.

코루틴과 생명주기의 관계

코루틴은 MonoBehaviour의 생명주기와 밀접하게 연결되어 있습니다:

  • 코루틴은 StartCoroutine()을 호출한 MonoBehaviour가 활성화된 상태에서만 실행됩니다.
  • MonoBehaviour가 비활성화되면(OnDisable()), 실행 중인 모든 코루틴이 자동으로 일시 중지됩니다.
  • MonoBehaviour가 다시 활성화되면(OnEnable()), 일시 중지된 코루틴이 자동으로 재개되지 않습니다.
  • GameObject가 파괴되면(OnDestroy()), 실행 중인 모든 코루틴이 자동으로 종료됩니다.
  • StopAllCoroutines()를 호출하면 해당 MonoBehaviour의 모든 코루틴이 중지됩니다.
  • StopCoroutine()을 호출하면 특정 코루틴만 중지할 수 있습니다.

코루틴 실행 시점과 yield 명령어의 의미

코루틴의 작동 원리: 코루틴을 시작하면, 첫 번째 yield 지점까지 즉시 실행된 후 제어권을 반환합니다. 다음 실행은 yield 명령어에 따라 결정된 시점에 계속됩니다.

yield 명령어 실행 시점 사용 사례
yield return null 다음 프레임의 Update() 직전 프레임 단위로 작업을 분산, 애니메이션 구현
yield return new WaitForFixedUpdate() 다음 FixedUpdate() 이후 물리 계산 결과를 기다릴 때, 물리 기반 작업 수행
yield return new WaitForEndOfFrame() 현재 프레임의 모든 카메라와 렌더링 작업이 완료된 후 렌더링 후 처리, 스크린샷 캡처, GUI 작업
yield return new WaitForSeconds(time) 지정된 시간(초)이 경과한 후 지연된 액션, 쿨다운, 타이머 구현
yield return new WaitUntil(() => condition) 지정된 조건이 참이 될 때까지 특정 조건 대기(예: 애니메이션 완료, 로딩 완료)
yield return new WaitWhile(() => condition) 지정된 조건이 참인 동안 대기 특정 상태가 지속되는 동안 대기(예: 일시 정지 상태)
yield return StartCoroutine(AnotherCoroutine()) 다른 코루틴이 완료될 때까지 순차적 코루틴 실행, 종속 작업 처리
yield return asyncOperation AsyncOperation(씬 로딩, 비동기 자원 로드 등)이 완료될 때까지 자원 로딩, 씬 전환 처리

고급 코루틴 패턴: 실전 예제

순차적 작업 처리 패턴

private IEnumerator GameStartSequence()
{
    // 1. 페이드 인 효과
    yield return StartCoroutine(FadeInEffect(1.0f));
    
    // 2. 인트로 애니메이션 재생
    _animator.SetTrigger("PlayIntro");
    
    // 애니메이션이 완료될 때까지 대기
    // AnimationClip.length를 사용하는 것보다 더 정확함
    yield return new WaitUntil(() => !_animator.GetCurrentAnimatorStateInfo(0).IsName("Intro") || 
                              _animator.GetCurrentAnimatorStateInfo(0).normalizedTime >= 1.0f);
    
    // 3. 초기 리소스 로드
    AsyncOperation resourceLoad = Resources.LoadAsync<GameObject>("GameAssets");
    yield return resourceLoad;
    
    if (resourceLoad.isDone)
    {
        GameObject assets = resourceLoad.asset as GameObject;
        Instantiate(assets);
    }
    
    // 4. 게임 시작 알림
    GameManager.Instance.OnGameReady();
    
    // 5. 플레이어 컨트롤 활성화 (0.5초 지연)
    yield return new WaitForSeconds(0.5f);
    _playerController.enabled = true;
    
    Debug.Log("게임 시퀀스 완료!");
}

주기적 작업 실행 패턴

private IEnumerator AIUpdateRoutine()
{
    // 재사용 가능한 WaitForSeconds 객체 캐싱
    WaitForSeconds updateInterval = new WaitForSeconds(0.2f);
    
    while (true) // 무한 루프
    {
        // 높은 계산 비용의 AI 처리
        foreach (var enemyAI in _activeEnemies)
        {
            if (enemyAI != null)
            {
                enemyAI.UpdatePathfinding();
                enemyAI.EvaluateTargets();
                
                // 몇 개의 AI만 처리한 후 다음 프레임까지 대기 (성능 최적화)
                if (_activeEnemies.IndexOf(enemyAI) % 5 == 0 && _activeEnemies.IndexOf(enemyAI) > 0)
                {
                    yield return null;
                }
            }
        }
        
        // 모든 AI 업데이트 후 지정된 간격만큼 대기
        yield return updateInterval;
    }
}

타임아웃 패턴

private IEnumerator LoadOperationWithTimeout(string resourcePath, float timeoutSeconds)
{
    float startTime = Time.time;
    bool isTimedOut = false;
    bool isCompleted = false;
    
    // 비동기 로드 시작
    AsyncOperation asyncLoad = Resources.LoadAsync<GameObject>(resourcePath);
    
    // 완료 콜백 등록
    asyncLoad.completed += (op) => { isCompleted = true; };
    
    // 타임아웃 또는 완료될 때까지 대기
    while (!isCompleted && !isTimedOut)
    {
        // 진행 상황 업데이트
        float progress = asyncLoad.progress;
        _loadingBar.fillAmount = progress;
        
        // 타임아웃 확인
        if (Time.time - startTime > timeoutSeconds)
        {
            isTimedOut = true;
            Debug.LogWarning($"로드 작업 타임아웃: {resourcePath}");
        }
        
        yield return null;
    }
    
    // 결과 처리
    if (isCompleted && !isTimedOut)
    {
        Debug.Log($"리소스 로드 완료: {resourcePath}");
        GameObject loadedResource = asyncLoad.asset as GameObject;
        if (loadedResource != null)
        {
            Instantiate(loadedResource);
        }
    }
    else
    {
        // 타임아웃 처리
        _errorPanel.SetActive(true);
        _errorText.text = "리소스 로드 실패. 네트워크 연결을 확인하세요.";
    }
}

성능 최적화 팁: WaitForSeconds와 같은 yield 객체를 반복해서 생성하면 가비지 컬렉션에 부담을 줄 수 있습니다. 자주 사용하는 시간 간격은 캐싱하여 재사용하는 것이 좋습니다.

생명주기와 성능 최적화

생명주기 메서드 선택과 성능 영향

Unity의 생명주기 메서드는 각각 호출 빈도와 실행 맥락이 다르므로, 적절한 메서드를 선택하는 것이 성능에 큰 영향을 미칩니다.

메서드 호출 빈도 성능 영향 최적화 권장사항
Update() 매 프레임 높음 (프레임 레이트에 직접적 영향) - 무거운 계산 피하기
- 빈 Update() 메서드 제거
- 조건부 실행으로 불필요한 처리 방지
FixedUpdate() 물리 타임스텝마다 (기본 0.02초) 중간 (물리 계산에 영향) - 물리 관련 코드만 넣기
- Fixed Timestep 값 최적화
- 불필요한 물리 계산 최소화
LateUpdate() 모든 Update() 후 매 프레임 중간 (프레임 마지막에 영향) - 카메라 관련 코드나 최종 위치 조정에만 사용
- 무거운 계산 피하기
코루틴 yield 지시자에 따라 다름 낮음 (분산 실행 가능) - 무거운 작업 분산에 사용
- yield 객체 캐싱
- 필요 없는 코루틴은 명시적 중지
OnRenderImage() 카메라 렌더링 후 매 프레임 매우 높음 (GPU 성능에 영향) - 간단한 셰이더 사용
- 조건부 처리로 불필요한 경우 바로 Blit
- 저해상도 렌더 텍스처 사용 고려

Update() 최적화 전략

커스텀 업데이트 매니저 구현

public class UpdateManager : MonoBehaviour
{
    public enum UpdateFrequency
    {
        EveryFrame,
        EverySecondFrame,
        EveryFifthFrame,
        OncePerSecond
    }
    
    // 업데이트 빈도별 액션 목록
    private List<Action> _everyFrameActions = new List<Action>();
    private List<Action> _everySecondFrameActions = new List<Action>();
    private List<Action> _everyFifthFrameActions = new List<Action>();
    private List<Action> _oncePerSecondActions = new List<Action>();
    
    // 시간 및 프레임 카운터
    private int _frameCount = 0;
    private float _secondCounter = 0;
    
    private static UpdateManager _instance;
    public static UpdateManager Instance
    {
        get
        {
            if (_instance == null)
            {
                GameObject go = new GameObject("UpdateManager");
                _instance = go.AddComponent<UpdateManager>();
                DontDestroyOnLoad(go);
            }
            return _instance;
        }
    }
    
    public void RegisterAction(Action action, UpdateFrequency frequency)
    {
        switch (frequency)
        {
            case UpdateFrequency.EveryFrame:
                _everyFrameActions.Add(action);
                break;
            case UpdateFrequency.EverySecondFrame:
                _everySecondFrameActions.Add(action);
                break;
            case UpdateFrequency.EveryFifthFrame:
                _everyFifthFrameActions.Add(action);
                break;
            case UpdateFrequency.OncePerSecond:
                _oncePerSecondActions.Add(action);
                break;
        }
    }
    
    public void UnregisterAction(Action action)
    {
        _everyFrameActions.Remove(action);
        _everySecondFrameActions.Remove(action);
        _everyFifthFrameActions.Remove(action);
        _oncePerSecondActions.Remove(action);
    }
    
    private void Update()
    {
        _frameCount++;
        _secondCounter += Time.deltaTime;
        
        // 매 프레임 실행
        for (int i = 0; i < _everyFrameActions.Count; i++)
        {
            _everyFrameActions[i].Invoke();
        }
        
        // 2프레임마다 실행
        if (_frameCount % 2 == 0)
        {
            for (int i = 0; i < _everySecondFrameActions.Count; i++)
            {
                _everySecondFrameActions[i].Invoke();
            }
        }
        
        // 5프레임마다 실행
        if (_frameCount % 5 == 0)
        {
            for (int i = 0; i < _everyFifthFrameActions.Count; i++)
            {
                _everyFifthFrameActions[i].Invoke();
            }
        }
        
        // 1초마다 실행
        if (_secondCounter >= 1.0f)
        {
            _secondCounter = 0;
            
            for (int i = 0; i < _oncePerSecondActions.Count; i++)
            {
                _oncePerSecondActions[i].Invoke();
            }
        }
    }
}

사용 예시:

private void Start()
{
    // 매 프레임 업데이트가 필요한 플레이어 이동
    UpdateManager.Instance.RegisterAction(UpdatePlayerMovement, UpdateManager.UpdateFrequency.EveryFrame);
    
    // 2프레임마다 업데이트해도 충분한 UI
    UpdateManager.Instance.RegisterAction(UpdateUI, UpdateManager.UpdateFrequency.EverySecondFrame);
    
    // 5프레임마다 업데이트하는 AI 로직
    UpdateManager.Instance.RegisterAction(UpdateAI, UpdateManager.UpdateFrequency.EveryFifthFrame);
    
    // 1초마다 업데이트하는 게임 시간
    UpdateManager.Instance.RegisterAction(UpdateGameTime, UpdateManager.UpdateFrequency.OncePerSecond);
}

private void OnDestroy()
{
    // 등록된 액션 제거
    UpdateManager.Instance.UnregisterAction(UpdatePlayerMovement);
    UpdateManager.Instance.UnregisterAction(UpdateUI);
    UpdateManager.Instance.UnregisterAction(UpdateAI);
    UpdateManager.Instance.UnregisterAction(UpdateGameTime);
}

오브젝트 풀링과 생명주기 최적화

오브젝트 풀링은 Instantiate()Destroy() 호출의 오버헤드를 줄여 성능을 향상시키는 중요한 기법입니다. 이는 생명주기 메서드와 밀접하게 관련됩니다.

고급 오브젝트 풀 구현 예제

public class AdvancedObjectPool : MonoBehaviour
{
    [System.Serializable]
    public class Pool
    {
        public string poolName;
        public GameObject prefab;
        public int initialSize;
        [Tooltip("풀이 부족할 때 자동으로 확장할지 여부")]
        public bool autoExpand = true;
        [Tooltip("자동 확장 시 한 번에 추가할 오브젝트 수")]
        public int expandSize = 5;
        [Tooltip("최대 유지할 오브젝트 수 (0 = 무제한)")]
        public int maxSize = 0;
        [HideInInspector]
        public Queue<PooledObject> queue = new Queue<PooledObject>();
    }
    
    // 풀링된 오브젝트를 위한 컴포넌트
    public class PooledObject : MonoBehaviour
    {
        public Pool parentPool;
        
        private IPooledObject[] _pooledBehaviours;
        
        private void Awake()
        {
            // IPooledObject 인터페이스를 구현한 모든 컴포넌트 찾기
            _pooledBehaviours = GetComponentsInChildren<IPooledObject>(true);
        }
        
        public void OnGetFromPool()
        {
            // 모든 IPooledObject 구현체에 풀에서 꺼내진 이벤트 알림
            foreach (var behaviour in _pooledBehaviours)
            {
                behaviour.OnObjectSpawn();
            }
        }
        
        public void OnReturnToPool()
        {
            // 모든 IPooledObject 구현체에 풀로 돌아가는 이벤트 알림
            foreach (var behaviour in _pooledBehaviours)
            {
                behaviour.OnObjectDespawn();
            }
        }
    }
    
    // 풀링된 오브젝트의 행동을 정의하는 인터페이스
    public interface IPooledObject
    {
        void OnObjectSpawn();
        void OnObjectDespawn();
    }
    
    [SerializeField] private List<Pool> _pools = new List<Pool>();
    private Dictionary<string, Pool> _poolDictionary = new Dictionary<string, Pool>();
    
    private static AdvancedObjectPool _instance;
    public static AdvancedObjectPool Instance
    {
        get
        {
            if (_instance == null)
            {
                _instance = FindObjectOfType<AdvancedObjectPool>();
                
                if (_instance == null)
                {
                    GameObject obj = new GameObject("AdvancedObjectPool");
                    _instance = obj.AddComponent<AdvancedObjectPool>();
                    DontDestroyOnLoad(obj);
                }
            }
            return _instance;
        }
    }
    
    private void Awake()
    {
        if (_instance != null && _instance != this)
        {
            Destroy(gameObject);
            return;
        }
        
        _instance = this;
        DontDestroyOnLoad(gameObject);
        
        // 풀 초기화
        InitializePools();
    }
    
    private void InitializePools()
    {
        foreach (Pool pool in _pools)
        {
            _poolDictionary[pool.poolName] = pool;
            
            // 초기 오브젝트 생성
            for (int i = 0; i < pool.initialSize; i++)
            {
                CreateNewPooledObject(pool);
            }
        }
    }
    
    private PooledObject CreateNewPooledObject(Pool pool)
    {
        GameObject obj = Instantiate(pool.prefab, transform);
        obj.SetActive(false);
        
        PooledObject pooledObj = obj.GetComponent<PooledObject>();
        if (pooledObj == null)
        {
            pooledObj = obj.AddComponent<PooledObject>();
        }
        
        pooledObj.parentPool = pool;
        pool.queue.Enqueue(pooledObj);
        
        return pooledObj;
    }
    
    public GameObject SpawnFromPool(string poolName, Vector3 position, Quaternion rotation)
    {
        if (!_poolDictionary.ContainsKey(poolName))
        {
            Debug.LogWarning($"풀 '{poolName}'이 존재하지 않습니다!");
            return null;
        }
        
        Pool pool = _poolDictionary[poolName];
        
        // 풀이 비었을 때 자동 확장
        if (pool.queue.Count == 0)
        {
            if (pool.autoExpand)
            {
                // 최대 크기 확인
                if (pool.maxSize > 0 && pool.queue.Count + pool.expandSize > pool.maxSize)
                {
                    Debug.LogWarning($"풀 '{poolName}'이 최대 크기에 도달했습니다!");
                    return null;
                }
                
                // 확장
                for (int i = 0; i < pool.expandSize; i++)
                {
                    CreateNewPooledObject(pool);
                }
                
                Debug.Log($"풀 '{poolName}'을 {pool.expandSize}만큼 확장했습니다.");
            }
            else
            {
                Debug.LogWarning($"풀 '{poolName}'이 비었으나 자동 확장이 꺼져 있습니다!");
                return null;
            }
        }
        
        // 풀에서 오브젝트 가져오기
        PooledObject obj = pool.queue.Dequeue();
        
        // 오브젝트가 파괴되었는지 확인 (씬 전환 등으로 인해)
        if (obj == null)
        {
            // 새 오브젝트 생성
            obj = CreateNewPooledObject(pool);
            pool.queue.Dequeue(); // 새로 생성된 오브젝트 큐에서 제거
        }
        
        // 위치와 회전 설정
        obj.transform.position = position;
        obj.transform.rotation = rotation;
        
        // 오브젝트 활성화
        obj.gameObject.SetActive(true);
        
        // OnObjectSpawn 호출
        obj.OnGetFromPool();
        
        return obj.gameObject;
    }
    
    public void ReturnToPool(GameObject obj)
    {
        PooledObject pooledObj = obj.GetComponent<PooledObject>();
        
        if (pooledObj == null || pooledObj.parentPool == null)
        {
            Debug.LogWarning("이 오브젝트는 풀에서 가져온 것이 아닙니다!");
            return;
        }
        
        // OnObjectDespawn 호출
        pooledObj.OnReturnToPool();
        
        // 오브젝트 비활성화
        obj.SetActive(false);
        
        // 풀에 반환
        pooledObj.parentPool.queue.Enqueue(pooledObj);
    }
}

구현 예시 - 발사체 컴포넌트:

public class Projectile : MonoBehaviour, AdvancedObjectPool.IPooledObject
{
    [SerializeField] private float _speed = 10f;
    [SerializeField] private float _lifetime = 3f;
    
    private Rigidbody _rigidbody;
    private TrailRenderer _trailRenderer;
    private float _timer;
    
    private void Awake()
    {
        _rigidbody = GetComponent<Rigidbody>();
        _trailRenderer = GetComponent<TrailRenderer>();
    }
    
    // 풀에서 꺼내질 때 호출
    public void OnObjectSpawn()
    {
        _timer = 0;
        
        // 발사 방향으로 속도 설정
        _rigidbody.velocity = transform.forward * _speed;
        
        // 트레일 리셋
        if (_trailRenderer != null)
        {
            _trailRenderer.Clear();
        }
    }
    
    // 풀로 돌아갈 때 호출
    public void OnObjectDespawn()
    {
        // 속도 리셋
        _rigidbody.velocity = Vector3.zero;
        _rigidbody.angularVelocity = Vector3.zero;
    }
    
    private void Update()
    {
        _timer += Time.deltaTime;
        
        // 수명이 다하면 풀로 반환
        if (_timer >= _lifetime)
        {
            AdvancedObjectPool.Instance.ReturnToPool(gameObject);
        }
    }
    
    private void OnCollisionEnter(Collision collision)
    {
        // 충돌 효과 생성
        Vector3 hitPoint = collision.contacts[0].point;
        AdvancedObjectPool.Instance.SpawnFromPool("HitEffect", hitPoint, Quaternion.identity);
        
        // 풀로 반환
        AdvancedObjectPool.Instance.ReturnToPool(gameObject);
    }
}

실전 활용 패턴

생명주기와 의존성 주입

의존성 주입(Dependency Injection)은 컴포넌트 간의 결합도를 낮추고 테스트 가능성을 높이는 중요한 디자인 패턴입니다. Unity의 생명주기와 결합하면 더 유연하고 유지보수하기 쉬운 아키텍처를 만들 수 있습니다.

// 서비스 로케이터 패턴과 생명주기를 결합한 의존성 관리자
public class ServiceLocator : MonoBehaviour
{
    private static Dictionary<Type, object> _services = new Dictionary<Type, object>();
    
    // Awake에서 초기화
    private void Awake()
    {
        DontDestroyOnLoad(gameObject);
        
        // 기본 서비스 등록
        RegisterService<IInputService>(new InputService());
        RegisterService<IAudioService>(new AudioService());
        RegisterService<IDataService>(new PlayerPrefsDataService());
        
        // 생명주기 이벤트를 사용해 서비스 초기화
        StartCoroutine(InitializeServicesSequentially());
    }
    
    // 서비스 순차 초기화 (의존성 순서 고려)
    private IEnumerator InitializeServicesSequentially()
    {
        // 데이터 서비스 먼저 초기화
        IDataService dataService = GetService<IDataService>();
        yield return StartCoroutine(dataService.Initialize());
        
        // 데이터 로드 후 설정 서비스 초기화
        var settings = dataService.LoadGameSettings();
        RegisterService<ISettingsService>(new SettingsService(settings));
        
        // 나머지 서비스 초기화
        foreach (var service in _services.Values.OfType<IInitializableService>())
        {
            yield return StartCoroutine(service.Initialize());
        }
        
        Debug.Log("모든 서비스 초기화 완료");
    }
    
    // OnApplicationQuit 생명주기 메서드에서 서비스 정리
    private void OnApplicationQuit()
    {
        foreach (var service in _services.Values.OfType<IDisposableService>())
        {
            service.Dispose();
        }
        
        _services.Clear();
    }
    
    // 서비스 등록
    public static void RegisterService<T>(T service)
    {
        _services[typeof(T)] = service;
    }
    
    // 서비스 조회
    public static T GetService<T>()
    {
        if (_services.TryGetValue(typeof(T), out object service))
        {
            return (T)service;
        }
        
        Debug.LogError($"서비스를 찾을 수 없음: {typeof(T).Name}");
        return default;
    }
}

// 서비스 인터페이스
public interface IInitializableService
{
    IEnumerator Initialize();
}

public interface IDisposableService
{
    void Dispose();
}

// 입력 서비스 예시
public interface IInputService : IInitializableService, IDisposableService
{
    Vector2 MovementInput { get; }
    bool JumpPressed { get; }
    event Action OnFireAction;
}

// 입력 서비스 구현
public class InputService : IInputService
{
    public Vector2 MovementInput { get; private set; }
    public bool JumpPressed { get; private set; }
    public event Action OnFireAction;
    
    public IEnumerator Initialize()
    {
        Debug.Log("입력 서비스 초기화 중...");
        // 초기화 로직
        yield return null;
    }
    
    public void Update()
    {
        // 입력 처리
        MovementInput = new Vector2(Input.GetAxis("Horizontal"), Input.GetAxis("Vertical"));
        JumpPressed = Input.GetButtonDown("Jump");
        
        if (Input.GetButtonDown("Fire1"))
        {
            OnFireAction?.Invoke();
        }
    }
    
    public void Dispose()
    {
        // 정리 로직
    }
}

사용 예시 - 플레이어 컨트롤러:

public class PlayerController : MonoBehaviour
{
    [SerializeField] private float _moveSpeed = 5f;
    [SerializeField] private float _jumpForce = 5f;
    
    private IInputService _inputService;
    private Rigidbody _rigidbody;
    private bool _isJumping = false;
    
    // 생명주기 메서드 Start에서 의존성 해결
    private void Start()
    {
        _rigidbody = GetComponent<Rigidbody>();
        

```html
        // 서비스 로케이터에서 입력 서비스 가져오기
        _inputService = ServiceLocator.GetService<IInputService>();
        
        // 이벤트 구독
        _inputService.OnFireAction += FireWeapon;
    }
    
    // OnDestroy 생명주기 메서드에서 이벤트 구독 해제
    private void OnDestroy()
    {
        if (_inputService != null)
        {
            _inputService.OnFireAction -= FireWeapon;
        }
    }
    
    private void Update()
    {
        // 점프 입력 감지
        if (_inputService.JumpPressed && !_isJumping)
        {
            _isJumping = true;
        }
    }
    
    private void FixedUpdate()
    {
        // 이동 처리
        Vector3 movement = new Vector3(_inputService.MovementInput.x, 0, _inputService.MovementInput.y);
        _rigidbody.MovePosition(_rigidbody.position + movement * _moveSpeed * Time.fixedDeltaTime);
        
        // 점프 처리
        if (_isJumping)
        {
            _rigidbody.AddForce(Vector3.up * _jumpForce, ForceMode.Impulse);
            _isJumping = false;
        }
    }
    
    private void FireWeapon()
    {
        // 무기 발사 로직
        Debug.Log("무기 발사!");
        
        // 풀링된 발사체 생성
        AdvancedObjectPool.Instance.SpawnFromPool("Projectile", transform.position + transform.forward, transform.rotation);
    }
}

복합 생명주기 관리 시스템

큰 게임에서는 다양한 시스템이 서로 다른 생명주기 단계에서 초기화되고 업데이트되어야 합니다. 이를 효과적으로 관리하기 위한 복합 생명주기 관리 시스템을 구현할 수 있습니다.

// 게임 시스템의 생명주기 단계
public enum SystemLifecycleStage
{
    PreInitialize,
    Initialize,
    PostInitialize,
    EarlyUpdate,
    Update,
    LateUpdate,
    PreRender,
    PostRender,
    PreShutdown,
    Shutdown
}

// 게임 시스템 인터페이스
public interface IGameSystem
{
    string SystemName { get; }
    int InitializationPriority { get; }
    int UpdatePriority { get; }
    bool IsInitialized { get; }
    
    void OnRegister();
    void Initialize();
    void Shutdown();
}

// 업데이트 가능한 시스템 인터페이스
public interface IUpdatableSystem : IGameSystem
{
    void OnEarlyUpdate();
    void OnUpdate();
    void OnLateUpdate();
}

// 렌더링 관련 시스템 인터페이스
public interface IRenderSystem : IGameSystem
{
    void OnPreRender();
    void OnPostRender();
}

// 생명주기 관리자 클래스
public class SystemLifecycleManager : MonoBehaviour
{
    private List<IGameSystem> _registeredSystems = new List<IGameSystem>();
    private List<IUpdatableSystem> _updatableSystems = new List<IUpdatableSystem>();
    private List<IRenderSystem> _renderSystems = new List<IRenderSystem>();
    
    private bool _isInitialized = false;
    private bool _isShuttingDown = false;
    
    private static SystemLifecycleManager _instance;
    public static SystemLifecycleManager Instance
    {
        get
        {
            if (_instance == null)
            {
                GameObject go = new GameObject("SystemLifecycleManager");
                _instance = go.AddComponent<SystemLifecycleManager>();
                DontDestroyOnLoad(go);
            }
            return _instance;
        }
    }
    
    private void Awake()
    {
        if (_instance != null && _instance != this)
        {
            Destroy(gameObject);
            return;
        }
        
        _instance = this;
        DontDestroyOnLoad(gameObject);
    }
    
    private void Start()
    {
        StartCoroutine(InitializationSequence());
    }
    
    private IEnumerator InitializationSequence()
    {
        Debug.Log("시스템 초기화 시작...");
        
        // 등록된 시스템을 초기화 우선순위에 따라 정렬
        _registeredSystems.Sort((a, b) => a.InitializationPriority.CompareTo(b.InitializationPriority));
        
        // 시스템 등록 이벤트 호출
        foreach (var system in _registeredSystems)
        {
            system.OnRegister();
            yield return null; // 프레임 단위로 분산
        }
        
        // PreInitialize 단계 이벤트 발생
        OnLifecycleStage(SystemLifecycleStage.PreInitialize);
        yield return null;
        
        // 각 시스템 초기화
        foreach (var system in _registeredSystems)
        {
            Debug.Log($"시스템 초기화 중: {system.SystemName}");
            system.Initialize();
            yield return null; // 프레임 단위로 분산
        }
        
        // Initialize 단계 이벤트 발생
        OnLifecycleStage(SystemLifecycleStage.Initialize);
        yield return null;
        
        // 업데이트 가능 시스템 필터링 및 정렬
        _updatableSystems = _registeredSystems.OfType<IUpdatableSystem>().ToList();
        _updatableSystems.Sort((a, b) => a.UpdatePriority.CompareTo(b.UpdatePriority));
        
        // 렌더링 시스템 필터링
        _renderSystems = _registeredSystems.OfType<IRenderSystem>().ToList();
        
        // PostInitialize 단계 이벤트 발생
        OnLifecycleStage(SystemLifecycleStage.PostInitialize);
        
        _isInitialized = true;
        Debug.Log("모든 시스템 초기화 완료");
    }
    
    private void Update()
    {
        if (!_isInitialized || _isShuttingDown)
            return;
            
        // EarlyUpdate 호출
        OnLifecycleStage(SystemLifecycleStage.EarlyUpdate);
        foreach (var system in _updatableSystems)
        {
            system.OnEarlyUpdate();
        }
        
        // Update 호출
        OnLifecycleStage(SystemLifecycleStage.Update);
        foreach (var system in _updatableSystems)
        {
            system.OnUpdate();
        }
    }
    
    private void LateUpdate()
    {
        if (!_isInitialized || _isShuttingDown)
            return;
            
        // LateUpdate 호출
        OnLifecycleStage(SystemLifecycleStage.LateUpdate);
        foreach (var system in _updatableSystems)
        {
            system.OnLateUpdate();
        }
    }
    
    private void OnPreRender()
    {
        if (!_isInitialized || _isShuttingDown)
            return;
            
        // PreRender 호출
        OnLifecycleStage(SystemLifecycleStage.PreRender);
        foreach (var system in _renderSystems)
        {
            system.OnPreRender();
        }
    }
    
    private void OnPostRender()
    {
        if (!_isInitialized || _isShuttingDown)
            return;
            
        // PostRender 호출
        OnLifecycleStage(SystemLifecycleStage.PostRender);
        foreach (var system in _renderSystems)
        {
            system.OnPostRender();
        }
    }
    
    private void OnApplicationQuit()
    {
        StartCoroutine(ShutdownSequence());
    }
    
    private IEnumerator ShutdownSequence()
    {
        if (_isShuttingDown)
            yield break;
            
        _isShuttingDown = true;
        
        // PreShutdown 단계 이벤트 발생
        OnLifecycleStage(SystemLifecycleStage.PreShutdown);
        yield return null;
        
        // 시스템 종료 (역순)
        for (int i = _registeredSystems.Count - 1; i >= 0; i--)
        {
            var system = _registeredSystems[i];
            Debug.Log($"시스템 종료 중: {system.SystemName}");
            system.Shutdown();
            yield return null;
        }
        
        // Shutdown 단계 이벤트 발생
        OnLifecycleStage(SystemLifecycleStage.Shutdown);
        
        _registeredSystems.Clear();
        _updatableSystems.Clear();
        _renderSystems.Clear();
        
        Debug.Log("모든 시스템 종료 완료");
    }
    
    // 시스템 등록
    public void RegisterSystem(IGameSystem system)
    {
        if (!_registeredSystems.Contains(system))
        {
            _registeredSystems.Add(system);
            Debug.Log($"시스템 등록: {system.SystemName}");
            
            // 이미 초기화된 상태라면 즉시 초기화
            if (_isInitialized && !system.IsInitialized)
            {
                system.OnRegister();
                system.Initialize();
                
                // 업데이트 가능 시스템이면 목록에 추가
                if (system is IUpdatableSystem updatableSystem)
                {
                    _updatableSystems.Add(updatableSystem);
                    _updatableSystems.Sort((a, b) => a.UpdatePriority.CompareTo(b.UpdatePriority));
                }
                
                // 렌더링 시스템이면 목록에 추가
                if (system is IRenderSystem renderSystem)
                {
                    _renderSystems.Add(renderSystem);
                }
            }
        }
    }
    
    // 시스템 제거
    public void UnregisterSystem(IGameSystem system)
    {
        if (_registeredSystems.Contains(system))
        {
            // 종료 호출
            system.Shutdown();
            
            // 목록에서 제거
            _registeredSystems.Remove(system);
            
            if (system is IUpdatableSystem updatableSystem)
            {
                _updatableSystems.Remove(updatableSystem);
            }
            
            if (system is IRenderSystem renderSystem)
            {
                _renderSystems.Remove(renderSystem);
            }
            
            Debug.Log($"시스템 제거: {system.SystemName}");
        }
    }
    
    // 생명주기 이벤트 발생
    private void OnLifecycleStage(SystemLifecycleStage stage)
    {
        // 여기에 다른 시스템이나 메시지 시스템에 알림을 보낼 수 있음
        Debug.Log($"생명주기 단계 진행: {stage}");
    }
}

구현 예시 - 플레이어 시스템:

public class PlayerSystem : MonoBehaviour, IUpdatableSystem
{
    public string SystemName => "PlayerSystem";
    public int InitializationPriority => 100; // 낮은 값이 먼저 초기화
    public int UpdatePriority => 10; // 낮은 값이 먼저 업데이트
    public bool IsInitialized { get; private set; }
    
    private PlayerController _playerController;
    
    private void Awake()
    {
        // 라이프사이클 매니저에 등록
        SystemLifecycleManager.Instance.RegisterSystem(this);
    }
    
    public void OnRegister()
    {
        Debug.Log("플레이어 시스템 등록됨");
    }
    
    public void Initialize()
    {
        Debug.Log("플레이어 시스템 초기화 중...");
        
        // 플레이어 생성 또는 찾기
        _playerController = FindObjectOfType<PlayerController>();
        if (_playerController == null)
        {
            GameObject playerObj = Resources.Load<GameObject>("Player");
            if (playerObj != null)
            {
                _playerController = Instantiate(playerObj).GetComponent<PlayerController>();
            }
        }
        
        IsInitialized = true;
    }
    
    public void OnEarlyUpdate()
    {
        // 플레이어 입력 처리 등 빠른 업데이트가 필요한 작업
    }
    
    public void OnUpdate()
    {
        // 기본 업데이트 작업
        if (_playerController != null)
        {
            // 플레이어 관련 업데이트 로직
        }
    }
    
    public void OnLateUpdate()
    {
        // 카메라 추적, 애니메이션 후처리 등
    }
    
    public void Shutdown()
    {
        Debug.Log("플레이어 시스템 종료 중...");
        IsInitialized = false;
    }
    
    private void OnDestroy()
    {
        // 라이프사이클 매니저에서 제거
        if (SystemLifecycleManager.Instance != null)
        {
            SystemLifecycleManager.Instance.UnregisterSystem(this);
        }
    }
}

마치며

Unity의 생명주기를 깊이 이해하는 것은 효율적이고 안정적인 게임 개발의 기초가 됩니다. 각 생명주기 메서드의 특성과 호출 시점을 정확히 파악하면, 의도한 대로 코드가 실행되도록 보장하고 성능을 최적화할 수 있습니다.

본 글에서 심층적으로 살펴본 내용을 요약하면:

  1. 생명주기 구조 이해: Unity 오브젝트의 생명주기는 초기화, 물리 연산, 메인 업데이트, 렌더링, 비활성화/파괴 단계로 나뉩니다.
  2. 코루틴과 생명주기 통합: 코루틴은 생명주기 메서드와 밀접하게 연동되어 분산 처리의 핵심 도구로 활용됩니다.
  3. 성능 최적화 전략: 올바른 생명주기 메서드 선택과 업데이트 빈도 조절을 통해 성능을 크게 향상시킬 수 있습니다.
  4. 오브젝트 풀링과 생명주기 활용: 오브젝트의 생성/파괴 비용을 줄이기 위한 풀링 시스템은 생명주기 이벤트를 효과적으로 활용합니다.
  5. 디자인 패턴과 생명주기 연동: 상태 패턴, 의존성 주입, 시스템 아키텍처는 생명주기 이벤트와 결합하여 확장성 있는 코드 구조를 만듭니다.
  6. 복합 시스템 관리: 대규모 게임에서는 여러 시스템의 생명주기를 효과적으로 관리하는 아키텍처가 필요합니다.

효과적인 생명주기 활용을 위한 핵심 요점을 다시 정리하면:

  • 목적에 맞는 메서드 선택: 물리 연산은 FixedUpdate(), 일반 게임 로직은 Update(), 후처리는 LateUpdate()에 배치
  • 초기화 로직 분리: 참조 설정은 Awake(), 다른 컴포넌트에 의존하는 초기화는 Start()에 배치
  • 이벤트 관리: 이벤트 리스너 등록/해제는 OnEnable()/OnDisable()에서 처리
  • 코루틴 활용: 분산 처리가 필요한 작업은 코루틴으로 구현하고, yield 객체는 캐싱하여 사용
  • 리소스 관리: 리소스 해제와 메모리 정리는 OnDisable()OnDestroy()에서 처리
  • 업데이트 최적화: 모든 로직을 매 프레임 업데이트하지 말고, 필요한 빈도에 맞게 분산 실행

이러한 원칙을 따르면서 설계하면, 더 안정적이고 유지보수하기 쉬운 코드를 작성할 수 있습니다. 유니티의 생명주기는 복잡하지만, 이를 마스터하면 게임 개발의 많은 문제를 우아하게 해결할 수 있습니다.

추가 학습 자료: Unity의 생명주기에 대해 더 자세히 알아보려면 Unity 공식 문서의 실행 순서 페이지를 참고하세요. 또한 Unity Profiler를 활용하여 생명주기 메서드의 실행 시간을 측정하고 최적화하는 방법을 익히는 것도 중요합니다.

```

반응형
LIST

'Unity' 카테고리의 다른 글

유니티 애널리틱스  (0) 2025.05.23
유니티의 생명주기  (0) 2025.05.21
FishNet 사용법  (0) 2025.05.20
Unity 렌더링 파이프라인  (0) 2025.05.19
유니티 쿼터니언(Quaternion)  (1) 2025.05.16