Unity 엔진을 사용하다 보면 Awake, Start, Update 같은 생명주기 함수들이 자동으로 호출되는 것은 누구나 알고 있다.
그런데 어느 순간 이런 의문이 생겼다.
"접근 제한자를 바꿔도 되는데?"
"virtual/override를 쓰지도 않는데?"
"대체 이 함수들은 어떻게 호출되는 걸까?"
그래서 오늘은 생명주기 함수의 원리에 대해 파헤치고 정리해보려고 한다.
Unity 엔진이 오브젝트의 생성 -> 활성화 -> 업데이트 -> 비활성화 -> 파괴 흐름에 따라 자동으로 호출해주는 콜백 함수들을 말한다.
ex)
이러한 생명주기 함수들은 개발자가 따로 정의하지 않더라도 Unity 엔진이 알아서 호출한다.
그럼 여기서 첫 번째 의문이 발생한다.
MonoBehaviour에는 Start나 Update 같은 생명주기 함수들이 virtual로 선언되어 있지 않다.
즉, 아래처럼 override해서 사용하지 않는다.
public class Player : MonoBehaviour { public override void Awake() {} // 이렇게 사용하지 않음 }
그런데도 엔진은 Update를 찾아서 자동으로 호출한다.
접근 제한자를 바꿔도 되고, 함수 이름만 제대로 작성하면 private라도 호출한다.
그렇다면 기준이 무엇일까?
-> 바로 "함수 이름과 시그니처"이다.
Unity는 "이름이 Update이고, 반환 타입이 void이고, 인자가 없다면 Update 콜백으로 취급" 하는 규칙 기반으로 호출한다.
그렇다면 Unity는 이 함수를 어떻게 찾아서 호출하는 걸까?
프로그램이 실행중(runtime)에 클래스, 메서드, 필드 같은 메타데이터 정보를 조회하거나 호출할 수 있는 기능이다.
var type = typeof(Player); var method = type.GetMethod("Update"); method.Invoke(playerInstance, null);
이 세 줄은
즉 컴파일 시점이 아니라 런타임에 메서드를 찾아 실행하는 것이 바로 리플렉션 기법인 것이다.
Unity의 실제 동작 방식은 다음과 같다.
1) 스크립트 로드 시점
C++엔진이 C#런타임에 요청한다.
"MonoBehaviour를 상속한 타입들을 모두 가져와줘."
C# 런타임은 Reflection으로
그리고 해당 메서드 정보를 C++ 엔진에게 전달한다.
2) 프레임 루프 실행 시점
Unity는 리플렉션을 통해 사전에 찾아놓은 메서드를 함수 포인터 형태로 캐싱해 놓는다. 따라서 매 프레임 리플렉션 기법을 사용하는 것이 아니라 매우 빠르게 동작한다.
즉,
두 단계로 분리된 구조이다.
Unity는 어떤 스크립트가 어떤 콜백을 구현했는지 컴파일 시점에 알 수 없다. 개발자가 마음대로 스크립트를 만들고, 마음대로 함수 이름을 정의하기 때문이다.
그래서 엔진은 런타임에 직접 스캔해서 필요한 콜백만 선별해야 한다.
만약 아래와 같은 구조를 사용했다고 가정해보자
public class MonoBehaviour { public virtual void Awake() {} public virtual void Start() {} public virtual void Update() {} public virtual void OnDestroy() {} } public class Player : MonoBehaviour { public voerride void Update() { // 매프레임 호출 } }
이렇게 사용했을 때 문제점은
즉, virtual 기반 구조는
반면 리플렉션 방식은 함수 이름만 맞추면 자동으로 호출되므로 훨씬 유연하다.
Unity는 다음과 같은 구조를 가진다.
C++은 C# 타입 정보를 알 수 없기 때문에
C# 런타임에게 리플렉션으로 정보를 요구해야 한다.
흐름은 다음과 같다.
<C++ -> C#>
<C# -> C++>
이 구조로 인해 C++과 C#은 강하게 묶이지 않을 수 있다.
Unity 생명주기 함수는 virtual/override 기반으로 호출되는 것이 아니라
1. 리플렉션으로 메서드 존재 여부를 탐색하고,
2. 함수 포인터로 캐싱하여 빠르게 호출하는 구조이다.
이 구조 덕분에 Unity는
개발을 하다 보면 "이 기능은 어떻게 동작하는 거지?"라는 궁금증이 생기기 마련인데, 이런 궁금증 하나하나가 엔진 내부 구조를 이해하는데 도움이 많이 되는 것 같다.
Unity 생명주기 함수도 그중 하나다. 겉으로는 단순해 보이지만, 내부에는 C++ 엔진 -> C# 런타임 -> 리플렉션 탐색 -> 함수 포인터 캐싱 이라는 복잡한 동작이 숨어 있다.
이러한 원리를 파헤치는 것은 Unity의 구조를 명확히 이해하게 되어 성능이나 최적화에 대한 관점이 넓어지는 것 같다.