내일배움캠프 - TIL/내일배움캠프 - TIL

내일배움캠프 30일차 - 구조 설계

rudals4469 2025. 6. 4. 16:35

1. 프로젝트의 커스텀 Enum 데이터 타입 관리

Enum을 활용하면 코드의 가독성이 높아지고, 값의 무결성을 유지할 수 있다. 특히 문자열이나 숫자 값으로 직접 비교하는 방식과 비교했을 때, 실수를 줄이고 유지보수가 쉬워진다.

 

장점:

  • 코드 자동완성을 활용할 수 있어 실수 방지가 가능하다.
  • 가독성이 높아지고, 오타로 인한 버그를 방지할 수 있다.
  • 데이터 타입이 명확하게 정의되므로 유지보수가 용이하다.

단점:

  • 변경이 잦은 경우, Enum을 수정해야 하므로 코드 수정이 필요하다.
  • 확장이 필요한 경우 기존 Enum을 변경해야 하는 부담이 있다.

예제:

namespace EnumTypes
{
    public enum AttackTypes { None, Melee, Range }
    public enum CardRanks { Normal, Special, Rare }
    public enum CardHowToUses { Normal, TargetGround, TargetEntity }
    public enum CardAfterUses { Discard, Destruct, Spawn }
    public enum GameFlowState { InitGame, SelectStage, Setting, Wave, EventFlow, Ending }
}

 


2. 프로젝트의 커스텀 Struct 데이터 타입 관리

Struct는 값 타입이므로 힙이 아닌 스택 메모리에 저장되며, GC(가비지 컬렉션)의 부담이 줄어든다. 객체 참조 타입인 클래스보다 메모리 할당이 빠르고, 값 복사가 이루어지므로 데이터의 변경이 독립적으로 이루어진다.

 

장점:

  • 값 타입이므로 GC 부담이 적고, 성능이 중요한 경우 유리하다.
  • 불필요한 참조를 방지하여 데이터 일관성을 유지할 수 있다.

단점:

  • 크기가 큰 데이터 구조를 Struct로 만들면 오히려 성능이 저하될 수 있다.
  • 값 타입이기 때문에 변경 시 복사가 일어나 메모리 사용량이 증가할 가능성이 있다.

예제:

namespace Structs
{
    [Serializable]
    public struct AttackData
    {
        public AttackTypes attackType;
        public int attackAnimationIndex;
    }

    [Serializable]
    public struct StatModifierData
    {
        public StatTypes statType;
        public ModifierTypes modifierType;
        public float value;
    }
}

 


3. 프로젝트의 유틸리티 함수 관리

유틸리티 함수는 자주 사용되는 기능을 하나의 클래스로 모아서 재사용성을 높이는 방식이다. 일반적으로 static 클래스로 정의하여 인스턴스 생성을 방지하고, 전역적으로 접근할 수 있도록 한다.

 

장점:

  • 중복 코드가 줄어들고 유지보수가 쉬워진다.
  • 인스턴스를 만들 필요 없이 어디서든 접근 가능하다.
  • 자주 사용하는 기능을 모듈화하여 코드의 가독성이 높아진다.

단점:

  • static 함수로만 구성되므로 다형성을 활용하기 어렵다.
  • 전역적으로 접근할 수 있어 남용할 경우 의존성이 증가할 위험이 있다.

예제:

public static class Utils
{
    public static float DirectionToAngle(float x, float y)
    {
        return Mathf.Atan2(y, x) * Mathf.Rad2Deg;
    }

    public static int GenerateID<T>()
    {
        return Animator.StringToHash(typeof(T).Name);
    }
}

 


4. 프로젝트의 글로벌 변수 관리

전역 변수를 한 곳에서 관리하면 여러 곳에서 동일한 값을 사용할 때 유지보수가 쉬워진다. 하지만 변하는 값을 전역으로 관리하면 프로그램의 흐름을 예측하기 어려워지므로, 반드시 불변 값만 글로벌 변수로 지정해야 한다.

 

