Unity/유니티 엔진

[Unity] - 코루틴(Coroutine)에 대한 이모저모

빗방울소리 2023. 4. 17. 09:03

 

 

유니티를 이용하여 게임 컨텐츠를 제작하다보면 코루틴을 자주 사용하게 된다. 코루틴은 비동기적인 작업을 쉽게 처리할 수 있게 해주는, 유니티에서 지원해주는 기능이다.

 

 

 

 개인적으로는 유니티를 사용하여 개발을 할 때 가장 잘 활용해야 하고 자주 사용해야 하는 것이 코루틴이라고 생각한다. 근데 코루틴이란 정확히 무엇인가? 우선 위키백과에는 아래와 같이 정의 되어있다.

 

 

 

코루틴(coroutine)은 루틴의 일종으로서, 협동 루틴이라 할 수 있다(코루틴의 "Co"는 with 또는 together를 뜻한다). 상호 연계 프로그램을 일컫는다고도 표현가능하다. 루틴과 서브 루틴은 서로 비대칭적인 관계이지만, 코루틴들은 완전히 대칭적인, 즉 서로가 서로를 호출하는 관계이다. 코루틴들에서는 무엇이 무엇의 서브루틴인지를 구분하는 것이 불가능하다. 코루틴 A와 B가 있다고 할 때, A를 프로그래밍 할 때는 B를 A의 서브루틴으로 생각한다. 그러나 B를 프로그래밍할 때는 A가 B의 서브루틴이라고 생각한다. 어떠한 코루틴이 발동될 때마다 해당 코루틴은 이전에 자신의 실행이 마지막으로 중단되었던 지점 다음의 장소에서 실행을 재개한다.

 

 

 

어떠한 코루틴이 발동될 때마다 해당 코루틴은 이전에 자신의 실행이 마지막으로 중단되었던 지점 다음의 장소에서 실행을 재개한다.

 

 

 

 정의가 길게 되어있는데 중요한 부분은 밑줄이 쳐진 부분이다. 코루틴은 일반적인 함수와 다르지 않으나 yield를 통해 일시적으로 작업을 멈추고 원래 자신을 호출했던 line으로 복귀하여 특정한 조건이 만족된 후에 yield를 통해 반환한 시점부터 다시 실행된다.

 

 

 

 

 

 

 

 함수의 경우 한번 반환하면 끝이고, 자신을 멈췄다가 특정 조건이 완료된 후에 다시 실행하는 것이 불가능하다. Thread Sleep을 이용해 어거지로 구현할 수 있지 않겠는가? 라고 의문을 가질 수 있지만 그건 명백히 코루틴과는 다르다. 이에 대해서는 뒤에서 더 알아보겠다.

 

 

 

 

 따라서 코루틴은 위의 이미지에서 보이는 것처럼 yield return 기능을 사용해 일시적으로 반환하여 다음 실행 시 마지막으로 실행했던 행부터 다시 실행이 가능하다.

 

 

 

 

IEnumerator Fade() {
    for (float f = 1f; f >= 0; f -= 0.1f) {
        Color c = renderer.material.color;
        c.a = f;
        renderer.material.color = c;
        yield return new WaitForSeconds(.1f);
    }
}

 

 유니티 공식문서에서 코루틴을 사용하는 예시 코드를 가져왔다. 0.1초 마다 color값을 변화시켜 fade 효과를 주는 함수로 보인다. 코드에서 보이는 것처럼 yield return 값의 뒤에 오는 반환 값을 이용해 어떤 조건에서 다시 코루틴을 실행 시킬 지 결정할 수 있다. 자주 쓰이는 것이 위의 코드에서 쓰인 WaitForSeconds이다. 지정된 시간 후에 다시 코루틴을 실행시키라는 의미로 이는 유니티가 동작하는 프레임과 별개로 실제 시간 이후에 실행되는 조건으로 스킬의 쿨타임 등을 구현하기에 용이하다.

 

 

 

 

 

 

 

 

 

 

 위에서 언급한 내용까지가 유니티에서 코루틴을 사용하고자 할 때 알아야 할 기본적인 특징이었다. 다만 이번 글에서는 코루틴의 동작원리에 대해 조금 더 정리해보려고 한다. 여태까지 위의 내용만을 가지고 유니티에서 코루틴을 사용하는 데에 불편함은 없었지만, 유니티의 핵심 축에 속하는 코루틴에 대해서는 좀 더 알아둘 필요가 있다고 느꼈기 때문이다.

 

 

 

 

 

 

IEnumerator 란 무엇인가?

 

 

 코루틴 함수를 사용하기 위해서는 반환 값으로 IEnumerator를 반환해야 한다. 이는 기계적으로 학습하고 써왔기에 따로 알아보지 않는 이상 IEnumerator는 뭐고 왜 이걸 반환해야 하는지도 알 수 없다. 또한 이런 의문도 들었다. Thread sleep을 이용해서도 코루틴과 비슷하게 동작하는 함수를 만들 수 있지 않을까? 무언가 몇 초 후에 다시 실행되어야 하는 함수라면 반복문을 걸어놓고 Thread sleep을 이용해 강제로 제어권을 반환하게끔 동작 시킬 수 있다. 정리하자면 두 가지의 해결해야할 의문이 있다고 볼 수 있다.

 

 

1) IEnumerator는 뭐고 왜 이것을 반환하는가?

2) Thread sleep을 안쓰고 코루틴을 쓰는 이유가 무엇인가?

 

 

 

 우선 첫 번째 의문을 해결하기 위해 정보를 찾아보았다. 

 

 

 

 

https://learn.microsoft.com/ko-kr/dotnet/api/system.collections.ienumerator?view=net-8.0 

 

IEnumerator 인터페이스 (System.Collections)

