ScriptableObject 단점 - ScriptableObject danjeom

샘플 프로젝트 (Unite - 2017)

소스 : github.com/roboryantron/Unite2017

roboryantron/Unite2017

Sample project for Game Architecture with Scriptable Objects from Unite Austin 2017 - roboryantron/Unite2017

github.com

ScriptableObject 단점 - ScriptableObject danjeom

기본 아이디어

ScriptableObject 단점 - ScriptableObject danjeom

아이디어를 크게 도식화 하면 위와 같다. Player와 다른 개체들 사이를 Asset이 이어주고 매개한다.

ScriptableObject 단점 - ScriptableObject danjeom

Player라는 프리팹 개체에는 UnitHealth.cs라는 MonoBehaviour가 정의됐다. 해당 스크립트에는 HP 라는 필드가 선언됐다. 그리고 값으로 해당 에셋이 링크됐다. Player 개체는 플레이 중 HP의 값을 변경한다.

MonoBehaviour는 필요한 프로퍼티를 명시만해주고 값이 정의된 SO의 asset파일을 링크해준다.

ScriptableObject 단점 - ScriptableObject danjeom

Player 개체가 HP의 값을 변경하는 한편, HealthUI개체는 현재 HP값을 토대로 UI에 디스플레이한다. HealthUI의 MonoBehaviour 스크립트는 Player와 똑같은 에셋을 참조하고 있다. 

즉, HP 에셋은 Player가 Write/Read, 다른 개체가 Read 하면서, 공유되고 있다. 이를 공유데이터(Shared Data)라고 한다. 

공유 데이터를 에셋으로 운용하는 것은 기존 방식과 어떤 것이 다르고 무슨 이점을 얻을 수 있을까?

기존 싱글톤 매니저의 장단점

ScriptableObject 단점 - ScriptableObject danjeom

기존 방식처럼 Player와 다른 개체들이 같이 사용하는 공유데이터를 싱글톤 PlayerManager가 매개한다고 생각해보자.

장점

1. 플로우를 이해하기 쉽다.

2. 접근하기 쉽다. 코드 상에서 PlayerManager.Instance만 타이핑하면 된다.

3. 계획하기 편리하다. PlayerManager는 프로젝트에서 사라지지 않고 영속한다. 

단점(Asset으로 극복 가능한)

1. 모듈화 원칙을 깬다.
결론부터 말하면 싱글톤 매니저는 변경에 유연하지 못하고 확장성을 저해한다.MonoBehaviour 스크립트는 디펜던시의 인스턴스가 있는지 없는지 확인해야하며 초기화에 신경써야 된다. 딱히 문제 없어보이지만 '런타임' 중에 해야한다는 것이 맹점이다. 모든 MonoBehaviour 스크립트의 인스턴스는 런타임 때 생성된다. 공든 탑처럼 쌓아올린 초기화 프로세스가 변경에 의해 무너질 수 있어서 변경이 두려워진다. 변경을 반영하기 위해서는 공든 탑을 반드시 점검해야되기 때문에 프로젝트 규모가 크면 클수록 변경 작업의 규모도 덩달아 커진다.

모듈화가 안되면 테스트 비용도 커진다. 새로운 테스트용 씬을 생성하고 그 안에 필요한 모든 오브젝트를 배치해야한다. 필요시 MonoBehaviour 스크립트의 초기화 처리도 수정해야한다.

MonoBehaviour 스크립트의 인스턴스는 런타임 중 생성된다.
따라서 디펜던시를 참조하기 위해 런타임 중에 인스턴스를 초기화해야 한다.
인스턴스를 구하는 2가지 방법이 있다.

첫 번째, Find() 함수를 사용한다.
방법은 간단하지만 Null 참조 없이 안정적으로 할당하려면 Race Condition을 관리해야한다.
개체 수가 많을수록 성능을 요구하며 최적화를 저해한다.