장점:

  • 동일한 값을 여러 곳에서 사용할 때 일관성을 유지할 수 있다.
  • 값 변경이 불가능하므로 의도하지 않은 수정으로부터 보호할 수 있다.

단점:

  • 너무 많은 글로벌 변수를 사용하면 관리가 어려워질 수 있다.
  • 값을 변경해야 할 경우 코드 수정이 필요하다.

예제:

public static class Globals
{
    public const int WorldSpaceUISortingOrder = 1;
    public static class LayerName
    {
        public static readonly string Default = "Default";
        public static readonly string UI = "UI";
    }
}

 


5. 생성될 객체들을 관리하는 관리자 클래스

Unity에서는 게임의 다양한 오브젝트를 효율적으로 관리하기 위해 관리자 클래스를 도입하는 것이 일반적이다. GameManager를 최상위 관리자로 두고, 하위 관리자 클래스를 계층적으로 배치하면 모든 오브젝트 간 관계를 명확하게 정리할 수 있다.

 

장점:

  • 게임의 핵심 관리 구조를 명확하게 구성할 수 있다.
  • 하위 관리자들이 상위 관리자와 명확한 관계를 유지하므로 유지보수가 쉬워진다.
  • 필요할 때만 특정 관리자에 접근하여 데이터를 효율적으로 처리할 수 있다.

단점:

  • 계층 구조가 깊어질 경우 접근 방식이 복잡해질 수 있다.
  • 잘못된 설계 시 특정 관리자에 과도한 역할이 집중될 위험이 있다.

예제:

public class GameManager : MonoBehaviour
{
    [SerializeField] private CharacterManager _characterManager;
    [SerializeField] private UIManager _uiManager;
}

public class CharacterManager : MonoBehaviour
{
    private GameManager _gameManager;
    public void Init(GameManager gameManager) => _gameManager = gameManager;
}

이렇게 설계하면 유지보수성이 높아지고, 개발 속도를 빠르게 할 수 있으며, 협업 시에도 원활한 관리가 가능하다. 코드 구조를 체계적으로 유지하여 유지보수를 쉽게 만들고, 확장 가능한 게임 개발을 위한 기반을 마련할 수 있다.


6. Scene 전환과 관계없이 유지되는 싱글턴 객체 관리

게임 개발에서는 Scene이 변경되더라도 유지되어야 하는 데이터가 존재한다. 예를 들면, 사용자 설정, 계정 정보, 네트워크 상태 등이 이에 해당한다. Unity에서는 DontDestroyOnLoad를 사용하여 객체가 Scene 전환 시에도 삭제되지 않도록 설정할 수 있다. 이를 관리하기 위해 일반적으로 싱글턴(Singleton) 패턴을 활용한다.

 

장점

  • Scene 변경 시에도 특정 데이터를 유지할 수 있어 게임 흐름이 끊기지 않는다.
  • 전역적으로 접근할 수 있어 관리가 편리하다.
  • 여러 클래스에서 동일한 객체를 공유할 수 있어 데이터의 일관성을 유지할 수 있다.

단점

  • 싱글턴을 과도하게 사용하면 객체 간 결합도가 높아지고 유지보수가 어려워질 수 있다.
  • 테스트 및 멀티스레딩 환경에서 동기화 문제가 발생할 수 있다.

예제:

아래 코드는 DontDestroyOnLoad를 활용하여 Scene이 변경되어도 유지되는 싱글턴 객체를 구현한 예제이다.

public class GameInstance : Singleton<GameInstance>
{
    private LogGUI _logGUI;
    private DebugStatGUI _debugStatGUI;
    private ScriptableObjects.GamePrefabs _gamePrefabs;
    private HttpManager _httpManager;
}

public abstract class Singleton<T> : MonoBehaviour where T : Component
{
    private static T instance;

