[Unity] 유니티 코루틴 작동 원리 (Coroutine)

Donghee·2024년 3월 11일
0

코루틴이란?

우선 유니티 공식 API에서 설명하는 코루틴을 알아보자.

코루틴을 사용하면 작업을 다수의 프레임에 분산할 수 있습니다. Unity에서 코루틴은 실행을 일시 정지하고 제어를 Unity에 반환하지만 중단한 부분에서 다음 프레임을 계속할 수 있는 메서드입니다.

대부분의 경우 메서드를 호출하면 실행을 완료한 뒤 호출한 메서드에 제어와 선택적 반환 값을 반환합니다. 즉 메서드 내에서 발생한 모든 행동은 단일 프레임 업데이트 내에서 발생해야 합니다.

표현이 다소 난잡하지만, 정리해보면
1. 원래 유니티 메서드는 호출하면 단일 프레임 내에서 실행되고, 완료된다.
2. 하지만 코루틴은 한 작업을 다수의 프레임에 분산시킬 수 있다.
3. 실행을 일시정지하고 그 중단한 부분에서 다음 프레임을 계속할 수 있는 메서드이다.

코루틴의 사용

코루틴은 다음과 같은 규칙을 가지고 사용해야 한다.

  • IEnumerator를 반환해야 한다.
  • 메서드 구현부에 yield 문으로 반환해야한다.
    • yield break 문을 만나면 코루틴이 종료된다.
    • yield return 문을 만나면 그 다음 구문부터는 쪼개져 다른 콜백에서 호출된다.
  • 모든 코루틴은 MonoBehaviour를 상속하는 객체들로 인해 실행되고, 그 객체들에게 코루틴의 소유권이 있다고 표현한다.
  • 이 소유권을 가진 객체들이 비활성화되거나 파괴될 경우, 코루틴이 같이 정지한다.
  • StartCoroutine, StopCoroutine 등의 MonoBehaviour 메서드로 실행과 정지를 시킨다.

여기서 IEnumerator과 yield 문은 무엇이며, 이들을 이용해 어떻게 한 메서드를 쪼개 다수의 프레임에서 호출시킬 수 있는 것일까?

코루틴의 작동 원리

IEnumerator와 IEnumerable

IEnumerator는 다른 언어의 Iterator와 비슷한 역할을 하는 열거자이다. IEnumerator를 알기 위해서는 먼저 IEnumerable를 알아야 한다. 다음 예제를 보자.

static void Main()
{
    DaysOfTheWeek days = new DaysOfTheWeek();

    foreach (string day in days)
    {
        Console.Write(day + " ");
    }
    // Output: Sun Mon Tue Wed Thu Fri Sat
    Console.ReadKey();
}

public class DaysOfTheWeek : IEnumerable
{
    private string[] days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
	
    // IEnumerable 인터페이스를 구현
    public IEnumerator GetEnumerator()
    {
        for (int index = 0; index < days.Length; index++)
        {
            // Yield each day of the week.
            yield return days[index];
        }
    }
}

DaysOfTheWeek 클래스는 IEnumerable 인터페이스를 구현한다. 여기서 구현해야 하는 메서드는 GetEnumerator로, 반환값이 IEnumerator이다. DaysOfTheWeek의 인스턴스인 days를 foreach 문을 통해 순회하는 모습을 보여주는데, 이는 IEnumerable을 통해 GetEnumerator를 구현했기 때문이다.

public interface IEnumerator
{
    object Current { get; }
    bool MoveNext();
    void Reset();
}

다음은 IEnumerator의 선언 모습이다. 열거자의 현재 위치가 저장된 Current, 다음으로 이동하는 MoveNext 메서드가 존재한다. IEnumerator 인스턴스가 MoveNext를 명시적으로 표현하는 경우는 드물고, 대부분 암시적으로 foreach문 등에서 실행되며 Current가 업데이트된다.

IEnumerator를 반환값으로 삼을 때는 yield 키워드를 사용해 반환한다. yield 키워드는 이 메서드를 호출한 호출자에게 이 컬렉션의 데이터를 하나씩 반환해주는 역할을 한다.

유니티에서 IEnumerator

유니티에서, 정확히는 MonoBehaviour를 상속받은 스크립트에서 IEnumerator 메서드는 yield 키워드를 통해 제어 권한을 유니티에 반환했다가, yield 반환문 조건 충족 후 다시 받아와 나머지 구문을 실행하는 특별한 메서드이다. 이를 유니티와 우리는 코루틴이라고 부르는 것이고, StartCoroutine이라는 독특한 방법을 통해 실행해야 한다.
yield 키워드를 통해 나머지 구문을 실행시킬 수 있는 이유는, yield return 문이 컴파일시 코드의 위치를 저장하는 상태 머신으로 변환되기 때문이다. 그 후 YieldInstruction (null, WaitForSeconds 등..)을 실행하고 MoveNext를 다시 호출한다. 즉, 코루틴은 MoveNext문이 실행되고, Current인 yield return 문이 실행되고, 다시 MoveNext문이 실행되는 것을 반복하는 것과 같다.

예제 코드로 살펴보자.

class TestEnumerator : IEnumerator
{
    public object Current => new WaitForSeconds(1);
		
    public bool MoveNext()
    {
        Debug.Log(Time.time);
        return true;
    }

    public void Reset()
    {
    }
}
IEnumerator Coroutine()
{
	while(true){
		Debug.Log(Time.time);
		yield return new WaitForSeconds(1);
	}
}

위 두개의 코드는 StartCoroutine로 코루틴을 시작시킬 시 같은 효과를 가진다.

코루틴 시작 -> yield return문 나오기 전까지 실행(MoveNext) -> yield return문(Current)으로 일시정지 -> yield return문 나오기 전까지 실행 -> 이를 반복

코루틴에 대한 프로파일링


유니티 프로파일러로 코루틴 호출을 분석해보자. DelayedCallManager로 yield return문의 조건이 끝나고 실행될 MoveNext, 지금 Update에서 호출해 실행하고 있는 코루틴의 MoveNext와 yield return문 (Current), 두 가지의 호출로 프로파일링 된다.

결론

결국 코루틴은 열거자를 이용해 여러 프레임에서 호출되는 듯한 효과를 발휘하는 메서드이다. 절대 멀티 스레드가 아니라는 사실을 기억하고, 다른 메서드와 같이 메인 스레드에서 동작하므로 무한 루프나 오래 실행하는 작업에 주의해야 한다.


레퍼런스

profile
마포고개발짱

0개의 댓글