[Unity] SerializeField vs SerializeReference vs Serializable
서론
유니티 2019.3에 [SerializeReference]가 추가되었고, 현재 프로젝트에서 Custom Inspector와 함께 사용 중이다.
이번 포스팅에선 SerializeField와 SerializeReference의 차이를 간략하게 살펴보고, SerializeReference의 개념, 올바른 사용법까지 살펴보자.
SerializeField는 무엇인가?
유니티를 좀 배워본 사람이라면, [SerializeField]를 아마 private 변수를 Inspector에 띄워주는 고마운 친구 정도로 알고 있을 것이다. 이는 절반 정도는 맞는 말이다.
해당 기능은 [SerializeField] Attribute의 부가 기능 정도로 볼 수 있으며, 실제 이 Attribute의 존재 의의는 필드를 직렬화 시킨다는 점에 있다. 그러나 지금 알아보기엔 글이 너무 길어지고 주제를 벗어나므로, 다음 포스팅에 직렬화에 대해 자세히 알아보겠다.
여기선 [SerializeField]에 대해, 다음과 같이 알고 있으면 충분하다.
- private 변수를 Inspector에 띄우고, 수정 가능하게 해준다.
- 다음 필드들에 사용 가능하다.
- 원시 데이터(int, string 등)
- 유니티 기본 제공 유형(Vector2, Color 등)
- Enum
- Struct
- UnityEngine.Object를 상속한 클래스(MonoBehaviour, Component, GameObject 등)
- System.Serializable이 명시된 클래스
- 위 필드로 구성된 배열 혹은 리스트
- Dictionary, 다차원 배열 등 멀티 레벨 타입엔 사용이 불가하다.
- 추상 클래스나 interface 등 다형성 필드는 지원하지 않는다.
위에서 알 수 있듯, [Serializable]은 사용자가 정의한 C# 클래스를 직렬화 가능하게 표시하는 Attribute로, [SerializeField]나 [SerializeReference]와 대비되는 관계가 아니다. 애초에, [Serializable]은 C#의 Attribute고, [SerializeField]와 [SerializeReference]는 유니티의 Attribute다. 사용처 역시 클래스의 앞과 변수의 앞으로 다르니, 비슷하게 생겼다고 헷갈리지 말자.
[SerializeField]는 UnityEngine.Object를 상속한 클래스는 참조 타입으로 직렬화하며, 그 외엔 값 타입으로 직렬화한다.
흔히, 우리가 GameObject나 Transform을 변수로 선언했을 때, 씬에 존재하는 객체의 참조가 저장되는 걸 떠올리면 된다.
즉, [SerializeReference]의 탄생은 [SerializeField]가 값 타입으로만 직렬화해서 탄생한 게 아니다. [SerializeField]는 이미 참조 타입 또한 지원하고 있다.
그럼 SerializeReference는 왜 생겼을까?
[SerializeField]의 한계인, 다형성 미지원을 극복하기 위해 만들어졌다.
유니티 공식 문서가 제공해주는 예시를 살짝 변형시켜 살펴보자.
SerializeField를 사용한 경우
using System;
using System.Collections.Generic;
using UnityEngine;
public interface IShape { }
[Serializable]
public class Cube : IShape
{
public Vector3 size;
}
[Serializable]
public class Sphere : IShape
{
public Color color;
}
[Serializable]
public class Thing
{
public int weight;
}
[ExecuteInEditMode]
public class BuildingBlocks : MonoBehaviour
{
[SerializeField]
public List<IShape> inventory;
[SerializeField]
public System.Object bin;
[SerializeField]
public List<System.Object> bins;
void OnEnable()
{
if (inventory == null)
{
inventory = new List<IShape>()
{
new Cube() {size = new Vector3(1.0f, 1.0f, 1.0f)}
, new Sphere() {color = new Color(0.2f, 0.5f, 0.7f)}
};
Debug.Log("Created list");
}
else
Debug.Log("Read list");
if (bins == null)
{
// This is supported, the 'bins' serialized field is declared as holding a collection type.
bins = new List<System.Object>() { new Cube(), new Thing() };
}
if (bin == null)
{
// !! DO NOT USE !!
// Although, this is syntaxically correct, it is NOT supported as a valid serialization construct because the 'bin' serialized field is declared as holding a single reference type.
bin = new List<System.Object>() { new Cube() };
}
}
}
interface인 IShape가 있고, Cube는 이를 상속, Thing은 아무 것도 상속받지 않는다.
그리고 inventory, bins가 각각 IShape, System.Object를 품는 리스트로 생성되었고, bin은 System.Object 변수다.
여기서 System.Object는 자바의 그것과 같으며, 모든 클래스의 부모 클래스를 뜻한다.
[ExecuteInEditMode]는 Play 중이 아니어도 스크립트를 실행하는 Attribute다.