두 번째, 런타임 이전에 Drag&Drop으로 미리 초기화한다.
씬에 미리 필요한 개체들을 씬에 배치하기 때문에 Race Condition이 발생할 우려는 없다.
하지만 디펜던시가 복잡할수록, 또 개체 수가 많을수록 Drag&Drop 작업 부담도 커진다.
이는 프리팹 단에서 해당 디펜던시를 미리 초기화 할 수 없기에 발생한다.

에셋으로 대체하면...

프리팹 단에서 Inspector 작업 한번으로 모든 초기화 작업을 마친다. 런타임 중에 고려하지 않아도 되므로 씬 내 배치 상태에 대한 초기화 프로세스는 고민하지 않아도 된다.

분리된 프리팹을 디펜던시에 의해 합치지 않아도 된다. 디펜던시 인스턴스는 디스크에 직렬화 된 에셋을 참조하면 된다.

변경 시 해당 MonoBehaviour 스크립트와 관련한 ScriptableObject만 수정하면 된다. 디펜던시와 연관된 모든 초기화 작업은 Inspector뷰에서 이뤄지기 때문에 코드 수정이나 씬 배치 점검이 필요없다.

2. OOP의 다형성 원칙을 깬다.
MonoBehaviour 스크립트는 사용자로써 인스턴스의 구체적인 타입이 뭔진 몰라도 어떤 형태든 동작하기만을 바란다. 이는 OOP가 지향하는 기본 원칙이다(변경시 사용자의 코드도 수정 범위에 포함돼 변경 비용이 커지는 것을 방지). 하지만 싱글톤 매니저는 Instance의 타입을 사용자가 직접 알고 있다는 점에서 다형성 원칙이 깨져버린다.

단, Singleton의 자식 클래스로 여러 Manager를 두고 MonoBehaviour 스크립트에 Drag&Drop으로 초기화하면 다형화 할 수 있다.

에셋으로 대체하면...

사용자인 MonoBehaviour 스크립트는 PlayerManager라는 추상클래스만 알고있고 실제 인스턴스는 에셋에 따라 디버깅용 PlayerManager, 튜토리얼 PlayerManager로 구체화 된다.

3. 유일한 하나의 인스턴스여야만 한다.
만약 플레이어가 2명이면 싱글톤 매니저의 의미는 무색해진다. PlayerManager를 하나 더 만들거나 시스템을 변경해야 한다.

에셋으로 대체하면...

PlayerManager와 관련한 ScriptableObject 에셋을 필요한 만큼 추가해주면 된다.

31 Jul 2019

어느덧 이 회사에 출근한지 3일이 되었다. 언제나 그렇지만 출근하는 첫 주는 굉장히 빨리 지나간다. 그리고 굉장히 피곤하다 (…) 회사 프로젝트의 코드를 파악하면서 여러 문제가 될 만한 것들을 발견했는데, 그 중 하나를 정리해볼까 한다.

이제까지 이 회사를 제외하고 4개의 회사를 다녔고, 그 중 3개의 회사에서 Unity를 사용했지만 이 회사에서 처음으로 ScriptableObject를 실전에서 꽤나 적극적으로 쓰고 있는 걸 보았다. 지난 2017년에 있었던 Unite Seoul에서의 한 세션을 들으면서 ScriptableObject가 좋다고 꼭 써보라고 했을 때만 하더라도 나와는 먼 이야기 같았고, 실제로 내가 겪었던 회사 프로젝트에서는 전혀 쓰지 않았기 때문에 제대로 마주할 기회가 없었다. 그러다가 이 회사에서 부딪히게 되었다.