    public static T Instance
    {
        get
        {
            if (instance == null)
            {
                instance = FindObjectOfType<T>();
                if (instance == null)
                {
                    GameObject obj = new GameObject { name = typeof(T).Name };
                    instance = obj.AddComponent<T>();
                }
            }
            return instance;
        }
    }

    protected virtual void Awake()
    {
        if (instance == null)
        {
            instance = this as T;
            DontDestroyOnLoad(gameObject);
        }
        else
        {
            Destroy(gameObject);
        }
    }
}

이렇게 하면 GameInstance 객체는 Scene이 변경되더라도 유지된다. 하지만 모든 데이터를 싱글턴으로 관리하면 결합도가 높아지고 유지보수가 어려워질 수 있으므로, 필요한 경우에만 싱글턴 패턴을 적용하는 것이 중요하다.


7. ScriptableObject를 활용한 데이터 관리

ScriptableObject는 에디터에서 데이터를 수정하고 런타임에서는 읽기 전용으로 사용하는 정적 데이터 관리 방식에 적합하다. 이는 게임 내 아이템 정보, 레벨 디자인 데이터, 캐릭터 능력치 등의 변하지 않는 데이터를 관리할 때 유용하다.

 

장점

  • 에디터에서 데이터를 편집할 수 있어 코드 수정 없이 데이터 변경이 가능하다.
  • 런타임에서는 읽기 전용으로 동작하여 불필요한 메모리 할당을 방지한다.
  • 여러 개의 오브젝트가 동일한 데이터를 공유할 수 있어 메모리 효율적이다.

단점

  • 런타임에서 ScriptableObject 데이터를 변경해도 저장되지 않으며, 재시작 시 초기화된다.
  • 동적으로 변경되는 데이터(예: 경험치, 인벤토리 상태)에는 적합하지 않다.

예제:

namespace ScriptableObjects
{
    [CreateAssetMenu(fileName = "ItemData", menuName = "ScriptableObjects/ItemData", order = 1)]
    public class ItemData : ScriptableObject
    {
        public string itemName;
        public int itemID;
        public Sprite itemIcon;
    }
}
namespace ScriptableObjects
{
	[Serializable]
	[CreateAssetMenu(fileName = "Data", menuName = "ScriptableObjects/Artifact", order = 1)]
	public class Artifact : ScriptableObject
	{
		[SerializeField]
		private Sprite _thumbnail;

		[SerializeField]
		private string _name;

		[SerializeField]
		private StatModifierGroupData _statModifierGroup;

		public Sprite Thumbnail
		{
			get { return _thumbnail; }
		}

		public string Name
		{
			get { return _name; }
		}

		public StatModifierGroupData StatModifierGroup
		{
			get { return _statModifierGroup; }
		}

	}

}

이렇게 정의된 ScriptableObject는 Inspector에서 데이터를 수정하고 게임 내에서 읽기 전용으로 사용할 수 있도록 설계되었다. 이를 통해 데이터 일관성을 유지하면서도, 직관적인 데이터 관리가 가능하다.


8. 데이터 덩어리 객체 관리

게임 개발에서는 여러 데이터를 한곳에 모아 관리할 필요가 있다. 이를 위해 **데이터 덩어리 객체(Data Container Object)**를 활용할 수 있다. 이는 여러 ScriptableObject를 한곳에서 관리할 수 있도록 설계된다.

 

장점

  • 관련된 데이터를 한곳에서 관리하여 접근이 용이하다.
  • 여러 개의 ScriptableObject를 참조하여 데이터 간 연관성을 쉽게 유지할 수 있다.

단점

  • 너무 많은 데이터를 한 객체에 모으면 메모리 사용량이 증가할 수 있다.
  • 필요하지 않은 데이터까지 불러올 가능성이 있어 성능 최적화에 주의해야 한다.

예제:

namespace ScriptableObjects
{
    [CreateAssetMenu(fileName = "Data", menuName = "ScriptableObjects/GameData", order = 1)]
    public class GameData : ScriptableObject
    {
        public List<CharacterStats> characters;
        public List<ItemData> items;
    }
}

