C# 중급 문법 (6) : Static에 대해서

안녕하세요! 지난 포스팅에서는 Struct라는 생소하고 Static과 헷갈리기 쉬운 구조체에 대해 알아봤습니다! 일반적인 객체 지향 프로그래밍에서는 static이 다소 특별하게 느껴질 수 있지만, 게임 엔진인 **유니티(Unity)**에서는 이 static 키워드가 정말 유용하고 흔하게 사용됩니다.

이번 포스팅에서는 static의 개념을 조금 더 쉽고 직관적으로 설명하고, 특히 유니티 프로젝트에서 static 멤버를 어떻게 활용할 수 있는지에 초점을 맞춰 이야기해볼게요!


1. Static, 쉽게 이해하기: '모두의 것' vs '나만의 것' 

우리가 평소에 만드는 C# 클래스(Monobehaviour 스크립트도 포함!)들은 대부분 **인스턴스(Instance)**를 만들어서 사용합니다. 인스턴스는 '나만의 것'을 가지는 개념이에요.

  • 인스턴스 멤버 (나만의 것):
    • Player 스크립트를 여러 개 만들면, 각 Player는 자기만의 health 값, position 값을 가집니다.
    • player1.health = 100; player2.health = 50; 처럼 각 인스턴스마다 다른 값을 가질 수 있죠.
    • 사용하려면 반드시 new Player()처럼 객체를 만들거나, 유니티에서는 씬에 오브젝트를 만들고 거기에 스크립트를 컴포넌트로 붙여야 합니다.
  • 정적(Static) 멤버 (모두의 것):
    • static이 붙은 멤버는 객체를 만들지 않아도 클래스 자체에 고정되어 있습니다.
    • 모든 Player 객체가 아니라, 모든 게임 오브젝트가 아니라, 그냥 Player 클래스 자체가 이 값을 가지고, 이 기능을 수행합니다.
    • 따라서 어떤 객체가 접근하든 모두 동일한 값을 보거나 동일한 기능을 사용하게 됩니다.
    • 유니티 씬에 스크립트를 붙이지 않아도, Player.TotalPlayersCreated = 5; 처럼 클래스 이름으로 바로 접근할 수 있습니다.

가장 좋은 비유는 스마트폰입니다.

  • 나의 스마트폰은 인스턴스입니다. (각자 다른 모델, 다른 앱, 다른 사진)
  • 하지만 스마트폰의 '배터리 잔량'을 %로 표시하는 기능은 모든 스마트폰에 공통적으로 필요한 '기능'입니다. 이 기능을 구현하는 '배터리 관리' 클래스가 있다면, 이 클래스의 특정 기능은 static으로 만들어질 수 있습니다. 또는, 모든 스마트폰이 공유하는 '통신 규약' 같은 것은 static으로 정의될 수 있겠죠.

2. 유니티에서 Static 활용하기: 흔하고 유용한 패턴들 💡

유니티는 게임 오브젝트와 컴포넌트 기반의 아키텍처를 가지고 있지만, C# 코드 내에서 static 멤버는 여전히 강력한 도구로 활용됩니다.


2.1. 게임 전반에 걸친 '단 하나의 정보' 공유

게임에서 **플레이어의 점수, 현재 레벨, 게임 상태(일시 정지 여부)**와 같이 모든 스크립트에서 접근해야 하고, 모든 플레이어가 공유하는 데이터가 있을 수 있습니다. 이런 데이터는 static 필드나 속성으로 관리하기 좋습니다.

 

예시: 게임 점수 관리

// GameManager.cs
using UnityEngine;

public class GameManager : MonoBehaviour
{
    // static int: 모든 곳에서 이 게임의 총 점수에 접근할 수 있습니다.
    public static int Score = 0;

    // static bool: 게임이 일시 정지 상태인지 전역적으로 알 수 있습니다.
    public static bool IsPaused = false;

    void Start()
    {
        // 게임 시작 시 점수 초기화 (일반적으로 GameManager는 하나만 존재)
        Score = 0;
        IsPaused = false;
        Debug.Log("게임 시작! 현재 점수: " + Score);
    }

