Unity에서 게임 데이터를 관리할 때 자주 사용하는 기능이 바로 ScriptableObject이다.
하지만 막상 내부 구조가 어떻게 되어 있는지, 왜 MonoBehaviour와 다르게 동작하는지 확실하게 알지 못해서 이번 기회에 파헤쳐보려고 한다.
Unity 공식 문서에는 ScriptableObject를 "클래스 인스턴스를 에셋 파일 형태로 저장할 수 있는 데이터 컨테이너"라고 설명한다.
하지만 이런 문장만 봐서는 정확한 감이 오지 않기 마련이다.
간단히 말하면 ScriptableObject는
씬에 존재하지 않아도 살아 있는 독립적인 C# 객체
이다.
MonoBehaviour처럼 GameObject에 붙을 필요도 없고, Prefab처럼 복사본이 여러 개 생기지도 않는다.
이 부분이 가장 궁금한 부분이다.
ScriptableObject는 프로젝트 폴더에 저장된 '에셋 파일'이다. 하지만 런타임에 불러오면 다음과 같은 구조가 된다.
<1. Unity는 ScriptableObject 에셋을 읽어들임>
AssetDatabase 또는 Resources/Addressable 로딩을 통해 메모리에 로드된다.
<2. 메모리에 "단일 인스턴스"가 생성됨>
씬마다 복사본이 생기지 않고, 프로젝트 전체에서 하나의 인스턴스만 존재한다.
따라서 여러 오브젝트가 같은 ScriptableObject를 참조하면 모두 동일한 데이터를 공유하는 효과가 있다.
이게 Prefab과 가장 큰 차이점이다.
- MonoBehaviour
- 항상 GameObject와 함께 존재
- 씬이 바뀌면 오브젝트도 Destroy
- 인스턴스는 씬마다 존재
- Prefab
- GameObject 구조를 템플릿처럼 저장한 것
- Instantiate 할 때마다 새로운 복사본이 생성됨
- ScriptableObject
- GameObject 필요 없음
- 프로젝트 전체에서 단일 인스턴스로 존재
- 씬이 바뀌어도 유지됨
- 데이터 공유에 최적화됨
즉,
ScriptableObject = 데이터 저장용 싱글 인스턴스 에셋
이라고 이해하면 된다.
많은 개발자들이 ScriptableObject를 사용하면 메모리를 아낀다는 사실은 알고 있을 것이다.
이 말의 본질은 중복 인스턴스가 생기지 않는다 라는 뜻이다.
예를 들어,
public class ItemInfo : ScriptableObject { public string name; public int price; }
이걸 100개의 오브젝트가 다같이 참조해도 인스턴스는 메모리에 딱 1개만 존재한다.
Prefab이나 MonoBehaviour처럼 오브젝트가 늘어날 때마다 데이터가 복사되는 구조가 아니다.
-> 대규모 데이터 기반 시스템에서는 이 점이 매우 중요하다.
ScriptableObject의 장점은 크게 세 가지이다.
<1. 에셋 파일로 저장되어 직렬화 지원이 뛰어남>
<2. 동일 인스턴스를 여러 곳에서 공유 가능>
<3. 씬과 생명주기가 분리되어 있음>
이건 DontDestroyOnLoad와도 비슷할 수 있는데,
"ScriptableObject는 아예 씬에 소속되지 않는다."
그래서
당연히 장점만 있는 기능은 아니다. 알고 쓰지 않으면 예상치 못한 문제가 생길 수 있으니 꼭 주의할 점을 숙지하자.
<1. 에디터에서는 값이 원본 에셋에 바로 반영됨>
런타임 중 수정하면 OnValidate를 통해 프로젝트의 에셋 값이 실제로 변경될 수 있다.
따라서 "런타임에서 수정 가능한 데이터"로 쓰는 것은 위험하다.
<2. 런타임 수정은 메모리 인스턴스만 바뀐다>
파일에는 반영되지 않기 때문에 다음 실행 때 값이 초기화된다.
<3. Instantiate를 사용하면 복사본이 생긴다>
ScriptableObject는 기본적으로 단일 인스턴스이지만, Instantiate(SO)를 하면 복사본이 생긴다.
따라서 디자인 의도를 명확히 하여 잘 구분해야 한다.
ScriptableObject의 핵심은 다음과 같다.
씬에 속하지 않고, 프로젝트 전체에서 하나의 인스턴스로 존재하는 데이터 에셋
이 구조 덕분에 ScriptableObject는
같은 다양한 장점들을 가진다.