이제 GameData 객체 하나만 불러오면 게임 내 모든 캐릭터 및 아이템 데이터를 쉽게 참조할 수 있다.


9. 리소스 그룹화 및 관리

게임 개발에서는 여러 개의 리소스를 효율적으로 관리해야 한다. 예를 들어, 스프라이트, 사운드, 파티클 효과 등 다양한 리소스를 그룹으로 묶어두면 코드에서 쉽게 참조할 수 있다.

 

장점

  • 리소스를 그룹화하여 접근이 쉬워지고, 관리가 편리해진다.
  • 필요할 때 한 번에 로드할 수 있어 성능 최적화가 가능하다.
  • 스크립트에서 하드코딩 없이 직관적으로 리소스를 할당할 수 있다.

단점

  • 한 번에 많은 리소스를 로드하면 메모리 사용량이 증가할 수 있다.
  • 리소스를 정리하지 않으면 사용하지 않는 데이터가 불필요하게 메모리를 차지할 수 있다.

예제:

namespace ScriptableObjects
{
    [CreateAssetMenu(fileName = "Data", menuName = "ScriptableObjects/ResourceGroup", order = 1)]
    public class ResourceGroup : ScriptableObject
    {
        public List<Sprite> characterSprites;
        public List<AudioClip> soundEffects;
    }
}

이제 ResourceGroup을 활용하면 캐릭터 스프라이트와 사운드 효과를 쉽게 관리할 수 있다. 이를 GameInstance에서 참조하면 프로젝트 전체에서 일관된 리소스 관리를 할 수 있다.

이러한 방식으로 리소스를 그룹화하면 코드의 유지보수가 쉬워지고, 프로젝트 구조가 더욱 체계적으로 정리된다.


10. Update 함수의 성능 최적화 및 관리 방안

10000번의 Update() 호출

 

10000번의 Update() 호출

void Update() { transform.Translate(0, 0, Time.deltaTime); }하지만 숙련된 개발자는 위의 코드에서 몇몇 의문이 들 수 있습니다. 이 함수는 도대체 언제 호출되지? 만약, 여러개의 스크립트가 있고 그 스크립

unity.com

 

Unity에서 Update 함수는 각 프레임마다 호출되며, 게임 로직, 사용자 입력 처리, 타이머, 상태 관리 등에 사용된다. 하지만 많은 수의 Update 함수가 존재할 경우 성능 저하가 발생할 수 있다. 이는 Unity가 모든 MonoBehaviour 스크립트를 순회하며 Update 함수를 호출하기 때문이다.

 

안쓰는 Unity 메시지 함수를 제거해야 하는 이유

Unity의 MonoBehaviour는 여러 메시지 함수를 제공하는데, Update, FixedUpdate, LateUpdate, OnEnable, OnDisable 등 다양한 함수가 포함된다. 그러나 Update 함수가 존재하기만 해도 Unity는 이를 검사해야 하므로, 필요하지 않은 Update 함수가 많을수록 성능 저하가 발생할 가능성이 커진다.

  • Unity는 모든 MonoBehaviour 인스턴스를 순회하며 Update를 호출할 수 있는지 검사한다.
  • 비어 있는 Update 함수도 호출 대상이 되며, 불필요한 오버헤드를 발생시킨다.
  • 프로젝트 내 MonoBehaviour 스크립트가 많아질수록 CPU 사용량이 증가한다.

따라서, 사용하지 않는 Update, FixedUpdate, LateUpdate 등의 메시지 함수는 반드시 제거해야 한다.

 

대안 및 관리법

1. 이벤트 기반 시스템을 활용

 

Update가 필요한 경우만 이벤트를 발생시키는 방식으로 불필요한 호출을 최소화한다.

예를 들어, 상태 변화가 있을 때만 특정 함수를 호출하도록 한다.

 

2. 매니저 클래스를 통한 중앙 집중식 관리

 

