[Unity] ScriptableObject과 GC(Garbage Collection)

 

ScriptableObject 장점

1. 데이터 재사용 용이

  • ScriptableObject는 하나의 에셋 개념으로, 여러 프리팹 또는 스크립트에서 공유 가능
  • 예) 동일한 몬스터 데이터 여러 개 스폰 시 재사용 -> 성능 최적화 및 일관성 보장

2. 설정 및 유지보수 용이

  • ScriptableObject는 인스펙터에서 직접 수정 가능
  • 코드 수정 없이 밸런싱 가능 (예 - 이동속도, 공격력, HP 등)

3. GC 부담 감소

  • 일반 클래스는 new로 생성되기 때문에 다수 객체 생성 -> 메모리 할당/해제 시 GC에 부담
  • ScriptableObject는 에셋이기 때문에 GC 대상이 아님 -> 런타임 성능 유리

4. 모듈화 및 설계 분리

  • 데이터를 구현 코드와 분리할 수 있음 -> 클린 아키텍쳐 적용에 유리
  • 전략 패턴, 팩토리 패턴과 잘 맞음

5. 씬 간 공유 가능

  • A씬에서 설정한 데이터를 B씬에서 그대로 참조 가능 -> 비종속적

 

ScriptableObject 주의점

1. 런타임에서 데이터 변경 시 저장되지 않음

  • 변경 사항은 플레이모드 종료 시 초기화됨

2. 동시성 문제

  • 공유된 ScriptableObject 인스턴스에 런타임 중 값 변경 시 그 값을 참조한 모든 객체에 영향을 줄 수 있음
  • 예를 들어 이동 속도, 최대 체력 등 설정 값만 사용하고, 현재 남은 체력 등의 요소는 ScriptableObject에 사용 금지
  • ScriptableObject 값을 MonoBehaviour에 복사해 원본 수정이 안되도록 사용하거나 읽기 전용으로 사용
/// <summary>
/// MonsterDataSO 데이터
/// </summary>
[CreateAssetMenu]
public class MonsterDataSO : ScriptableObject
{
    public float hp;
}




/// <summary>
/// MonsterDataSO 데이터를 공유할 Monster 클래스
/// </summary>
public class Monster : MonoBehaviour
{
    public MonsterDataSO data;

    public void TakeDamage(float damage)
    {
        data.hp -= damage; //  여기서 문제 발생
    }
}




// 1. slimePrefab1 → data.hp = 100 (SO 참조)
// 2. slimePrefab2 → data.hp = 100 (같은 SO 참조)

// → slimePrefab1이 공격받아서 hp -= 50 하면...
// → slimePrefab2의 data.hp도 50이 되어버림



/// <summary>
/// MonsterDataSO 데이터의 올바른 사용법
/// </summary>
public class Monster : MonoBehaviour
{
    public MonsterDataSO data;

    private float currentHp; // 복사본 생성

    void Start()
    {
        currentHp = data.hp; // 복사해서 상태로 사용
    }

    public void TakeDamage(float dmg)
    {
        currentHp -= dmg; // 원본 SO는 건드리지 않음
    }
}

 

 

 

ScriptableObject는 일반 클래스와 어떻게 다른가??

항목 ScriptableObject  일반 C# 클래스 (new로 생성하는 인스턴스)
메모리 위치 Unity 엔진이 관리하는 Asset 영역 일반적으로 RAM 영역 (GC 대상)
생성 방법 CreateAssetMenu로 에셋 파일 생성 new MyClass()로 런타임에 생성
에디터 저장 가능 (프로젝트 저장됨) 불가능 (런타임에만 존재)
직렬화 Unity 직렬화 지원 (Inspector에 보임) Unity 직렬화 안 됨 (보통 별도 처리 필요)
씬 간 공유 가능 (프리팹, 오브젝트 간 공유 쉬움) 불가능 (매번 복사해서 넘겨야 함)
어드레서블 연동 가능 (데이터도 Addressable 관리 가능) Addressable 관리 대상 아님
성능 측면 GC 없이 사용 가능 GC 대상, 필요시마다 생성/해제 필요

 

 

 

