[Unity] 코루틴과 IEnumerator, Heap
Unity 코루틴은 어떻게 동작할까 — IEnumerator, 상태머신, 그리고 힙
Unity로 이동/구르기 같은 걸 만들다 보면 StartCoroutine을 자주 쓰게 됩니다.
저는 처음에 "코루틴 = 가벼운 스레드" 정도로 막연히 알고 있었습니다.
그런데 코루틴 안에 브레이크포인트를 걸었더니 콜스택이 이상하게 나오고, 스텝 실행도 엉뚱하게 튀어서 "이게 대체 내부적으로 뭐지?" 싶어 파보게 됐습니다.
제가 이해한 바를 정리해봤습니다. 틀린 부분이 있으면 알려주시면 감사하겠습니다.
결론부터 3줄로 요약하면 다음과 같습니다.
- 코루틴은 스레드가 아니라, Unity가 매 프레임
MoveNext()를 불러주는 상태머신 함수입니다. yield에서 멈출 때 지역변수를 살려두려고 컴파일러가 상태를 힙 객체로 옮깁니다.- 그래서 실행이 프레임 단위로 쪼개지고, 디버깅이 일반 함수처럼 매끄럽지 않습니다.
1. 먼저, 코루틴은 스레드가 아니다
C++을 하다 오면 "동시에 뭔가 진행된다"는 말에 자연스럽게 스레드를 떠올리게 됩니다.
Unity에서 우리가 짠 게임 로직(Update, 코루틴, 대부분의 Unity API)은 사실 전부 하나의 메인 스레드에서 돕니다. Unity API가 스레드 안전하지 않아서, 아예 한 스레드로 몰아둔 것으로 이해하고 있습니다.
그 메인 스레드가 하는 일은 큰 반복문 하나입니다. 한 바퀴가 한 프레임입니다.
while (게임 실행 중) // 한 바퀴 = 1 프레임
{
입력 처리
각 MonoBehaviour.Update() 호출
실행 중인 코루틴들 MoveNext() 호출 // ← 코루틴이 여기서 한 조각씩
물리 / 애니메이션
렌더링
}
즉 코루틴은 별도 스레드에서 병렬로 도는 게 아니라, 이 단일 스레드 루프 안에서 "한 작업을 여러 프레임에 나눠 조금씩" 진행하는 도구라고 이해했습니다.
1-1. 참고: 스레드와 코어
스레드를 깊게 다루지는 않고, 코루틴과의 차이를 짚을 만큼만 간단히 정리하겠습니다.
CPU 안에는 코어(ALU, 레지스터, CU 묶음)가 여러 개 들어 있고, 하나의 코어는 한 순간에 스레드 하나의 명령어만 실행한다고 이해하고 있습니다. 스레드가 코어보다 많아도, OS 스케줄러가 컨텍스트 스위칭으로 번갈아 실행시켜서 동시에 도는 것처럼 보이는 것입니다.
@startuml
skinparam backgroundColor #111217
skinparam defaultFontColor #F5F5F5
skinparam ArrowColor #FFB86C
skinparam componentBorderColor #BD93F9
skinparam componentBackgroundColor #1E1B2E
skinparam componentFontColor #F5F5F5
skinparam packageBorderColor #FFB86C
skinparam packageFontColor #F5F5F5
skinparam noteBackgroundColor #1E1B2E
skinparam noteBorderColor #FFB86C
skinparam noteFontColor #F5F5F5
title 제가 이해한 스레드와 코어
package "CPU" {
[코어 1] as Core
}
package "OS 스케줄러" {
[스레드 A] as TA
[스레드 B] as TB
[스레드 C] as TC
}
TA ..> Core : 배치
TB ..> Core : 번갈아 배치
TC ..> Core : 번갈아 배치
note right of Core : 코어는 한 순간에 스레드 하나만 실행\n컨텍스트 스위칭으로 번갈아 → 동시처럼 보임
@enduml
코루틴은 이 그림처럼 스레드를 늘리는 방식이 아닙니다. 메인 스레드 하나 안에서 프레임마다 나눠 실행하는 것이라, 컨텍스트 스위칭도 없고 진짜 병렬도 아닙니다. 이 차이가 뒤에서 계속 중요합니다.
2. IEnumerator가 뭔가 (그리고 왜 코루틴이 이걸 재사용하는가)
코루틴 함수의 반환형이 IEnumerator인데, 사실 이건 코루틴을 위해 만들어진 타입이 아닙니다. 원래는 컬렉션을 하나씩 순회하기 위한 "반복(iteration) 인터페이스" 입니다. 그런데 이 인터페이스의 성질이 코루틴이 필요로 하는 것과 정확히 맞아떨어져서 재사용된 것으로 이해했습니다. 이 절에서 그 이유를 짚어보겠습니다.
2.1 원래 IEnumerator는 "반복 인터페이스"입니다
핵심은 두 가지입니다.
public interface IEnumerator
{
bool MoveNext(); // 다음 요소로 전진. 남았으면 true, 끝났으면 false
object Current; // 현재 요소 (방금 내놓은 값)
void Reset();
}
여기서 중요한 성질이 하나 있습니다. MoveNext()를 부를 때마다 필요한 만큼만 한 걸음 전진한다는 점입니다. 미리 전부 계산해두는 게 아니라, 요청받을 때(pull) 하나씩 만들어내는 지연 실행(lazy) 방식입니다.
2.2 순수 C# 예시 — 지연 실행과 상태 보존
Unity를 떠나서 순수 C#으로 보면 감이 더 잡힙니다. 무한 피보나치 수열을 yield로 표현해보겠습니다.
// 무한 수열인데도 프로그램이 멈추지 않는다. 값을 "요청할 때만" 하나씩 계산하기 때문이다.
IEnumerable<int> Fibonacci()
{
int a = 0, b = 1;
while (true)
{
yield return a; // 값 하나를 내놓고 여기서 멈춘다
(a, b) = (b, a + b); // 다음 MoveNext() 때 이 줄부터 재개
}
}
이걸 직접 손으로 굴려보면 성질이 드러납니다.
IEnumerator<int> e = Fibonacci().GetEnumerator();
e.MoveNext(); // Current = 0 (a=0까지 계산하고 멈춤)
e.MoveNext(); // Current = 1
e.MoveNext(); // Current = 1
e.MoveNext(); // Current = 2 ... 필요할 때 하나씩만 계산
두 가지가 보입니다.
- 필요한 만큼만 계산됩니다. 무한 수열이어도
MoveNext()를 부른 횟수만큼만 진행합니다. MoveNext()사이에a,b값이 그대로 유지됩니다. 즉 이 반복자는 "멈췄다 재개하면서 상태를 기억하는 객체"입니다.
바로 이 두 성질이 코루틴이 원하던 것과 같습니다.
2.3 IEnumerable과 IEnumerator, 그리고 foreach
한 가지 구분만 짚겠습니다. foreach가 도는 대상은 IEnumerable이고, 실제 순회를 담당하는 커서가 IEnumerator입니다.
foreach (var x in list) { ... }
// 컴파일러가 대략 이렇게 바꾼다:
IEnumerator e = list.GetEnumerator();
while (e.MoveNext()) { var x = e.Current; ... }
그리고 yield가 든 메서드는 컴파일러가 이 인터페이스들을 구현한 숨겨진 반복자 클래스로 바꿉니다.
@startuml
skinparam backgroundColor #111217
skinparam defaultFontColor #F5F5F5
skinparam ArrowColor #FFB86C
skinparam classBorderColor #BD93F9
skinparam classBackgroundColor #1E1B2E
skinparam classFontColor #F5F5F5
skinparam noteBackgroundColor #1E1B2E
skinparam noteBorderColor #FFB86C
skinparam noteFontColor #F5F5F5
title IEnumerable / IEnumerator 와 컴파일러가 만든 반복자
interface IEnumerable {
+GetEnumerator() : IEnumerator
}
interface IEnumerator {
+MoveNext() : bool
+Current : object
+Reset() : void
}
class "컴파일러 생성 반복자\n(Fibonacci d__0)" as Gen {
-int state
-int a
-int b
+MoveNext() : bool
+Current : object
}
IEnumerable ..> IEnumerator : GetEnumerator()가 반환
Gen ..|> IEnumerable
Gen ..|> IEnumerator
note bottom of Gen : yield가 든 메서드는\n이 반복자 클래스로 변환된다
@enduml
2.4 왜 코루틴이 하필 IEnumerator를 재사용하는가 (의도)
코루틴이 원하는 건 결국 "실행 도중에 멈췄다가, 나중에 그 지점부터 이어서 실행" 하는 것입니다. 그런데 방금 본 반복자가 정확히 그 일을 합니다. MoveNext()가 "다음 지점까지 실행하고 멈춤"이니까요.
그래서 Unity는 새 문법을 발명하는 대신, 이미 언어에 있던 반복자 구조를 의미만 다르게 재해석해서 씁니다.
| 원래 반복자에서 | Unity 코루틴에서 재해석 |
|---|---|
MoveNext() = 다음 요소로 전진 |
한 프레임치 실행하고 멈춤 |
Current = 현재 요소 값 |
언제 다시 재개할지 알려주는 지시 (예: WaitForSeconds) |
순회가 끝나면 false |
코루틴이 끝나면 false |
즉 Unity는 Current에 담긴 값을 "데이터"로 쓰는 게 아니라 "다음에 언제 MoveNext를 부를까"의 힌트로 씁니다. 반복자 기계장치는 그대로 두고 운전자만 Unity로 바꾼 셈입니다.
2.5 왜 "생성"되어야 하는가 (근거)
그럼 왜 컴파일러가 굳이 클래스를 만들어야 할까요. 일반 함수로는 "중간에서 멈췄다가 재개"가 불가능하기 때문입니다. 함수가 리턴하는 순간 스택 프레임과 지역변수가 사라지므로, 다시 부르면 처음부터 시작할 수밖에 없습니다.
"멈췄다 재개"를 하려면 최소한 두 가지를 담을 곳이 필요합니다.
- 어디까지 실행했는지 (재개 지점)
- 그때의 지역변수 값 (상태)
이 둘을 담고 "다음으로 전진(MoveNext)"을 제공하는 객체 — 그게 바로 IEnumerator를 구현한 클래스입니다. 그래서 컴파일러가 yield를 발견하면 이 반복자 클래스를 자동으로 합성하는 것입니다.
그래서 아래 코루틴을 호출해도,
private IEnumerator RollUpdate(float rollDistance)
{
float currentRollTime = 0f;
while (true)
{
currentRollTime += Time.deltaTime;
// ... 이동 계산 ...
if (currentRollTime >= rollTime) break;
yield return null; // 이번 프레임은 여기까지
}
IsRolling = false;
}
몸통이 바로 실행되는 게 아니라, 위에서 말한 반복자(상태머신) 객체 하나가 만들어져서 반환됩니다. 실제 실행은 그 객체의 MoveNext()가 불릴 때 조금씩 진행됩니다.
C++로 비유하면, 우리가 손으로 짜던
switch(state)상태머신을 컴파일러가 대신 만들어주는 것과 같습니다. (C++20의 코루틴과 개념이 비슷하다고 이해했습니다.)
그렇다면 그 "생성된 반복자"가 실제로 어떻게 생겼는지 다음 장에서 뜯어보겠습니다.
3. yield가 만드는 상태머신
RollUpdate가 컴파일되면 대략 이런 모양의 클래스로 바뀐다고 이해했습니다. 실제 생성 코드는 더 복잡하지만, 뼈대만 옮기면 다음과 같습니다.
// 컴파일러가 생성하는 숨겨진 클래스 (개념 코드)
class RollUpdateStateMachine : IEnumerator
{
public int state; // 지금 몇 번째 yield에서 멈췄나
public EntityMovement self; // 캡처된 this
public float rollDistance; // 파라미터 → 필드
public float currentRollTime; // 지역변수 → 필드 (스택 아님!)
public object Current { get; private set; }
public bool MoveNext()
{
switch (state)
{
case 0:
currentRollTime = 0f;
break;
case 1:
goto RESUME; // yield 다음 줄부터 재개
}
while (true)
{
currentRollTime += Time.deltaTime;
// ... 이동 계산 ...
if (currentRollTime >= rollTime) break;
state = 1;
Current = null;
return true; // ← yield return null (여기서 멈춤)
RESUME: ;
}
self.IsRolling = false;
state = -1;
return false; // 메서드 끝
}
}
포인트는 두 가지입니다.
MoveNext()는state를 보고 "이전에 멈춘 지점"부터 이어서 실행합니다.yield return null은state를 저장하고return true하는 지점으로 바뀝니다.
4. 힙은 왜 등장하나
일반 함수의 지역변수는 스택 프레임에 있고, 함수가 리턴하면 그 프레임은 사라집니다.
그런데 코루틴은 yield에서 MoveNext()가 일단 리턴합니다. 제어권을 Unity에 넘겨야 하기 때문입니다. 그 순간 스택 프레임이 사라집니다. 만약 currentRollTime이 스택에 있었다면 증발합니다.
하지만 다음 프레임에 재개하면 그 값이 살아있어야 합니다. 그래서 컴파일러가 이렇게 처리한다고 이해했습니다.
지역변수·파라미터를 스택이 아니라, 힙에 있는 상태머신 객체의 필드로 끌어올린다(hoist).
class(참조 타입)라서 힙에 할당되고, 스택 프레임이 사라져도 그 객체는 살아있으니 값이 유지되는 것입니다.
@startuml
skinparam backgroundColor #111217
skinparam defaultFontColor #F5F5F5
skinparam ArrowColor #FFB86C
skinparam classBorderColor #BD93F9
skinparam classBackgroundColor #1E1B2E
skinparam classFontColor #F5F5F5
skinparam packageBorderColor #FFB86C
skinparam packageFontColor #F5F5F5
skinparam noteBackgroundColor #1E1B2E
skinparam noteBorderColor #FFB86C
skinparam noteFontColor #F5F5F5
title 지역변수가 힙으로 옮겨지는 이유
package "스택" {
class "MoveNext() 호출 프레임" as Frame
}
package "힙 (GC 대상)" {
class "RollUpdateStateMachine" as SM {
int state
float currentRollTime
float prevRollDistance
float rollDistance
object Current
}
}
Frame --> SM : this 참조로 접근
note bottom of Frame : yield로 멈추면\n이 스택 프레임은 사라진다
note bottom of SM : 지역변수가 필드로 옮겨져\n프레임이 사라져도 값이 유지된다
@enduml
그래서 부작용도 하나 생깁니다. 상태머신이 힙 객체라 GC 대상이라는 점입니다. 매 프레임 StartCoroutine을 남발하면 GC 부담이 될 수 있어서 조심하는 편이 좋다고 합니다.
5. Unity는 코루틴을 어떻게 굴리나
StartCoroutine(RollUpdate())을 호출하면 이런 흐름으로 이해했습니다.
@startuml
skinparam backgroundColor #111217
skinparam defaultFontColor #F5F5F5
skinparam ArrowColor #FFB86C
skinparam sequenceLifeLineBorderColor #BD93F9
skinparam sequenceLifeLineBackgroundColor #1E1B2E
skinparam sequenceParticipantBorderColor #BD93F9
skinparam sequenceParticipantBackgroundColor #1E1B2E
skinparam sequenceParticipantFontColor #F5F5F5
skinparam sequenceGroupBackgroundColor #1E1B2E
skinparam sequenceGroupBorderColor #FFB86C
skinparam sequenceGroupHeaderFontColor #FFB86C
skinparam sequenceGroupFontColor #F5F5F5
skinparam noteBackgroundColor #1E1B2E
skinparam noteBorderColor #FFB86C
skinparam noteFontColor #F5F5F5
title 코루틴을 매 프레임 구동하는 흐름
participant "내 코드" as Me
participant "Unity 코루틴 스케줄러" as Unity
participant "상태머신 객체\n(IEnumerator)" as SM
Me -> Unity : StartCoroutine(RollUpdate())
note right of Me : 몸통 실행이 아니라\n상태머신 객체를 생성해 반환
Unity -> SM : 목록에 등록 (state = 0)
loop 매 프레임
Unity -> SM : MoveNext()
SM -> SM : 다음 yield 지점까지 실행
SM --> Unity : true (Current = 대기 지시)
note right of Unity : Current를 보고\n다음 호출 시점을 결정
end
Unity -> SM : MoveNext()
SM --> Unity : false (메서드 끝)
Unity -> Unity : 목록에서 제거
@enduml
여기서 Current(yield한 값)가 다음에 언제 MoveNext를 부를지를 정합니다.
| yield 문 | 다음 재개 시점 |
|---|---|
yield return null |
다음 프레임 |
yield return new WaitForSeconds(2f) |
2초 뒤 |
yield return new WaitForFixedUpdate() |
다음 물리 갱신 뒤 |
yield return new WaitForEndOfFrame() |
이번 프레임 렌더 직후 |
yield return StartCoroutine(other) |
다른 코루틴이 끝난 뒤 |
조금 더 깊이:
WaitForSeconds같은 값들은YieldInstruction이라는 객체이고, Unity가 그 종류를 보고 재개 타이밍을 다르게 잡는 것으로 이해했습니다. 참고로WaitForSeconds는 스케일된 시간(Time.timeScale영향)을 사용하고, 실제 시간이 필요하면WaitForSecondsRealtime을 사용한다고 합니다. 또WaitForSeconds를 매 반복마다new하면 GC가 생기니, 가능하면 캐싱해두는 편이 좋다고 배웠습니다.
6. 상태머신 관점에서 다시 보기
앞의 RollUpdate를 상태 값 기준으로 그리면 다음과 같습니다. 소스는 while로 위→아래로 읽히지만, 실제로는 state를 기준으로 중간부터 재개된다는 점이 핵심입니다.
@startuml
skinparam backgroundColor #111217
skinparam defaultFontColor #F5F5F5
skinparam ArrowColor #FFB86C
skinparam StateBackgroundColor #1E1B2E
skinparam StateBorderColor #BD93F9
skinparam StateFontColor #F5F5F5
title RollUpdate가 변환된 상태머신 (state 값 기준)
state "state = 0\n(초기화)" as S0
state "state = 1\n(yield return null 지점)" as S1
state "state = -1\n(종료)" as SEnd
[*] --> S0 : 상태머신 객체 생성
S0 --> S1 : 첫 MoveNext()\n한 프레임 실행 후 멈춤
S1 --> S1 : 다음 프레임 MoveNext()\n(rollTime 아직 안 끝남)
S1 --> SEnd : rollTime 종료 → break
SEnd --> [*]
@enduml
7. 그래서 왜 디버깅이 어려운가
여기까지 이해하고 나니, 코루틴 디버깅이 껄끄러웠던 이유가 전부 이 구조 때문이라는 것을 알게 됐습니다.
① 콜스택이 논리적 호출자를 안 보여준다
코루틴 안에 브레이크포인트를 걸면, 콜스택 위쪽이 StartCoroutine을 부른 곳이 아니라 Unity 내부의 MoveNext 호출로 나옵니다. "누가 이걸 시작했지?"를 따라 올라가기가 어렵습니다.
② 스텝(F10)이 시간을 건너뛴다yield return null에서 한 줄 넘기면, 다음 줄로 가지 않고 제어가 Unity로 빠져나가 프레임의 나머지를 다 돌고 다음 프레임에야 그 줄로 돌아옵니다. 스텝이 튀는 것처럼 느껴집니다.
③ 지역변수가 이상한 이름의 필드로 보인다currentRollTime이 디버거에서 this.<currentRollTime>5__2 같은 형태로 표시됩니다. 힙 객체의 필드로 옮겨졌기 때문인데, 처음 보면 당황스럽습니다.
④ 소스 순서와 실제 실행 순서가 다르다
소스는 순차적으로 읽히는데 실제로는 switch(state)로 중간부터 재개됩니다. 이 재개 로직이 소스에는 보이지 않아서 흐름이 헷갈립니다.
⑤ 예외 스택 트레이스가 엉뚱하다
코루틴 안에서 예외가 나면 트레이스가 MoveNext를 가리켜서, 정작 StartCoroutine 한 위치를 찾기 어렵습니다.
그래서 저는 요즘 코루틴은 브레이크포인트보다 프레임별 Debug.Log로 흐름을 찍어서 보는 편이 더 편했습니다.
8. 코루틴의 생명주기와 정리 (try/finally)
앞에서 코루틴은 상태를 힙 객체에 들고 프레임에 걸쳐 실행된다고 정리했습니다. 그러면 자연스럽게 "실행 도중에 강제로 끊기면 그 상태는 어떻게 되는가"라는 문제가 생깁니다. 이 부분이 실무에서 버그로 자주 이어져서 따로 정리하겠습니다.
8.1 코루틴은 누구에게 매여 있나
코루틴은 StartCoroutine을 호출한 그 MonoBehaviour와 그것이 붙은 GameObject에 종속됩니다. 그래서 소유자 상태에 따라 코루틴도 함께 영향을 받습니다.
| 상황 | 코루틴 동작 |
|---|---|
GameObject를 SetActive(false) |
멈춥니다. 다시 켜도 자동으로 재개되지 않습니다(종료된 것) |
컴포넌트만 enabled = false |
계속 돕니다. 멈추지 않습니다 ← 자주 오해하는 지점 |
Destroy(gameObject) |
함께 사라집니다 |
StopCoroutine / StopAllCoroutines |
중단됩니다 |
여기서 헷갈리기 쉬운 것은, 코루틴이 컴포넌트의 enabled가 아니라 GameObject의 활성 상태에 묶여 있다는 점입니다. 그래서 스크립트만 비활성화(enabled = false)해도 코루틴은 멈추지 않는다고 이해하고 있습니다.
8.2 도중에 끊기면 상태가 남는 문제
앞서 본 RollUpdate를 다시 보겠습니다. 시작할 때 IsRolling = true로 켜고, 끝에서 false로 끕니다.
IsRolling = true;
// ... 여러 프레임에 걸쳐 이동 ...
IsRolling = false; // 정상 종료 시에만 실행된다
문제는, 구르는 도중에 StopCoroutine이나 사망 처리로 코루틴이 중간에 끊기면 IsRolling이 true인 채로 남는다는 것입니다. 그러면 캐릭터가 영원히 "구르는 중" 상태로 오작동할 수 있습니다.
일반 함수라면 return 직전에 정리하면 됩니다. 하지만 코루틴은 어느 yield 지점에서든 강제로 종료될 수 있어서, "어디서 끊겨도 정리된다"는 보장이 필요합니다.
8.3 try/finally로 "끊겨도 반드시 정리"
이때 try/finally를 쓰면 됩니다.
private IEnumerator RollUpdate(float rollDistance)
{
IsRolling = true;
try
{
float currentRollTime = 0f;
while (currentRollTime < rollTime)
{
currentRollTime += Time.deltaTime;
// ... 이동 계산 ...
yield return null;
}
}
finally
{
IsRolling = false; // 정상 종료든 중간 중단이든 반드시 실행된다
}
}
이게 동작하는 근거는 2장에서 본 "컴파일러가 만든 반복자"에 있습니다. 그 반복자 클래스는 IEnumerator뿐 아니라 IDisposable도 구현합니다. 코루틴이 정상 종료되거나 StopCoroutine 등으로 중단되면, Unity가 그 반복자의 Dispose()를 호출하고, Dispose()가 finally 블록을 실행시킵니다.
@startuml
skinparam backgroundColor #111217
skinparam defaultFontColor #F5F5F5
skinparam ArrowColor #FFB86C
skinparam sequenceLifeLineBorderColor #BD93F9
skinparam sequenceLifeLineBackgroundColor #1E1B2E
skinparam sequenceParticipantBorderColor #BD93F9
skinparam sequenceParticipantBackgroundColor #1E1B2E
skinparam sequenceParticipantFontColor #F5F5F5
skinparam noteBackgroundColor #1E1B2E
skinparam noteBorderColor #FFB86C
skinparam noteFontColor #F5F5F5
title 코루틴이 중단될 때 finally가 실행되는 과정
participant "내 코드" as Me
participant "Unity" as Unity
participant "반복자 객체\n(IEnumerator, IDisposable)" as SM
Me -> Unity : StopCoroutine(rollCoroutine)
Unity -> SM : Dispose()
SM -> SM : finally 블록 실행\n(IsRolling = false)
note right of SM : 어느 yield에서 끊겼든\n정리 코드가 반드시 실행된다
@enduml
즉 "시작할 때 켜고 끝날 때 끄는" 작업(구르기, 이동, 연출, UI 표시 등)의 상태를 중단에도 안전하게 원복할 수 있습니다. 이게 코루틴에서 try/finally를 쓰는 이유이자 의도라고 이해했습니다.
8.4 주의점
finally블록 안에서는yield를 쓸 수 없습니다(반복자 문법 제약).Dispose()가 불리는 경로는 정상 완료,StopCoroutine,StopAllCoroutines,GameObject파괴/비활성화 등입니다. 다만 문자열 방식StartCoroutine("...")은 어떤 인스턴스를 멈추는지 모호하므로, 확실히 정리하려면Coroutine핸들을 저장해 그 핸들로StopCoroutine하는 편이 안전하다고 이해했습니다.
9. C++ 관점에서 정리
제 모국어가 C++이라, 마지막으로 익숙한 언어로 정리해봤습니다.
// C++: 예전엔 이런 상태머신을 손으로 짰다
struct RollUpdate {
int state = 0;
float currentRollTime = 0.f;
bool moveNext() { // Unity의 MoveNext()에 해당
switch (state) { /* 멈춘 지점부터 재개 */ }
// ...
state = 1; return true; // yield return null
}
};
// C#: 컴파일러가 위 상태머신을 자동으로 만들어준다
IEnumerator RollUpdate() {
// 지역변수가 곧 상태
yield return null; // 자동 생성된 상태 저장 지점
}
- 스레드 컨텍스트 스위칭: OS가 레지스터/스택을 통째로 저장·복원 (비쌈, 진짜 병렬)
- 코루틴 재개: 그냥 힙 객체의
MoveNext()호출 (값쌈, 같은 스레드, 협력적)
그래서 코루틴은 스레드보다는 C++20의 코루틴이나 fiber에 더 가깝다고 이해하고 있습니다.
마치며
정리하면, IEnumerator는 MoveNext() + Current짜리 반복 인터페이스이고, yield가 있으면 컴파일러가 이걸 구현한 힙 상태머신 클래스로 바꿉니다. 스택 프레임은 yield에서 사라지므로, 지역변수를 힙 객체 필드로 옮겨 상태를 보존합니다. 디버깅이 어려운 것은, 디버거가 이 숨겨진 상태머신을 보고 있고 실행이 프레임마다 쪼개져 있어서 콜스택·스텝·변수명이 다 어긋나기 때문이라고 이해했습니다.
'Unity' 카테고리의 다른 글
| [Unity] GC를 돌려도 왜 메모리가 안 줄어드는 이유 — 관리되는 메모리, Clean/Dirty 메모리, 세그먼트 그리고 .NET VM (0) | 2026.07.21 |
|---|---|
| [Unity] 코루틴은 프레임, yield 타이밍, 그리고 DLL 분석하기 (0) | 2026.07.13 |
| [Unity] 직렬화 시스템 — [SerializeField], .asset, .meta가 하는 일 (0) | 2026.06.17 |
| [Unity] UnityEngine.Object의 이중 구조와 == null의 진짜 의미 (1) | 2026.06.11 |
| [Unity] C# 컴파일 과정과 IL, Mono, IL2CPP, asmdef 이해하기 (0) | 2026.06.10 |