제네릭이 아닌 컬렉션을 단순하게 반복할 수 있도록 지원합니다.

learn.microsoft.com

 

 

.Net Framework 공식 문서에 정리가 되어있고, 요약하자면 제네릭이 아닌 컬렉션을 단순하게 반복할 수 있도록 지원하는 인터페이스이다. 

 

 

 

 

속성

Current 컬렉션에서 열거자의 현재 위치에 있는 요소를 가져옵니다.

메서드

MoveNext() 열거자를 컬렉션의 다음 요소로 이동합니다.
Reset() 컬렉션의 첫 번째 요소 앞의 초기 위치에 열거자를 설정합니다.

 

 

IEnumorator 는 위의 필드와 메서드를 구현하여 사용이 가능하다고 되어있다. 정리하자면 아래와 같다.

 

 

 

 

C#에서 IEnumerator 인터페이스는 반복 가능한 컬렉션의 요소를 열거하고, 컬렉션 내의 요소에 순차적으로 접근하기 위한 메커니즘을 제공한다. 이를 통해 foreach 루프와 같은 반복문을 사용하여 컬렉션의 요소에 접근할 수 있다.

 

 

 

 

 

 

 그럼 이것을 코루틴과 연관지어서 생각해 보자. 코루틴은 비동기적인 작업을 처리하기 위해 일시적으로 실행을 중지하고, 나중에 다시 실행되도록 하는 기능을 제공한다. 이 때, 코루틴은 일시 중지된 상태에서 대기하며, 특정 조건이 충족될 때까지 기다리게 된다. 이러한 동작을 가능하게 하기 위해 코루틴은 IEnumerator 인터페이스를 반환하는 함수로 작성되어야 한다는 것이다.

 

 

 

 IEnumerator 인터페이스를 반환하는 함수로 작성된 코루틴은 Unity 엔진에서 특정 조건이 충족되었을 때, 해당 조건에 따라 다음 yield 문으로 진행되거나, 실행이 종료되도록 제어할 수 있다. 이를 통해 비동기적인 작업을 조절하고, 게임 로직의 흐름을 제어할 수 있다.

 

 

 

 

 첫 번째 의문에 대한 답을 정리하자면 위와 같다고 할 수 있겠다. 코루틴이 실제로 어떤 방식으로 IEnumerator를 이용해 일시적으로 멈췄다가 특정 조건 이후에 다시 코루틴이 실행되도록 하는 지 까지는 코드를 뜯어 볼 순 없지만 특정 조건이 완료 되었을 때 IEnumerator의 moveNext와 같은 메서드를 이용하여 다음 실행을 이어 나간다는 사실 까지는 유추해 낼 수 있다.

 

 

 

 

 

 첫 번째 의문에 대한 답은 찾았으니 두 번째 의문에 대한 답을 찾아보자.

 

 

 

 

 

왜 굳이 코루틴을 쓸까?

 

 

 

 

 

 

 유니티 코드를 찾아보면 대부분의 비동기 작업은 코루틴으로 되어있다. 그에 대한 이유는 사실 간단하다. 코루틴이 쓰레드를 직접 제어하는 비동기 작업보다 훨씬 가볍기 때문이다. 

 

 

 

 

 왜 가벼운가? 우선 유니티는 기본적으로 단일 스레드 환경에서 동작한다. 게임 로직, 렌더링, 물리 엔진 등이 동일한 쓰레드에서 처리된다. (코루틴과 관련된 내용은 아니지만, 게임과 관련된 작업을 유니티의 쓰레드에서만 처리하게끔 되어있기 때문에 MonoBehavior를 상속받지 않은 별도의 클래스는 게임에 관여하지 못하게끔 되어있다.) 이러한 유니티의 구조상 멀티 스레딩을 이용하여 비동기 작업을 처리하는 것보다 효율적이다.

 

 

 

 

 게다가 유니티의 코루틴은 유니티 엔진에 적합하게 동작하도록 되어있어 오브젝트의 라이프사이클과 맞물려서 동작하여 게임 오브젝트를 켜고 끄거나 씬 전환, 플레이어 입력 등 작지만 자주 일어나면서 비동기적으로 처리해야 하는 작업들을 낮은 비용으로 처리가 가능하다.

 

 

 

 특히 스킬 쿨타임이나 플레이어의 입력 지연시간 등을 일일이 쓰레드를 할당해서 비동기 처리했다면 얼마나 비효율적으로 자원을 먹을지 가슴이 답답해질 것이다. 다만 앞서 언급하였던 것처럼 코루틴은 작지만 자주 일어나면서 비동기적으로 처리해야 하는 작업들에 적합한 것이지 계속해서 일정한 컴퓨터 자원을 할당해서 일을 해야하는 비교적 큰 일(네트워크 라던가... 네트워크 라던가... 네트워크 라던가...) 은 기존에 쓰던 멀티스레딩으로 처리해주는 것이 올바르다.

 

 

 

 사실 두 번째 질문인 '왜 굳이 코루틴을 쓸까?'와 관련된 내용들은 실제로 헤딩을 해봐야 느끼는 것들이라고 생각한다. 너무 이론적인 이야기기도 하고, 프로그램에서 특히나 게임에서 컴퓨터 자원관리 라는건 중요한 요소라고 생각하기 때문에 더욱 그런거 같다.

 

 

 

 

 

 코루틴은 유니티를 쓸 때 정말 조심히 정말 잘 써야하는 것이라고 생각한다. 코루틴에 대한 내용을 이런 글 하나로 정리하기엔 역부족이지만 이 정도면 유니티의 코루틴에 대해 말해보라고 할 때 대답할 수 있는 수준까지는 알아봤으므로 필요한 내용은 필요할 때 찾아보겠다.