    // static 메서드: 점수를 추가하는 전역적인 방법
    public static void AddScore(int amount)
    {
        Score += amount;
        Debug.Log("점수 획득! 현재 점수: " + Score);
    }

    // static 메서드: 게임 일시 정지/재개 토글
    public static void TogglePause()
    {
        IsPaused = !IsPaused;
        Time.timeScale = IsPaused ? 0f : 1f; // 시간 흐름 제어
        Debug.Log("게임 일시 정지 상태: " + IsPaused);
    }
}

활용 예시 (다른 스크립트에서):

// Enemy.cs (다른 스크립트)
using UnityEngine;

public class Enemy : MonoBehaviour
{
    public int pointsOnDefeat = 10;

    void OnMouseDown() // 예시: 마우스 클릭 시 적 처치
    {
        if (!GameManager.IsPaused) // 게임이 일시 정지 상태가 아닐 때만 작동
        {
            GameManager.AddScore(pointsOnDefeat); // GameManager의 static 메서드 호출
            Destroy(gameObject); // 적 제거
        }
    }
}

// PlayerController.cs (다른 스크립트)
using UnityEngine;

public class PlayerController : MonoBehaviour
{
    void Update()
    {
        if (Input.GetKeyDown(KeyCode.P))
        {
            GameManager.TogglePause(); // GameManager의 static 메서드 호출
        }
    }
}

이제 어느 스크립트에서든 GameManager.Score, GameManager.AddScore(int) 등으로 게임의 핵심 데이터와 기능을 쉽게 사용할 수 있습니다. 유니티 씬에서 GameManager 오브젝트를 찾아서 GetComponent 할 필요가 없습니다!


2.2. 유틸리티/도우미 함수 (Helper Functions)

특정 객체에 종속되지 않고, 범용적으로 사용될 수 있는 계산이나 변환 함수들은 static 메서드로 만드는 것이 일반적입니다.

예시: 수학 계산 도우미

C#
 
// MathHelpers.cs
using UnityEngine;

public static class MathHelpers // static class로 만들면 모든 멤버가 static이어야 함
{
    // 두 점 사이의 2D 거리 계산 (Vector3.Distance도 있지만 예시)
    public static float GetDistance2D(Vector3 p1, Vector3 p2)
    {
        Vector2 v1 = new Vector2(p1.x, p1.y);
        Vector2 v2 = new Vector2(p2.x, p2.y);
        return Vector2.Distance(v1, v2);
    }

    // 각도를 라디안에서 도로 변환
    public static float RadiansToDegrees(float radians)
    {
        return radians * Mathf.Rad2Deg;
    }
}

이제 어떤 스크립트에서든 float dist = MathHelpers.GetDistance2D(transform.position, otherObject.transform.position); 처럼 바로 사용할 수 있습니다. Mathf 클래스도 유니티에서 제공하는 대표적인 정적 클래스입니다.


2.3. 싱글톤 패턴 (Singleton Pattern) 구현의 기반

싱글톤 패턴은 어떤 클래스의 인스턴스가 프로그램 전체에서 단 하나만 존재하도록 강제하는 디자인 패턴입니다. 유니티에서 **GameManager**나 **AudioManager**처럼 게임 전체를 관리하는 객체에 자주 사용됩니다. static 멤버는 싱글톤을 구현하는 핵심 요소입니다.

// AudioManager.cs (간단한 싱글톤 예시)
using UnityEngine;

public class AudioManager : MonoBehaviour
{
    // 인스턴스를 저장할 static 변수 (어디서든 접근 가능)
    public static AudioManager Instance { get; private set; }

    public AudioClip backgroundMusic;
    public AudioClip soundEffect;

    private AudioSource _audioSource;