IShape, Thing 모두 [Serializable] Attribute가 달려 있음에도, Inspector에 나오지 않는다. 이는 직렬화에 실패했음을 나타낸다.
inventory의 경우 (List<IShape>)
IShape는 interface며, 이를 상속한 Cube가 inventory에 들어가 있다. 그러나 직렬화가 되지 않아, 내용을 살펴볼 수 없다.
bins의 경우 (List<System.Object>)
이번엔 아예, 최상위 클래스인 System.Object를 사용했음에도 직렬화에 실패했다. 이로써 SerializeField는 어떤 경우에도 다형성을 지원하지 않음을 알 수 있다.
여기서 중요한 건, List가 보이지만 내부가 빈 게 아닌, List 자체가 보이지 않는다는 점이다. 단순히 다형성을 가진 객체뿐만이 아닌, 다형성 객체를 포함한 자료구조 역시 직렬화가 실패함을 알아야 한다.
SerializeReference를 사용한 경우
using System;
using System.Collections.Generic;
using UnityEngine;
public interface IShape { }
[Serializable]
public class Cube : IShape
{
public Vector3 size;
}
[Serializable]
public class Sphere : IShape
{
public Color color;
}
[Serializable]
public class Thing
{
public int weight;
}
[ExecuteInEditMode]
public class BuildingBlocks : MonoBehaviour
{
[SerializeReference]
public List<IShape> inventory;
[SerializeReference]
public System.Object bin;
[SerializeReference]
public List<System.Object> bins;
void OnEnable()
{
if (inventory == null)
{
inventory = new List<IShape>()
{
new Cube() {size = new Vector3(1.0f, 1.0f, 1.0f)}
, new Sphere() {color = new Color(0.2f, 0.5f, 0.7f)}
};
Debug.Log("Created list");
}
else
Debug.Log("Read list");
if (bins == null)
{
// This is supported, the 'bins' serialized field is declared as holding a collection type.
bins = new List<System.Object>() { new Cube(), new Thing() };
}
if (bin == null)
{
// !! DO NOT USE !!
// Although, this is syntaxically correct, it is NOT supported as a valid serialization construct because the 'bin' serialized field is declared as holding a single reference type.
bin = new List<System.Object>() { new Cube() };
}
}
}
이번엔 같은 코드에서, [SerializeField]만 [SerializeReference]로 교체했다.

