[유니티 / C#] static (정적) 한정자

⭐ static 이란?

static은 변수나, 함수, 클래스에 정적 속성을 부여하는 것으로 클래스로부터 객체를 생성하지 않고 변수나 함수를 호출할 수 있도록 해주는 것입니다.

 

앞서 클래스에 대한 글에서도 잠깐 설명을 드린 적이 있었던 static입니다.

막연히 static 정의를 보면 언제, 어떻게 사용해야하는지, 도대체 그래서 static이 뭔데? 라는 생각이 드실겁니다.

 

먼저 정적이라는 개념에 대해서 알아보겠습니다.

프로그래밍에 대해서 공부를 한다면 동적 할당, 동적 타입 등의 말을 자주 들어보셨을 것입니다.

프로그래밍 관점에서 동적(dynamic)이란 런타임, 즉 프로그램 실행 중에 변수의 타입을 결정하는 것을 말합니다.

반대로 정적(static)컴파일 시점에 변수의 타입을 결정짓는 것을 의미합니다.

데이터가 스택과 힙 메모리 중 알맞은 영역에 할당되듯이 static 한정자가 붙은 필드, 클래스, 생성자 등은 데이터 영역에 할당됩니다.

그리고 프로그램 컴파일 시 바로 해당 데이터를 사용 가능한 상태가 된다고 생각하시면 됩니다.

 

 

static 변수

예제를 통해서 더 static에 대해서 더 알아보겠습니다.

static 한정자는 변수, 프로퍼티, 메서드, 생성자, 클래스 등 다양한 곳에서 사용할 수 있습니다.

먼저 클래스 필드에 있는 정적 변수에 대해서 알아보겠습니다.

 

일반 클래스인 Option 클래스 내에서 정적 변수 bmgVolume과, soundVolume을 선언했습니다.
그리고 main 함수의 내용을 보시면 객체를 생성해서 접근하는 방법이 아니라 클래스명을 통해서 바로 접근을 하였습니다.

여기서 static의 첫 번째 특징이 나오는데 정적 한정자가 선언된 멤버의 경우 객체를 통해 접근할 수 없고 바로 클래스명을 통해서 접근해야합니다.
그 이유에 대해서는 바로 밑에서 설명하겠습니다.


또 bgmVolume의 경우 public으로 선언하였기에 외부에서 접근이 가능하지만 soundVolume의 경우 private으로 선언하였기 때문에 당연하게도 외부에서 접근하면 오류가 생깁니다.
internal class Program
{
    static void Main(string[] args)
    {
        Option.bgmVolume = 5;		//접근제한자가 public으로 접근 가능
        Option.soundVolume = 3;		//접근제한자가 private으로 접근 불가능 -> Error
    }
}

public class Option
{
    public static int bgmVolume;      //외부에서 접근 가능
    private static int soundVolume;   //외부에서 접근 불가능

    public void SetVolumeOption()
    {
        //실제 볼륨 값 조절 코드
    }
}​

 

 

 

static 프로퍼티

 

static 한정자는 프로퍼티에도 사용할 수 있습니다.

실제로 싱글턴 패턴을 구현할 때 static 프로퍼티를 사용하여 해당 타입의 인스턴스가 이미 생성이 되어있는지, 생성이 되어있다면 this(나 자신) 객체가 인스턴스인지 등을 판단합니다.

 

internal class Program
{
    static void Main(string[] args)
    {
        GameManager.Gold = 500;
    }
}

public class GameManager
{
    private static int gold;
    public static int Gold
    {
        get { return gold; }
        set { gold = value; }
    }
}​

 

 

 

 

static 생성자

생성자를 정의할 때도 static 한정자를 사용할 수 있습니다.

하지만 여기서 주의해야하는 것은 static은 클래스 당 하나만 존재하는 것이기 때문에 처음 호출하거나 생성되는 순간에만 호출됩니다.

또한 static 생성자는 접근제한자를 함께 쓸 수 없으며, 매개변수를 사용할 수 없고 정적 멤버만 생성자 구문에 사용할 수 있습니다.

 

//일반 클래스 - 정적 생성자 => 객체 생성시 1회 호출
internal class Program
{
    static void Main(string[] args)
    {
        User user = new User();    //생성자 호출됨
        User user2 = new User();   //생성자 호출 안됨
        User user3 = new User();   //생성자 호출 안됨
    }
}

public class User
{
    private const int id = 123;
    private static string name;
    private static int count = 1;

    static User()  //정적 생성자는 최초의 선언이나 호출시에만 1회 호출됨
    {
        name = $"{count}번째 이름";
        Console.WriteLine(name);
        Console.WriteLine(id);
        count++;
    }
}

출력 결과
=> 1번째 이름
   123
   
   
//정적 클래스 - 정적 생성자 => 호출할 경우 생성자 1회 호출
internal class Program
{
    static void Main(string[] args)
    {
        User.Init();
    }
}

public static class User
{
    private const int id = 123;
    private static string name;
    private static int count = 1;

    static User()
    {
        name = $"{count}번째 이름";
        Console.WriteLine(name);
        Console.WriteLine(id);
        count++;
    }

    public static void Init()
    {
        //코드코드
    }
}

 

 

static 메서드

유틸 관련된 메서드를 직접 커스텀할 때 아주 유용한 정적 메서드입니다.

사실 수천, 수만번은 사용했을 Console.WriteLine(), 여러 Math.관련 메서드들 또한 정적 메서드입니다.

 

단, 정적 생성자와 마찬가지로 정적 메서드 내부에서도 정적 멤버와 상수만 사용할 수 있습니다.

만약 정적 메서드 내부에서 일반 변수를 선언한 경우에는 사용할 수 있습니다.

internal class Program
{
    static void Main(string[] args)
    {
        User.DisplayUserInfo();
    }
}

public static class User
{
    private static string name = "한삼식";
    private static int age = 29;

    public static void DisplayUserInfo()
    {
        int a = 5; -> 내부에서 선언 시 사용 가능
        
        Console.WriteLine($"이름: {name}");   //이름: 한삼식
        Console.WriteLine($"나이: {age}");    //나이: 29
        Console.WriteLine(a);                 //5
    }
}

 

static 클래스

클래스 자체도 정적 클래스로 정의할 수 있습니다.

정적 클래스는 정적 멤버와 상수만 선언하고 구현할 수 있습니다.

클래스와 멤버들에 정말 간편히 접근할 수 있지만 그만큼 클래스 간의 결합도가 높아질 수 있기 때문에 정말 필요한 경우에만 사용하여야 합니다.

 

//유니티에서 코루틴 캐싱하는 정적 클래스를 따로 정의
public static class CoroutineManager
{
    //WaitForEndOfFrame과 WaitForFixedUpdate 같은 경우에는 인자 값이 없기 때문에 미리 캐싱만 해둠
    private static readonly WaitForEndOfFrame waitForEndOfFrame = new WaitForEndOfFrame();
    private static readonly WaitForFixedUpdate waitForFixedUpdate = new WaitForFixedUpdate();

    //WaitForSeconds객체를 풀링하여 사용
    private static readonly Dictionary<float, WaitForSeconds> _waitForSeconds = new Dictionary<float, WaitForSeconds>();
    private static readonly Dictionary<float, WaitForSecondsRealtime> _waitForSecondsRealtime = new Dictionary<float, WaitForSecondsRealtime>();

    //인자로 받은 seconds를 _waitForSeconds 딕셔너리의 키 값으로 값이 있나 확인
    //값이 없다면 -> 새로운 WairForSeconds 객체를 생성하여 값을 넣어줌
    //값이 있다면 해당 객체를 리턴
    public static WaitForSeconds waitForSeconds(float seconds)
    {
        if(!_waitForSeconds.TryGetValue(seconds, out var waitforseconds))
        {
            _waitForSeconds.Add(seconds, waitforseconds = new WaitForSeconds(seconds));
        }

        return waitforseconds;
    }

    public static WaitForSecondsRealtime waitForSecondsRealtime(float seconds)
    {
        if(!_waitForSecondsRealtime.TryGetValue(seconds, out var waitForSecondsRealtime))
        {
            _waitForSecondsRealtime.Add(seconds, waitForSecondsRealtime = new WaitForSecondsRealtime(seconds));
        }

        return waitForSecondsRealtime;
    }
}

 

 

 

⭐ static 멤버 특징 정리

위의 예제에서 설명한 각각의 static 멤버들의 특징에 대해서 다시 정리해놓은 단락입니다.

 

 

💡 static 멤버는 항상 클래스명을 통해서 접근할 것!!! 💡

 

💡 static 멤버 특징

● 정적 메서드 내부에는 정적 멤버 또는 상수만 사용할 수 있습니다. 단 메서드 내부에서 직접 선언하는 경우에는 사용이 가능합니다.

 

● 정적 클래스는 정적 멤버와 상수만 가질 수 있습니다.

 

● 정적 생성자는 클래스가 처음 로드될 때 (일반 클래스의 경우 생성될 때, 정적 클래스의 경우 호출될 때) 딱 한 번만 호출됩니다.

 

정적 생성자는 정적 클래스, 정적 메서드와 마찬가지로 정적 멤버와 상수만 사용할 수 있으며, 생성자 내부에서 지역변수 선언 시 사용 가능합니다.

 

● 정적 생성자는 매개변수를 가질 수 없고, 접근제한자를 명시할 수 없습니다. (암묵적으로 private 취급)

 

 

 

⭐ 정적 멤버가 클래스명을 통해서 접근해야하는 이유

정적 멤버는 특정 인스턴스에 속하지 않으며 클래스 단위로 하나만 존재하는 멤버입니다.

또한 프로그램 실행 시 이미 영역을 할당받고 객체를 여러개 생성해도 결국 하나의 메모리만 유지됩니다.

 

정적 멤버는 '어느 객체의 것이다'라는 개념이 없기 때문에 객체를 통한 참조가 불가능한 것 입니다.

 

정적 클래스, 메서드, 생성자는 모두 정적 멤버와 상수로만 이루어져야 하는 이유도 이 때문입니다.

정적 멤버는 해당 클래스의 객체가 생성되기도 전에 영역을 할당받고 호출받을 수 있지만

일반 멤버는 객체가 생성되고 초기화되기 전까지 값을 가질 수 없기 때문에 정적 멤버해서 호출되면 런타임 에러를 일으킬 수 있습니다.

정적 메서드 내부에서 직접 선언한 지역변수는 메서드 호출 시 선언과 초기화를 할 수 있고 메서드가 종료되면 해제되기 때문에 사용할 수 있는 것입니다.