    void Awake() // Start()보다 먼저 호출되어 초기화에 적합
    {
        // 1. 이미 인스턴스가 있는지 확인
        if (Instance != null && Instance != this)
        {
            Destroy(gameObject); // 중복된 AudioManager 제거
            return;
        }

        // 2. 현재 객체를 유일한 인스턴스로 설정
        Instance = this;
        DontDestroyOnLoad(gameObject); // 씬 전환 시 파괴되지 않도록

        _audioSource = GetComponent<AudioSource>();
        if (_audioSource == null)
        {
            _audioSource = gameObject.AddComponent<AudioSource>();
        }
        // 배경 음악 재생
        _audioSource.clip = backgroundMusic;
        _audioSource.loop = true;
        _audioSource.Play();
    }

    public void PlaySoundEffect()
    {
        _audioSource.PlayOneShot(soundEffect);
    }
}

활용 예시 (다른 스크립트에서):

// ButtonScript.cs (다른 스크립트)
using UnityEngine;
using UnityEngine.UI; // UI 사용 시

public class ButtonScript : MonoBehaviour
{
    public Button myButton;

    void Start()
    {
        myButton.onClick.AddListener(OnClickButton);
    }

    void OnClickButton()
    {
        // AudioManager의 인스턴스를 찾을 필요 없이 static Instance를 통해 바로 접근
        if (AudioManager.Instance != null)
        {
            AudioManager.Instance.PlaySoundEffect();
        }
    }
}

이런 식으로 AudioManager.Instance를 통해 언제든 유일한 AudioManager 객체에 접근하여 음악을 제어할 수 있습니다.


3. Static 사용 시 주의할 점 

static은 편리하지만, 잘못 사용하면 코드에 문제가 생길 수 있습니다.

  • 컴포넌트의 생명주기 문제: MonoBehaviour 스크립트의 static 멤버를 사용할 때는 주의해야 합니다. 유니티 씬에서 오브젝트가 파괴되면 해당 인스턴스의 데이터는 사라지지만, static 데이터는 클래스가 메모리에서 해제되기 전까지 남아 있습니다. 씬이 바뀌거나 게임이 재시작될 때 static 값이 초기화되지 않아 예상치 못한 버그가 발생할 수 있습니다. (위 싱글톤 예시의 Awake에서 중복 검사와 DontDestroyOnLoad를 통해 이를 관리합니다.)
  • 객체 지향 원칙과의 충돌: static을 너무 많이 사용하면 객체 지향의 가장 큰 장점인 **유연성(Polymorphism)**과 테스트 용이성이 저해될 수 있습니다. 모든 것을 static으로 만들면 코드를 재사용하거나 확장하기 어려워집니다.
  • 다중 스레드 문제: 멀티스레딩 환경(유니티에서는 일반적으로 백그라운드 작업 시)에서 static 변수를 여러 스레드가 동시에 접근하면 동기화 문제가 발생할 수 있습니다. (게임 로직은 주로 메인 스레드에서 돌아가므로 자주 발생하는 문제는 아니지만, 알아두는 것이 좋습니다.)

마무리하며: 유니티에서 Static, 현명하게 사용하기! 

static 키워드는 유니티에서 전역적으로 접근 가능한 데이터나 기능을 구현할 때 매우 유용합니다. 특히 게임 매니저, 오디오 매니저와 같은 게임 전체를 아우르는 단 하나의 컨트롤러를 만들 때 싱글톤 패턴과 함께 빛을 발합니다.

하지만 static은 객체 지향의 인스턴스 기반 사고와는 다른 방식이므로, 언제 사용할지 신중하게 결정해야 합니다.

  • 게임 전반의 공유 데이터나 유틸리티 기능처럼 '모두의 것'이 필요할 때 static을 사용하세요.
  • 각각의 개별적인 '나만의 것'을 가져야 하는 게임 오브젝트나 컴포넌트에는 static을 사용하지 않는 것이 좋습니다.

이제 static을 이해하고 유니티 프로젝트에서 효율적으로 활용하여 더 깔끔하고 강력한 게임 코드를 작성해보세요!

static에 대해 더 궁금한 점이 있으시거나, 유니티 특정 상황에서의 static 활용법에 대해 알고 싶으신가요? 언제든지 질문해주세요!