서론
어제, 버그를 고치려다 우연히 다른 버그의 해결법을 찾게 되었다.


맨 위에서부터 스킬을 사용하는데, "Gimmick Triggerd"라는 스킬이 스택을 꽉 막고, RecoverAPSkill을 무한 증식 시키는 버그였다.

이 부분을 잘못 구현해서 생긴 이슈였는데, 문제는 정상 스킬로 되돌리고 id를 다르게 바꾸어도 해당 스킬이 사라지지 않았던 것이다. (아마 스킬 용도가 바뀌면서 변수명도 바꿨나보다.)

ctrl + shift + f로 모든 파일을 검색해봐도 Gimmick Triggerd라는, 오타난 스킬은 여기 말곤 없었는데, 여길 바꿔도 게임만 실행하면 유령처럼 스택에 스킬이 생성됐다.
배경
나는 [SerializeField]와 [HideInInspector] 어트리뷰트를 적극적으로 사용하고 있다.
흔히 유니티를 막 배웠다면 public은 인스펙터에 나타나고 private은 안 나타나요. 같은 말을 들었을 텐데, 사실 인스펙터에 나타나는 건 부차적인 효과고, 다른 클래스에서 접근 가능하냐가 이 두 접근 제한자의 핵심이다.
그렇기에 타 클래스에서 접근하면 안 되지만 인스펙터에 나타나야 하는 경우나, 타 클래스에서 접근해야 하지만 인스펙터에 나오면 안 되는 경우를 위해 접근 제한은 public과 private으로, 인스펙터에 나타내는 건 [SerializeField]와 [HideInInspector]로 조절하고 있었다.
원인
허나 내가 간과한 것은, 두 어트리뷰트의 기능이 완전 다르다는 것.
이전 포스트에서도 한번 언급했는데, SerializeField는 인스펙터에 나타내기 같은 기능이 아닌, 직렬화 수행이다. 해당 필드가 직렬화되기에 인스펙터에 나타나는, 따지고보면 2차 효과 같은 느낌이다.
[Unity] 직렬화(Serialization)를 이해해보자
서론유니티로 개발하다보면, 알기 싫어도 알게 되고, 또 모르면 가끔 크게 데이는 것들이 있다. 그중 하나가 바로 직렬화다. 이번 시간엔 앞서 예고했듯, 직렬화란 무엇인지 차근차근 이해해보
autumncat.tistory.com
직렬화에 관한 건 이 포스트를 참고하자.
그런데, HideInInspector는 이름 그대로 인스펙터에서 숨기기만 한다. 이게 무슨 말이냐하면, 인스펙터에 나타나지 않아도 해당 필드는 직렬화되고 있다는 뜻이다.

그렇기에, 실제 파일을 열어보면 내가 스크립트 내에서 수정한 것과 달리 notifySkill이 수정 전 상태로 저장되어 있다.

이렇게보면 당연한 결과였다. 하지만, 이름이 다르다는 것도, 동작 방식이 다르다는 것도 분명 알고는 있었는데, 막상 체득은 덜 됐는지 이런 일이 생겼다.
해결
해결 방법은 다양하다.
- private으로 바꿔서 파일들을 전부 갱신한 후(레거시 삭제), 다시 public으로 되돌린다.
- 필드를 private으로 만들고, 함수나 프로퍼티로 캡슐화한다.
- 직렬화를 못하게 막는다.
1은 간단히 봐도 노가다고, 임시 방편이다. notifySkill이 또 바뀌면 다시 모든 파일을 갱신해줘야 하는데, 이걸 별도의 툴로 자동화시킨다한들 귀찮고, 불필요한 작업이다.
2는 일종의 정석이다. 애초에 멤버 변수를 직접 노출시키고, 그대로 갖다 쓰는 건 좋은 방법은 아니다.