그럼 ScriptableObject는 어떻게 메모리 측면에서 유리한가??

일반 클래스 방식

// 일반 클래스
public class MonsterData
{
    public float hp;
    public float speed;
}


// 웨이브 1에서 슬라임 10마리 소환 시
List<Monster> monsters = new List<Monster>();
for (int i = 0; i < 10; i++) {
    var data = new MonsterData { hp = 100, speed = 1.0f }; // ← 여기서 매번 인스턴스 생성됨
    monsters.Add(new Monster(data));
}

결과

  • MonsterData가 메모리에 10개 복사됨
  • new 키워드로 생성 시 GC 대상이 되고 양이 많아질 수록 할당/해제 비용이 증가
  • 데이터 내용이 같지만 매번 new 로 생성되므로 메모리 낭비 발생

 

ScriptableObject 방식

// MonsterDataSO 데이터
[CreateAssetMenu]
public class MonsterDataSO : ScriptableObject
{
    public float hp;
    public float speed;
}


// 웨이브 1에서 슬라임 10마리 소환 시
for (int i = 0; i < 10; i++) {
    Instantiate(slimePrefab).Init(MonsterDataSO);  // ← 하나의 SO를 공유해서 참조만 전달
}

결과

  • 메모리에 존재하는 MonsterDataSO는 1개
  • 10마리 몬스터는 모두 같은 ScriptableObject 인스턴스를 참조만 함
  • 메모리 절약 및 ScriptableObject는 에셋으로 GC 부하도 없음

항목 일반 클래스 (new) ScriptableObject

항목 일반 클래스 (new) ScriptableObject
생성 횟수 N번 복사 1번만 로딩
메모리 사용 데이터당 중복 공유 참조
GC 영향 O (할당/해제) X (에셋으로 존재)
구조 일관성 직접 관리해야 함 에셋으로 중앙 관리
변경 시 일괄 반영 X O (SO 수정 시 모든 참조에 적용)

 

 

 

 

그렇다면 GC는 무엇이고 ScriptableObject이 어떤 영향을 주는가??

먼저 GC란?

  • C#에서 사용하지 않게 된 메모리를 자동으로 정리해주는 기능
  • new로 객체 생성 시 메모리에 올라가서 공간 차지
  • 그 객체를 더 이상 참조하지 않는 경우 메모리만 차지하고 쓸모없어짐
  • 이 때 GC가 쓸모없는 객체를 찾아서 정리해줌

유니티에서 GC가 발생되는 시점

  • new로 객체 생성
  • string 문자열 조작 (덧셈연산, Split, Substring, Replace, ToUpper, ToLower..... StringBuilder는 살짝 논외. 객체 생성 후 ToString() 할 때 한 번만 GC 발생)
  • List<T>, Dictionary<K,V> 등의 컬렉션 조작
  • LINQ 사용
  • Instantiate / Destroy 사용
  • Boxing / Unboxing (object o = 42;   int x = (int)o;)
  • 반복적인 이벤트 등록 및 해제

 

Profiler에서 보는 GC 지표

항목 의미 언제 증가? 줄이는 법 주의 포인트
GC Alloc (Frame) 현재 프레임에서 새로 힙에 할당된 GC 대상 메모리 크기 new, string, List<T>.Add, LINQ, Split() 등 GC 발생 유도 줄이기 (StringBuilder, 풀링 등) 값이 매 프레임 >0이면 주의!
Total
Reserved
전체 힙에서 예약된 메모리 크기 (GC가 사용할 메모리 풀) 유니티가 내부적으로 힙 공간 늘릴 때 직접 제어 불가, 하지만 줄일 수 있는 간접 방법 존재 계속 증가만 한다면 메모리 누수 가능성
GC Count GC.Collect가 실행된 횟수
(실제 메모리 회수된 시점)
GC가 조건에 따라 자동 또는 수동 호출 시 GC Alloc 줄이기 너무 자주 발생하면 프레임 드랍 유발
GC Time (ms) GC가 실행되는데 걸린 시간 위와 동일 위 3가지 줄이면 자연히 줄어듦 값이 높으면 GC 중지 시간 동안 FPS 뚝 떨어짐

 

반응형