유니티 개발을 하다보면 MonoBehaviour 생명주기(Lifecycle)에 대한 개념을 반드시 다루게 됩니다. 오브젝트 최초 생성 시에는 Awake()가 호출되고 enabled 상태일때 Start()가 호출되며 매 프레임마다 Update()가 실행되고 삭제 시에는 OnDestroy()가 실행 된다는 것을 바탕으로 모든 게임 로직들을 작성하게 됩니다.
그런데 잘 생각해보면 이상합니다. 누가 함수들을 실행시키고 있는걸까요? 일단 내 코드에서는 실행하지 않습니다. override 키워드를 사용하지 않는걸 보니 상속 구조를 통해 메서드 오버라이딩을 하는 것 같지는 않습니다. 그렇다고 인터페이스인가 싶어서 보면 어떠한 접근 제한자(private, protected, public)를 사용해도 동작에 전혀 문제가 없습니다.
using UnityEngine;
public class Player : MonoBehaviour
{
    private void Awake() 
    {
    	// private도 문제 없고
    }
    protected virtual void Start()
    {
    	// protected virtual이어도 문제가 없습니다.
    }
    public void Update()
    {
        // public도 당연히 가능합니다. 
        // 외부에서 호출해도 상관은 없지만 예상치 못한 결과가 나올 수 있으니 권장하지 않습니다.
    }
    void OnDestroy() 
    {
    
    }
}
유니티는 기존의 C#이 제공하지 않는 새로운 패러다임의 문법을 이용하고 있는걸까요?
이에 대한 답변은 다음 유니티 공식 블로그 포스팅에서 찾을 수 있었습니다.
방법은 생각보다 간단하면서 얼핏 보면 구조적으로 비효율적으로 보입니다. 바로 각 스크립트에 정해진 메서드들(Awake, Start 등)이 있는지 메서드 이름으로 찾는 것입니다. 이 때 정해진 메서드들은 Magic Method라고 합니다.
Unity에서의 Magic Method는 사전에 미리 정해진 이름의 메서드를 뜻합니다.
다음 링크에서 MonoBehaviour의 모든 Magic Method들을 확인할 수 있습니다.
모든 MonoBehaviour은 사용이 될 때 엔진 레벨에서 스크립트 내의 Magic Method들을 찾고 그 정보를 적당한 리스트에 넣은 뒤 실행 타이밍에 맞춰 해당 메서드를 실행하게 됩니다. 여기에서 사용이 될 때 라는 것은 GameObject가 Active 상태인 것을 뜻합니다. 만일 GameObject가 Inactive 상태일 경우에는 Awake 메서드 조차 실행되지 않습니다.
아래 그림과 같이 Tree라는 이름의 MonoBehaviour 컴포넌트가 추가 된 세 개의 GameObject를 배치하고 GameObject와 Component를 활성화 및 비활성화 하여 이벤트 메서드의 호출 여부를 확인해봤습니다. 그 결과 GameObject가 Active 상태인 경우에만 이벤트 메서드가 호출되는 것을 볼 수 있었습니다.

활성화 된 GameObject들을 순회하며 저장된 Magic Method들의 정보를 이용하여 MonoBehaviour의 생명주기에 따라 메서드들을 실행하게 됩니다. 공식적으로는 System.Reflection을 사용하지 않는다고 했으니 별도의 다른 방식을 통해 reflection 기법을 사용하고 있을 것으로 보입니다.
소프트웨어 아키텍쳐를 공부하는 분이라면 이런 생각이 들 수도 있습니다. 상속이나 인터페이스를 활용하는게 더 안정적이고 좋은 구조를 가질텐데 왜 직관적이지 않은 reflection 기법을 사용했을까? 맞는 말입니다. 직관적이지 않은 구조는 물론이고 사용자의 실수로 인해 이벤트 메서드가 호출되지 않을 수도 있습니다.
저의 경우는 LateUpdate()를 구현할 때 LastUpdate()라고 입력한 뒤 실행이 안되는 것을 보고 한참 고생한적이 있습니다.
이와 같은 구조를 사용하는 이유는 성능 때문입니다. 간단하게 설명하자면, Unity의 핵심 로직이 동작하는 Native C++ 레벨에서 껍데기에 해당하는 Managed C# 레벨의 메서드를 호출할 때 불필요한 비용이 발생하기 때문입니다. 자세한 내용과 실험은 앞서 제공해드린 유니티 공식 블로그 포스팅을 통해 확인하실 수 있습니다.
위의 스크린샷 Scene을 보시면 세 개의 Tree 오브젝트가 추가된 것을 볼 수 있습니다. Tree1, Tree2, Tree3 중 어떤 오브젝트의 Awake()가 먼저 호출되는지 알 수 있을까요? 답은 '알 수 없다' 입니다. 가장 마지막에 화면에 추가된 오브젝트의 이벤트 메서드가 먼저 실행되긴 하지만 Scene을 다시 로딩하면 그 순서가 뒤죽박죽이 됩니다. 아마 Unity 내부적으로 GameObject들을 관리하는데 사용하는 별도의 로직이 있을 것으로 보입니다. 
Script Execution Order 설정을 통해 스크립트 간의 실행 우선순위는 결정할 수 있지만 같은 스크립트를 가지면서 다른 오브젝트인 경우에는 실행 순서를 알 수 없습니다.
이 호출 순서가 문제가 되는 경우는 대체로 초기화 단계입니다. 만일 Tree1이 Awake() 메서드에서 나무의 높이를 설정하고 Tree3는 Awake()에서 Tree1의 높이값을 받아와 자신의 높이값을 설정한다고 하면, Tree1의 높이값이 초기화되지 않은 상태에서 Tree3가 Tree1의 높이값을 가져오는 경우가 발생할 수 있습니다.
그렇기 때문에 Awake() 메서드에서는 오브젝트 스스로의 값만을 초기화하고 Start() 메서드에서 타 객체에 의존적인 초기화를 진행하는 것이 바람직합니다. 혹은 Initialize()라는 이름의 메서드를 별도로 만들어서 외부 객체에서 직접 초기화 메서드를 호출해주는 것도 방법입니다.
스크립트의 생명 주기에 해당하는 이벤트 메서드를 누가 언제 실행 시켜주는지에 대해 알아봤습니다. 이벤트 메서드는 '유니티 엔진 내부에서 MonoBehaviour 스크립트에 포함된 메서드의 이름을 찾아 별도의 리스트에 넣고 관리하며 실행' 합니다. 이 구조에 대해서는 왈가왈부가 많습니다. 'Unity가 채택한 구조이니 다른 프로젝트에도 이런 방식을 적용해야지'라는 생각까지는 하지 않으셔도 될 것 같네요.
사실 이 내용은 게임을 개발하는데 큰 지장은 없습니다. 유니티를 사용하는데 있어서 일종의 교양 지식을 하나 배웠다고 보시면 되겠습니다. 유니티 엔진을 사용함에 있어서 가장 기본이 되는 이벤트 메서드에 대해 두루뭉술하게 알고 있는 것보단 어떤 방식으로 이벤트 메서드가 동작하는지 알면 개발에 조금이나마 도움이 될 것 같습니다.
혹시나 프로젝트 구조 개선에 큰 관심을 가진 누군가가 BaseMonoBehaviour라는 이름으로 각종 이벤트 메서드를 virtual 메서드로 구현하여 직관적인 상속구조 기반의 MonoBehaviour를 만드려고 하면 이 글에서 배우신 교양을 바탕으로 적극적으로 말려주세요. 유니티 내부 구조까지 관심을 가지는 고급 개발자로 보일 수 있지 않을까요?