[Unity] Fake Null 퀴즈
댓글 0
댓글을 작성하려면 로그인이 필요합니다.
아직 댓글이 없습니다. 첫 번째 댓글을 작성해보세요!
배운 내용을 정리하고 점검하기 위해 가장 좋은 방법은 남에게 설명하는 것이다. 근데 설명하려면 제대로 알아야지. 내가 제대로 알고 있는지를 파악하기 위해, AI에게 문제지를 뽑아달라 요청했다.
아래 코드를 읽고, Console에 출력될 메시지를 추측해보자. 만약 모두 명확한 근거와 함께 정답을 자신할 수 있다면, 이 글은 읽을 필요가 없다.
// ========== 공통 전제 ==========
public interface IDamageable { void TakeDamage(); }
public class Enemy : MonoBehaviour, IDamageable{
public void TakeDamage() { }
}
GameObject go = new GameObject("Target");
Enemy enemy = go.AddComponent<Enemy>();
IDamageable target = enemy;
object boxed = enemy;
Destroy(go);
// ========== Quiz 1: Destroy(go)와 같은 프레임 ==========
bool a1 = (go == null);
bool a2 = (go != null);
bool a3 = go ? true : false;
bool a4 = ReferenceEquals(go, null);
Debug.Log($"1. {a1}, {a2}, {a3}, {a4}");
// ========== Quiz 2: Destroy(go) 다음 프레임 ==========
bool b1 = (go == null);
bool b2 = (go != null);
bool b3 = go ? true : false;
bool b4 = ReferenceEquals(go, null);
Debug.Log($"2. {b1}, {b2}, {b3}, {b4}");
// ========== Quiz 3: 다음 프레임, 언어 기능 ==========
bool c1 = (go is null);
bool c2 = (go is not null);
Debug.Log($"3. {c1}, {c2}");
List<Action> acts = new();
acts.Add(() => {
go?.SetActive(false);
Debug.Log("?. 통과");
});
acts.Add(() => {
GameObject fallback = new GameObject("Fallback");
GameObject result = go ?? fallback;
Debug.Log(ReferenceEquals(result, fallback));
});
foreach (var act in acts){
try { act(); }
catch (Exception e) { Debug.Log(e.GetType().Name); }
}
// ========== Quiz 4: 다음 프레임, 업캐스팅 ==========
bool d1 = (target == null);
bool d2 = (boxed == null);
bool d3 = ((UnityEngine.Object)target == null);
Debug.Log($"4. {d1}, {d2}, {d3}");
아래에 정답이있다!
1. F, T, T, F
2. T, F, F, F
3. F, T
MissingReferenceException
F
4. F, F, T
먼저, Destroy는 해당 프레임이 종료될 때가 되어서야 실제로 동작함을 아는가를 묻는 문제다. 즉, go 는 아직 파괴되지 않았으므로 null 상태가 아니다.
a1 과 a2 는 자명하다.
a3 는 암묵적인 형변환이다. 이는 != null 과 동일한 연산이다. 따라서 a2 와 그 값이 같다.
a4 는 다음과 같이 정의된 System.Object 의 메소드다.
public static bool ReferenceEquals(object objA, object objB);이는 두 참조가 메모리상 동일한 객체를 가리키는지 평가한다. 따라서 False 이다.
이제 Destroy가 실행된 프레임 이후 시점에서의 평가이다. 즉 실제로 함수가 작용하여 객체를 파괴한 뒤의 시점이다.
b1 은 True이다. 다만, Destroy가 go 를 파괴했기 때문이라고 설명하면 반쪽짜리 정답이다. Native 객체가 파괴되었고 Managed 객체는 살아있으며, 이 Managed 객체의 m_CachedPtr 이 IntPtr.Zero 로 설정되어서 오버로딩된 UnityEngine.Object 의 == 연산자가 True를 반환했다고 한다면 만점이다. 그리고 이를 FakeNull이라 부른다.
b2 = !b1 이므로 b2 는 False이다.
b3의 경우, UnityEngine.Object 의 암묵적 형변환이 != 와 같다고 위에서 이미 언급한 바 있다. 따라서 False이다.
b4 는 ReferenceEquals 메소드가 System.Object 에 정의된 static 메소드이므로 Unity에서 오버로딩할 수 없다는 점을 고려해야한다. 즉, 아직 죽지않은 Managed 객체와 null 을 비교하므로 False이다.
c1 과 c2 모두 is 를 사용하는데, 이는 C#이 제공하는 언어 사양으로, CLR 수준에서 참조 null 검사를 수행하도록 정의되어있다. go 는 실제로는 null 이 아닌 FakeNull 상태이므로, is null 에는 False, is not null 에는 True를 반환한다.
?. 연산자는 UnityEngine.Object 에 사용하면 예상과 다른 결과를 낳을 수 있고, 이 문제가 그 예시이다. 이 경우 FakeNull상태지만 실제로는 null 이 아닌 go 에 대해 메소드를 실행하게 되는데, 정작 SetActive() 는 icall을 통해 m_CachedPtr 에 접근하려하기 때문에 MissingReferenceException 이 발생하는 것이다.
?? 연산자도 마찬가지로 작동하여 result 엔 fallback 이 아닌 go 가 들어간다. 따라서 출력 결과는 False다.
d1 은 go 를 인터페이스로 업캐스팅한 target 으로, d2 는 System.Object 로 업캐스팅한 boxed 로, d3 는 인터페이스를 UnityEngine.Object 로 다운캐스팅하여 == 연산 결과를 평가하고 있다.
go 가 참조하고 있는 Managed 객체는 실제로 파괴되지 않았음에 주의하자. GC가 수거하기 전까지는 살아있는 참조다. 따라서, d1 과 d2 는 System.Object 의 == 연산이 수행되어 실제로는 Managed 객체의 존재 여부에 대한 평가이므로 False, d3 는 UnityEngine.Object 의 == 연산이 수행되어 m_CachedPtr == IntPtr.Zero 에 의해 FakeNull로 처리되므로 True이다.
두 개 틀림.
하나는 a3에서의 암묵적 형변환 문제다. 잘 안 쓰던 방법이라 헷갈린듯.
다른 하나는 MRE 대신 NRE(Null Ref Exception)라고 답해서 틀렸는데, 사소하다.
FakeNull 정복했다고 할 수 있을까? 사실 프로젝트 하면서 실제로 문제를 맞닥뜨리는 것하고는 천지차이라서, 그때되면 또 당황하겠거니 싶다. 그래도 이만한 긴 글 썼으니 잊버리면 사람 아님 진짜.
이 시리즈를 쓰게 된 계기는 인터페이스 업캐스팅으로 인한 FakeNull 문제였다. 처음엔 하나의 Problem Shooting으로 다루려고 한 주제였는데, 정작 Native에 대해 아는 게 없어 FakeNull을 제대로 이해할 수 없었다. 그렇게 시리즈가 시작되었고 이게 상당히 길어졌다.
최근에 Unity7과 관련해서 얘기들이 도는 것도 같은데, 아마도 그 기반이 되었을 CoreCLR 포팅 과정에 대해서도 얼핏 그 배경을 이해할 수 있는 수준으로 제 자신을 올려놓는 데에는 이 시리즈 연재가 확실히 도움이 된 것 같다. 그리고 프로파일러 돌릴 때에도 도움이 될 듯함.
진짜 끗.
![[Unity] 렌더 큐](https://data.develog.develrocket.com/upload/develog/user_1777093328305/1785158340538-3i4lkp/_____2026-07-27_215606.png)
유니티 렌더 큐
타워 디펜스에 타워 합성 시스템 구현
![[Unity] 피벗은 뭐고 앵커는 머요](https://data.develog.develrocket.com/upload/develog/user_1776858831954/1784966048688-1uin6m/image.png)
네모들의 위치를 피벗과 앵커로 설명하시오
![[Unity] 렌더 큐](https://data.develog.develrocket.com/upload/develog/user_1777093328305/1785158340538-3i4lkp/_____2026-07-27_215606.png)
![[Unity] 피벗은 뭐고 앵커는 머요](https://data.develog.develrocket.com/upload/develog/user_1776858831954/1784966048688-1uin6m/image.png)