그리고 한 가지 방법이 더 있는데, 직렬화를 못 하게 막는 어트리뷰트를 붙이는 것이다.
이전에 본 [System.Serializable]이 사용자 클래스를 직렬화시킨 것처럼, [System.NonSerialized]를 붙이면 직렬화에서 제외된다. 앞에 System 네임스페이스가 붙은 것에서 유추할 수 있듯, 이또한 .NET의 기본 기능이다.
직렬화가 되지 않기에, 당연히 인스펙터에도 나타나지 않는다. 정말 기능만 놓고 보면, 사실 [SerializedField]의 반대 기능은 [System.NonSerialized]였던 것.
어떤 방법을 써야 할까?
정답은 없다. 애초에 2와 3은 지향하는 방향이 조금씩 틀리니까.
프로퍼티는 클래스 간 연결 방식을 다룬다. 해당 필드가 타 클래스에서 접근이 가능한지(애초에 불가능하면 프로퍼티를 안 쓰겠지만), get만 가능한지, set만 가능한지 등을 유동적으로 조절할 수 있다. 단, 이는 함수를 축약해놓은 것이기에 성능 오버헤드가 약간 있으며, public 변수로도 readonly 등을 통해 어느 정도 구현이 가능하다.
어트리뷰트는 해당 필드의 직렬화 여부를 다룬다. 하나의 클래스와, 이를 사용하는 유니티 엔진, 파일 저장 방식 등과 관련된 것이다.
당신이 만든 이 필드가, 유니티를 껐다켜고, 게임을 껐다 켰을 때 스크립트 외적으로 값을 저장하고 있어야 하는지(재사용 되는 객체인지), 혹은 매번 게임을 실행할 때마다 그때그때 생성해도 되는 것인지에 따라 달라질 것이다.
이번 케이스라면, notifySkill은 직렬화할 필요 없고, 타 클래스에겐 읽기 전용으로 공유되어야 하기에, 둘을 섞어 쓰거나 [System.NonSerialized]만 쓸 것 같다.
개인 스타일로는, 프로퍼티가 익숙하지 않고 큰 효용을 못 느끼는 만큼, 단순히 어트리뷰트로 끝낼 것 같다.
결과

그래서 내가 선택한 결과물은 이건데, 간단히 내가 편한 방식으로 써서 이런 거지, 만약 내가 회사나 팀에 합류했을 때 팀이 프로퍼티를 자주 사용한다면 프로퍼티도 붙였을 것이다.
본인의 손에 더 익은 방식이 있다면 그걸 써도 좋다. 단, 프로퍼티와 어트리뷰트의 역할이 다름은 인지하고, 정확한 의도로 써야할 것이다.
마무리
이번 시간엔 흔히 알아채기 힘든, [HideInInspector] public 사용 시 발생할 수 있는 유령 데이터 문제에 대해 알아봤다.
이번 기회에 자주 쓰던 어트리뷰트에 대해 자세히 알게 된, 뜻깊은 시간이었다. (왜 하루 지나서 블로그 썼냐면 글쓰는 게 생각보다 오래 걸려서...)
또 개발하다 신기한 거 발견하면 자랑하러 와야 겠다. 확실히 GPT보단 제미나이가 코딩을 잘 하는 것 같고, 클로드는 저번에 약관 읽어보다 쎄한 부분이 있어서 쓰지 않고 있다. (제미나이는 같은 약관 있어도 이미 구글 계정 만든 시점에 털렸을 거 같아서 그냥 쓴다.) 우선 제미나이에 가볍게 물어보고, 나온 결과를 구글링해서 교차 검증하는 식으로 개발하니 버그 고치는 속도가 확연히 빨라졌다. 오히려, 구글링으로 찾은 포스팅들에 잘못된 정보가 섞이거나, 완전 초보자용 겉핥기 지식만 있는 경우도 많다보니, AI 답변대로 해보고 되면 그냥 넘어가는 게 나은 것 같기도 하다. 앞으로 더 많은 게임을, 더 쉽게 만들게 되면, 더 좋은 게임들이 나올 수 있겠지.
참고 자료
- Google Gemini
- kj1241, "Unity에서 [NonSerialized] 사용법과 C# 직렬화의 이해", GitHub Pages, 2022.12.23, https://kj1241.github.io/unity/UnityNonSerialized
'Unity > 팁' 카테고리의 다른 글
| [Unity] Unity Recorder - 게임 화면 녹화하기 (2) | 2024.03.12 |
|---|