회사 프로젝트에서는 순수 데이터로만 쓰는 것들은 DB(SQLite)로, Unity Asset과 연관된 데이터들은 ScriptableObject로 관리하고 있었다. 이는 내가 이 회사에 오기 전에 있었던 다른 프로그래머가 했던 코드인데, 이런 식의 쓰임새는 처음이었기에 꽤나 흥미로웠다. 게다가 이 ScriptableObject를 처음 커밋한 로그에는 무려 로딩 개선이라는 문구가 있었다. 실제로 그런 효과가 있었는지는 잘 모르겠지만, 아마 ScriptableObject를 쓰면 게임이 실행됨과 동시에 메모리에 올려버리기 때문에 처음에만 불러오는 데 시간을 쓰고 추후에는 로딩이 없으니 효과가 있었을 지도 모른다. 나도 첫 회사 프로젝트에서 그런식으로 했었으니까. 문제는 게임의 규모가 커지면 항상 들고 있어야 하는 메모리의 양이 장난이 아니기 때문에 가면 갈수록 상황이 나빠진다. 그래서 나도 첫 회사 때 분산으로 로딩하도록 바꾼 기억이 있다. 우리 게임의 런타임 메모리 용량이 꽤나 큰 것으로 알고 있어서 ScriptableObject에서 불러오는 것 중에서 바로 메모리에 올리지 않아도 될만한 것들을 추릴 수만 있다면 꽤나 큰 개선이 되지 않을까 싶었다.

혀튼 ScriptableObject로 관리되는 데이터들을 훑어보았다. 눈에 띄는게 하나 있었다. audiotable… 오디오를 ScriptableObject로 관리한다고? 그럴수도 있다…라고 잠시 생각했지만 압축을 푼 오디오, 특히 배경음들은 굉장한 몸집을 자랑한다. 아직은 회사 저장소에 커밋할 용기(…)가 나지 않아서 새 프로젝트를 만든 후, 게임에서 쓰는 배경음 몇 개를 불러다가 ScriptableObject에 넣고 불러오는 코드를 만들어서 프로파일링을 해보니…

ScriptableObject 단점 - ScriptableObject danjeom

42MB라는 아름다운 숫자가 등장했다. 허허… 재생하고 있지 않아도, 일부만 테스트했는데도 저정도면 대충 어떨지 상상이 된다. 이 시스템을 걷어내고 싶어도 라이브 중인지라 사이드 이펙트가 굉장히 신경쓰였다. 그렇다고 냅둘수도 없는 노릇… 그래서 혹시나 배경음으로 쓰이는 AudioClip들이 Streaming으로 되어있는지 살펴보았는데, 아니나 다를까 Decompress On Load로 설정되어 있었다. 이를 Streaming으로 변경하고 하니까

ScriptableObject 단점 - ScriptableObject danjeom

재생하지 않으니 299B씩만 차지하고 있다. 42MB를 1.4KB 수준으로 낮췄으니 엄청난 성과가 아닐까? 실제 Streaming 상태에서는 얼마가 나오는지는 테스트를 해보아야 겠지만 그리 크지는 않을 것이다. 문제는 Streaming 시 저사양 기기에서 제대로 동작하는지, 다른 문제는 없는지 고려되야 할 것 같다. AudioClip 설정에 대해서 다른 사이트에서 더 자세히 설명 되어 있으니 한번쯤 정독하는 것을 추천한다. 이것 외에는 대부분 Prefab들을 설정해두고 있었다. Prefab들은 그렇게 큰 공간을 차지하지 않고 있었다.

회사를 옮기면서 가장 힘든 것은 적응이다. 하지만 다른 사람이 작업했던 코드를 보는 것도 재미있다. 길지 않은 경력에 여러 회사를 다니게 되었지만 가는 곳마다 코드가 제각각이라 참 재미있다. 물론 그걸 다 파악해야 하는 건 굉장히 힘들고 고된 일이다 (…) 이렇게 새 회사에 와서 새로운 것을 하나하나 경험하고 배워가는 것도 나쁘지 않은 것 같다. 너무 잦으면 피곤하지만 말이다; 일단 정리가 대충 끝났으니 다시 코드의 바다로…ㅠㅠ