개별 객체의 Update 호출을 통합하여 관리하면 불필요한 함수 호출을 줄일 수 있다.

 

3. FixedUpdate와 LateUpdate의 적절한 활용

 

물리 연산이 필요한 경우 FixedUpdate를 사용하여 일정한 간격으로 실행되도록 한다.

렌더링 후 처리해야 할 작업(예: 카메라 위치 보정)은 LateUpdate를 활용하여 불필요한 Update 호출을 방지한다.

 

4. 코루틴(Coroutine) 활용

 

Update 대신 필요할 때만 실행하는 코루틴을 사용하여 불필요한 함수 호출을 줄일 수 있다.


11. 오브젝트 풀링(Object Pooling)을 통한 성능 최적화

오브젝트 풀링은 자주 생성 및 파괴되는 객체를 미리 생성하여 재사용하는 방식이다. 이는 GC(가비지 컬렉션) 부담을 줄이고, 성능을 최적화하는데 효과적이다.

 

왜 필요한가?

  • Instantiate와 Destroy는 높은 성능 비용을 유발하므로, 반복적으로 생성/삭제되는 오브젝트(예: 총알, 이펙트)를 풀링하면 성능이 크게 향상된다.
  • GC 부담 감소: 새로 생성되는 객체가 많으면 GC가 자주 호출되어 프레임 드랍이 발생할 수 있다.

대안 방법

  • 필요할 때마다 Instantiate를 호출하는 방식 대신, 미리 생성된 객체를 활성화/비활성화하는 방식으로 최적화한다.
  • 오브젝트가 필요하지 않을 때는 SetActive(false)로 비활성화하고, 다시 사용할 때 SetActive(true)로 활성화한다.

예제

public class ObjectPool<T> where T : MonoBehaviour
{
    private Queue<T> pool = new Queue<T>();
    private T prefab;
    private Transform parent;

    public ObjectPool(T prefab, int initialSize, Transform parent = null)
    {
        this.prefab = prefab;
        this.parent = parent;

        for (int i = 0; i < initialSize; i++)
        {
            var obj = GameObject.Instantiate(prefab, parent);
            obj.gameObject.SetActive(false);
            pool.Enqueue(obj);
        }
    }

    public T Get()
    {
        if (pool.Count > 0)
        {
            var obj = pool.Dequeue();
            obj.gameObject.SetActive(true);
            return obj;
        }

        var newObj = GameObject.Instantiate(prefab, parent);
        return newObj;
    }

    public void Return(T obj)
    {
        obj.gameObject.SetActive(false);
        pool.Enqueue(obj);
    }
}

이러한 방식으로 풀링을 활용하면 GC 부담을 줄이고, 오브젝트 생성 비용을 최적화할 수 있다.


12. 씬(Scenes) 관리 및 로딩 최적화

Unity에서는 여러 개의 씬을 사용하여 게임을 구성할 수 있으며, 씬을 효율적으로 관리하는 것이 성능과 유지보수에 중요한 요소가 된다.

 

씬 관리 전략

  1. 싱글 씬 vs 멀티 씬 구조
    • 싱글 씬: 하나의 씬에서 모든 기능을 처리하지만, 규모가 커질수록 유지보수가 어려워진다.
    • 멀티 씬: UI, 배경, 게임플레이 등을 나누어 관리하면 효율적이다. (예: MainMenu.unity, Game.unity, UI.unity)
  2. 씬 전환 방식
    • SceneManager.LoadScene()를 사용하면 기존 씬이 언로드되고 새로운 씬이 로드된다.
    • SceneManager.LoadSceneAsync()를 사용하면 비동기적으로 씬을 로드하여 프레임 드랍을 방지할 수 있다.
    • Additive 씬 로딩을 활용하면 여러 씬을 동시에 유지할 수 있다. (예: UI 씬을 별도로 유지하여 매번 새로 로드하지 않도록 함)

예제 코드

