[Unity] 생명주기 함수가 호출되는 원리

ChangBeom·2025년 11월 19일

Unity

목록 보기
9/17
post-thumbnail

Unity 엔진을 사용하다 보면 Awake, Start, Update 같은 생명주기 함수들이 자동으로 호출되는 것은 누구나 알고 있다.

그런데 어느 순간 이런 의문이 생겼다.

"접근 제한자를 바꿔도 되는데?"
"virtual/override를 쓰지도 않는데?"
"대체 이 함수들은 어떻게 호출되는 걸까?"

그래서 오늘은 생명주기 함수의 원리에 대해 파헤치고 정리해보려고 한다.


[1. 생명주기(Life Cycle) 함수란?]

Unity 엔진이 오브젝트의 생성 -> 활성화 -> 업데이트 -> 비활성화 -> 파괴 흐름에 따라 자동으로 호출해주는 콜백 함수들을 말한다.

ex)

  • Awake
  • Start
  • Update
  • FixedUpdate
  • OnEnable
  • OnDisable
  • OnDestroy

이러한 생명주기 함수들은 개발자가 따로 정의하지 않더라도 Unity 엔진이 알아서 호출한다.

그럼 여기서 첫 번째 의문이 발생한다.


[2. 생명주기 함수는 virtual도 아닌데 어떻게 호출될까?]

MonoBehaviour에는 Start나 Update 같은 생명주기 함수들이 virtual로 선언되어 있지 않다.

즉, 아래처럼 override해서 사용하지 않는다.

public class Player : MonoBehaviour
{
	public override void Awake() {} // 이렇게 사용하지 않음
}

그런데도 엔진은 Update를 찾아서 자동으로 호출한다.
접근 제한자를 바꿔도 되고, 함수 이름만 제대로 작성하면 private라도 호출한다.

그렇다면 기준이 무엇일까?
-> 바로 "함수 이름과 시그니처"이다.

Unity는 "이름이 Update이고, 반환 타입이 void이고, 인자가 없다면 Update 콜백으로 취급" 하는 규칙 기반으로 호출한다.

그렇다면 Unity는 이 함수를 어떻게 찾아서 호출하는 걸까?


[3. 리플렉션(reflection)]

리플렉션이란?

프로그램이 실행중(runtime)에 클래스, 메서드, 필드 같은 메타데이터 정보를 조회하거나 호출할 수 있는 기능이다.

var type = typeof(Player);
var method = type.GetMethod("Update");
method.Invoke(playerInstance, null);

이 세 줄은

  • Player 타입의 메타데이터를 가져오고
  • 그 안에서 이름이 Update인 메서드를 찾은 뒤
  • playerInstance 객체의 Update()를 실행한다는 뜻이다.

즉 컴파일 시점이 아니라 런타임에 메서드를 찾아 실행하는 것이 바로 리플렉션 기법인 것이다.


[4. Unity는 생명주기 함수를 리플렉션으로 탐색한다.]

Unity의 실제 동작 방식은 다음과 같다.

1) 스크립트 로드 시점
C++엔진이 C#런타임에 요청한다.

"MonoBehaviour를 상속한 타입들을 모두 가져와줘."

C# 런타임은 Reflection으로

  • Awake
  • Start
  • Update
  • OnDestroy
    등 생명주기 함수가 있는지 탐색한다.

그리고 해당 메서드 정보를 C++ 엔진에게 전달한다.

2) 프레임 루프 실행 시점
Unity는 리플렉션을 통해 사전에 찾아놓은 메서드를 함수 포인터 형태로 캐싱해 놓는다. 따라서 매 프레임 리플렉션 기법을 사용하는 것이 아니라 매우 빠르게 동작한다.

즉,

  • 탐색은 리플렉션으로 한 번만
  • 호출은 캐싱한 함수 포인터로 매프레임

두 단계로 분리된 구조이다.


[5. 유니티는 왜 리플렉션을 사용할까?]

<1. 모든 스크립트를 동적으로 찾기 위해>

Unity는 어떤 스크립트가 어떤 콜백을 구현했는지 컴파일 시점에 알 수 없다. 개발자가 마음대로 스크립트를 만들고, 마음대로 함수 이름을 정의하기 때문이다.

그래서 엔진은 런타임에 직접 스캔해서 필요한 콜백만 선별해야 한다.

<2. virtual/override 구조보다 훨씬 유연함>

만약 아래와 같은 구조를 사용했다고 가정해보자

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에는 수십 개가 아니라 수백 개의 콜백이 있다.
    이 모든 걸 MonoBehaviour에 virtual로 넣는 것은 매우 비효율적이다.
  • override하지 않아도 base 함수가 계속 호출된다.
    빈 virtual 함수가 매 프레임 호출되면 큰 성능 낭비가 발생한다.
  • 콜백 추가/ 변경 시 MonoBehaviour 정의 자체를 바꿔야 한다.
    엔진과 C#이 강하게 결합되어 유지보수가 어려워지는 단점이 생긴다.

즉, virtual 기반 구조는

  • 유지보수 비용 증가
  • 성능 낭비
  • 백엔드 제약
    등의 문제가 많다.

반면 리플렉션 방식은 함수 이름만 맞추면 자동으로 호출되므로 훨씬 유연하다.

<3. C++ 엔진과 C# 스크립트 사이를 느슨하게 연결하기 위해>

Unity는 다음과 같은 구조를 가진다.

  • 엔진 코어: C++
  • 사용자 스크립트: C#

C++은 C# 타입 정보를 알 수 없기 때문에
C# 런타임에게 리플렉션으로 정보를 요구해야 한다.

흐름은 다음과 같다.

<C++ -> C#>

  • "MonoBehaviour 상속 타입 리스트 줘"
  • "이 타입에 Update 함수 있어?"
  • "있으면 메서드 핸들 넘겨줘"

<C# -> C++>

  • 메서드 정보 전달
  • IL2CPP 또는 Mono가 함수 포인터로 변환
  • 이후 엔진 루프가 해당 메서드 호출

이 구조로 인해 C++과 C#은 강하게 묶이지 않을 수 있다.


[6. 정리]

Unity 생명주기 함수는 virtual/override 기반으로 호출되는 것이 아니라

1. 리플렉션으로 메서드 존재 여부를 탐색하고,
2. 함수 포인터로 캐싱하여 빠르게 호출하는 구조이다.

이 구조 덕분에 Unity는

  • 콜백을 자유롭게 추가/수정/삭제할 수 있고
  • virtual 구조의 성능 문제를 피할 수 있으며
  • C++ 엔진과 C# 스크립트를 느슨하게 연결한다.

[7. 마치며]

개발을 하다 보면 "이 기능은 어떻게 동작하는 거지?"라는 궁금증이 생기기 마련인데, 이런 궁금증 하나하나가 엔진 내부 구조를 이해하는데 도움이 많이 되는 것 같다.

Unity 생명주기 함수도 그중 하나다. 겉으로는 단순해 보이지만, 내부에는 C++ 엔진 -> C# 런타임 -> 리플렉션 탐색 -> 함수 포인터 캐싱 이라는 복잡한 동작이 숨어 있다.

이러한 원리를 파헤치는 것은 Unity의 구조를 명확히 이해하게 되어 성능이나 최적화에 대한 관점이 넓어지는 것 같다.

0개의 댓글