inventory의 경우 (List<IShape>)
IShape는 interface며, 이를 상속한 Cube와 Sphere가 inventory에 들어가 있다. 리스트가 다형성을 지원해, 리스트 내에 다른 클래스가 있음에도 잘 표시되는 걸 볼 수 있다.
bins의 경우 (List<System.Object>)
IShape를 상속한 Cube도, 상속이 없는(System.Object만 상속받는) Thing도 리스트에 잘 나타난다.
bin의 경우
여기서 bin은 System.Object로 선언된 변수인데, 이 코드에선 리스트를 할당했다. 주석에도 적혀 있듯, 이는 문법적으론 문제 없으나 지원되지 않는 직렬화다. System.Object로 선언했다면, 여기엔 단일 객체만 할당하도록 하자.
여기서, [SerializeReference]의 진가는 다형성을 지원할 때 나옴을 알 수 있다. 따라서, 앞으로 우리가 필드를 직렬화할 때, 어떤 Attribute를 선택해야하는지 이제 명확히 알 수 있다.
SerializeReference 사용 시 주의사항
SerializeReference를 사용할 때, 짚고 넘어갈 주의사항이 있다.
1. SerializeReference는 SerializeField보다 느리다.
따라서 꼭 필요한 경우가 아니라면, SerializeReference를 남발하는 것은 좋지 않다.
2. UnityEngine.Object의 파생형엔 사용할 수 없다.
GameObject, Transform 등의 클래스 변수엔 [SerializeReference]를 사용할 수 없다.
3. 필드 값은 null일 수 있다.
[SerializeField]를 사용해 [Serializable] 클래스를 등록할 경우, 필드 값은 null이 되지 않는다. 만약 비어있다면, 새로운 None 객체를 생성해 할당한다. 이는 예기치 못한 에러를 일으킬 수 있다.
그러나 [SerializeReference]는 null일 수 있고, null 체크를 통해 이를 검사할 수 있다.
4. SerializeReference를 배열 또는 리스트에 사용하면, 그 자체가 아닌 요소에 적용된다.
흔히 착각하기 쉬운 부분으로, List<IShape>에 [SerializeReference]를 달았다고 해서, List 자체가 직렬화된 게 아님을 알아야 한다.
정확히는 리스트의 요소인 IShape에 해당 Attribute가 적용된 것이고, IShape가 직렬화된 것이다. 배열 및 리스트는 기본 직렬화 법칙을 따른다.
5. 참조된 값은 UnityEngine.Object 인스턴스 공유되지 않는다.
두 MonoBehaviour 객체가 [SerializeReference]로 같은 객체를 가리킨다해서, 두 객체 간 값이 공유되지 않는다. 이는 Reference라는 이름에서 흔히 혼동할 수 있는 부분이다.
만약 두 객체에서 같은 객체를 참조하고 싶다면, ScriptableObject를 사용해 공유해야 한다.
그럼 왜 SerializeField, SerializeReference라는 이름이 붙었을까?
이를 이해하려면 직렬화에 대한 이해가 필요하다. 따라서, 다음 포스팅에서 직렬화와 함께 알아보겠다.
마무리
오늘은 SerializeField, SerializeReference, Serializable에 대해 차이점을 위주로 살펴보았다.
Custom Inspector를 쓰기 위해 아무 생각 없이 썼던 Attribute인데, 탄생 배경과 기존 [SerializeField]와의 차이점을 비교하니 기능이 명확히 이해되었다.
직렬화에 대한 개념이 빠져 있어 완벽한 글은 아니기에, 다음 포스팅은 직렬화에 대해 써보려 한다. 만약 글 작성이 완료되면 스크랩을 걸어두겠다.
오늘 내가 정리한 내용이 다른 사람들에게 도움이 되었으면 한다.
+) SerializeReference 사용 시 발생하는 문제 추가
최근 이와 관련해 이슈를 하나 만나서 블로그에 공유한다.
다음 포스트에서 자세히 설명하지만, SerializeReference는 직렬화된 데이터에 참조 값, 즉 주소를 저장한다.
그런데, Generic.List<T>를 비롯한 ReorderbleList의 Add(+) 버튼은, 마지막 요소를 복제(값 복사)한다.
이는 직렬화된 데이터 기준으로, 참조 값이 그대로 복사되는 것이며, 새로 생성한 요소의 필드 중 [SerializeReference]가 적용된 필드는 이전 요소와 값을 공유하는 문제가 발생한다.
아직까진 ReorderbleList의 + 버튼을 오버라이딩 하는 것 외엔 해결법을 찾지 못해, 단순히 해당 필드만 직접 바꿔주는 방식을 취하고 있다. 만약 좀 더 나은 해결법을 찾는다면 다시 공유하겠다.
참고 자료
- maintain_H, "Unity-스크립트-직렬화-SerializeField-Serializable", 티스토리, 2023. 3. 24., https://maintaining.tistory.com/entry/Unity-스크립트-직렬화-SerializeField-Serializable
- artiper, "[Unity Editor Scripting] 유니티의 직렬화(serialization)와 재귀 문제 회피", 티스토리, 2024. 11. 9., https://artiper.tistory.com/357
- Unity Documentation, "SerializeReference", Unity 스크립팅 API Ver. 2020.1, https://docs.unity3d.com/kr/2020.1/ScriptReference/SerializeReference.html
- Unity Documentation, "SerializeReference", Unity 스크립팅 API Ver. 6.1, https://docs.unity3d.com/6000.1/Documentation/ScriptReference/SerializeReference.html
- 테샤르, "Unity) SerializeField / SerializeReference", 티스토리, 2022. 8. 30., https://drehzr.tistory.com/1507
- ThetaTT, "[SerializeReference] is very powerful, why is no one speaking about it?", Reddit, 2023. 7. 13., https://www.reddit.com/r/Unity3D/comments/14y0c1q/serializereference_is_very_powerfull_why_is_no/?tl=ko
- wlsdn629, "유니티-SystemSerializable에-대해-알아보자", 티스토리, 2024. 5. 10., https://wlsdn629.tistory.com/entry/유니티-SystemSerializable에-대해-알아보자