IEnumerator LoadSceneAsync(string sceneName)
{
    AsyncOperation asyncLoad = SceneManager.LoadSceneAsync(sceneName);
    while (!asyncLoad.isDone)
    {
        yield return null;
    }
}

비동기 로딩을 사용하면 로딩 중에도 게임이 끊기지 않고 부드럽게 진행될 수 있다.


13. UI 최적화 및 효율적인 이벤트 처리

설명

Unity의 UI 시스템(Canvas, RectTransform, UI 이벤트 시스템)은 잘못 설계하면 불필요한 재렌더링과 성능 저하를 초래할 수 있다.

 

UI 최적화 전략

  1. Canvas의 갱신을 최소화
    • Canvas가 업데이트될 때마다 전체 UI가 다시 렌더링되므로, 빈번한 UI 갱신을 줄이는 것이 중요하다.
    • 변경이 잦은 UI는 별도의 Canvas로 분리하여 불필요한 업데이트를 방지한다.
  2. 레이아웃 재계산 방지
    • ContentSizeFitter, Layout Group 등을 남용하면 매 프레임마다 레이아웃을 재계산할 수 있다.
    • 레이아웃 변경이 필요할 때만 수동으로 LayoutRebuilder.ForceRebuildLayoutImmediate()를 호출하도록 한다.
  3. 오버레이 Canvas 지양
    • Screen Space - Overlay 모드는 UI가 변경될 때마다 전체 화면을 다시 렌더링해야 하므로, 성능이 저하될 수 있다.
    • 가능하면 Screen Space - Camera 또는 Screen Space - World 모드를 사용하여 최적화한다.

UI 이벤트 처리 최적화

  • EventTrigger를 사용하기보다는 UnityEvent나 AddListener()를 활용하는 것이 성능 면에서 유리하다.
  • UI 버튼을 람다 표현식 없이 직접 할당하는 것이 성능적으로 더 효율적이다.

예제

public class UIButtonHandler : MonoBehaviour
{
    public Button myButton;

    private void Start()
    {
        myButton.onClick.AddListener(OnButtonClick);
    }

    private void OnButtonClick()
    {
        Debug.Log("버튼이 클릭됨!");
    }
}

이러한 방식으로 UI 최적화를 하면 프레임 드랍을 줄이고 렌더링 성능을 높일 수 있다.


14. 데이터 저장 및 불러오기 방식 (PlayerPrefs, JSON, ScriptableObject)

설명

게임 데이터 저장 방식에는 여러 가지가 있으며, 각 방식의 장단점이 다르므로 적절하게 선택해야 한다.

 

1. PlayerPrefs (간단한 설정 저장)

  • 장점: 간단한 값 저장 (설정 값, 최고 점수 등)
  • 단점: 보안 취약, 대용량 데이터 저장에 부적합
PlayerPrefs.SetInt(\\\\"HighScore\\\\", 100);
int score = PlayerPrefs.GetInt(\\\\"HighScore\\\\", 0);

 

2. JSON 파일 저장 (플레이어 데이터, 설정 데이터)

  • 장점: 직렬화가 가능하여 복잡한 데이터 구조 저장 가능
  • 단점: 직접 파일 저장/불러오기 구현 필요
string json = JsonUtility.ToJson(playerData);
File.WriteAllText(Application.persistentDataPath + \\\\"/playerData.json\\\\", json);

 

3. ScriptableObject 활용 (에디터 수정용 데이터)

  • 장점: 런타임에서 수정되지 않는 데이터 관리 (아이템 목록, 스테이지 정보 등)
  • 단점: 런타임에서 수정한 데이터를 저장할 수 없음
[CreateAssetMenu(fileName = \\\\"Data\\\\", menuName = \\\\"Game/StageData\\\\")]
public class StageData : ScriptableObject
{
    public int stageNumber;
    public string stageName;
}

 

데이터 저장 방식은 목적에 맞게 선택하는 것이 중요하며, 보안이 필요한 경우 암호화된 저장 방식을 고려해야 한다.