Computer >> 컴퓨터 >  >> 프로그래밍 >> C#

C# 정적 생성자(Static Constructor)란? 개념, 특징, 활용 예제 총정리

C# 정적 생성자란 무엇인가?

정적 생성자(static constructor)는 클래스의 정적 데이터를 초기화하거나, 프로그램 전체에서 단 한 번만 수행해야 하는 작업을 처리하기 위해 사용됩니다. 정적 생성자는 첫 번째 인스턴스가 생성되기 전 또는 어떤 정적 멤버가 처음 참조되기 전에 런타임에 의해 자동으로 호출됩니다.

정적 생성자는 비관리 코드(unmanaged code)를 위한 래퍼(wrapper) 클래스를 작성할 때 특히 유용합니다. 이 경우 생성자 내부에서 LoadLibrary 메서드를 호출하여 필요한 네이티브 라이브러리를 로드할 수 있습니다. 또한 컴파일 시점의 제약 조건(constraints)만으로는 검증할 수 없는 형식 매개변수에 대한 런타임 검사를 수행하기에도 적합한 위치입니다.

정적 생성자의 주요 특징

  • 정적 생성자는 접근 한정자(access modifier)를 가질 수 없으며, 매개변수도 받지 않습니다.

  • 클래스나 구조체에는 정적 생성자를 하나만 정의할 수 있습니다.

  • 정적 생성자는 상속되거나 오버로드될 수 없습니다.

  • 정적 생성자는 직접 호출할 수 없으며, 공용 언어 런타임(CLR)만이 이를 자동으로 호출합니다.

  • 사용자는 프로그램 내에서 정적 생성자가 실행되는 시점을 제어할 수 없습니다.

  • 첫 번째 인스턴스가 생성되거나 정적 멤버가 참조되기 전에 자동으로 호출되어 클래스를 초기화하며, 항상 인스턴스 생성자보다 먼저 실행됩니다. 이벤트나 대리자(delegate)에 할당된 정적 메서드가 실제로 호출될 때 형식의 정적 생성자가 실행되며, 단순히 할당되는 시점에는 호출되지 않습니다. 정적 생성자가 있는 클래스에 정적 필드 이니셜라이저가 포함되어 있다면, 해당 초기화 구문은 클래스 선언에 작성된 순서대로 정적 생성자 실행 직전에 수행됩니다.

  • 정적 필드를 초기화하기 위한 정적 생성자를 제공하지 않으면, 모든 정적 필드는 C# 형식 기본값 규칙에 따라 기본값으로 초기화됩니다.

  • 정적 생성자에서 예외가 발생하면 런타임은 이를 다시 호출하지 않으며, 프로그램이 실행되는 애플리케이션 도메인의 수명 동안 해당 형식은 초기화되지 않은 상태로 남게 됩니다. 정적 생성자가 형식을 인스턴스화하지 못하거나 내부에서 처리되지 않은 예외가 발생하면 일반적으로 TypeInitializationException이 throw됩니다. 소스 코드에 명시적으로 정의되지 않은 암시적 정적 생성자의 문제를 해결하려면 IL(중간 언어) 코드를 확인해야 할 수도 있습니다.

  • 정적 생성자가 존재하면 BeforeFieldInit 형식 특성이 부여되지 않아 런타임 최적화가 제한될 수 있습니다.

  • static readonly로 선언된 필드는 선언부에서 할당하거나 정적 생성자 내에서만 할당할 수 있습니다. 명시적인 정적 생성자가 꼭 필요하지 않다면, 더 나은 런타임 최적화를 위해 정적 생성자 대신 필드 선언 시점에 정적 필드를 초기화하는 것이 좋습니다.

예제 1: 정적 생성자와 인스턴스 생성자의 호출 순서

using System;
namespace DemoApplication{
    public class Program{
        static void Main(string[] args){
            Car user = new Car();
            Car user1 = new Car();
            Console.ReadLine();
        }
    }
    public class Car{
        static Car(){
            Console.WriteLine("정적 생성자 호출됨");
        }
        public Car(){
            Console.WriteLine("기본 생성자 호출됨");
        }
    }
}

실행 결과

정적 생성자 호출됨
기본 생성자 호출됨
기본 생성자 호출됨

위 예제에서 볼 수 있듯이, 인스턴스를 두 번 생성했음에도 불구하고 정적 생성자는 단 한 번만 호출됩니다. 반면 인스턴스 생성자는 객체가 생성될 때마다 매번 실행됩니다. 이것이 정적 생성자가 '프로그램 수명 동안 딱 한 번' 실행된다는 것을 보여주는 핵심 동작입니다.

예제 2: static readonly 필드 초기화에 활용하기

using System;
using System.Threading;
namespace DemoApplication{
    public class Car{
        protected static readonly DateTime globalStartTime;
        protected int RouteNumber { get; set; }
        static Car(){
            globalStartTime = DateTime.Now;
            Console.WriteLine($"정적 생성자 호출됨. 전역 시작 시간:
            {globalStartTime.ToLongTimeString()}");
        }
        public Car(int routeNum){
            RouteNumber = routeNum;
            Console.WriteLine($"{RouteNumber}번 자동차가 생성되었습니다.");
        }
        public void Drive(){
            TimeSpan elapsedTime = DateTime.Now - globalStartTime;
            Console.WriteLine($"{this.RouteNumber}번 자동차가 전역 시작 시간
            {globalStartTime.ToShortTimeString()}으로부터 {elapsedTime.Milliseconds}밀리초 후
            경로를 시작합니다.");
        }
    }
    class TestCar{
        static void Main(){
            Car car1 = new Car(1);
            Car car2 = new Car(2);
            car1.Drive();
            Thread.Sleep(25);
            car2.Drive();
            Console.ReadLine();
        }
    }
}

실행 결과

정적 생성자 호출됨. 전역 시작 시간:
오전 7:09:06
1번 자동차가 생성되었습니다.
2번 자동차가 생성되었습니다.
1번 자동차가 전역 시작 시간 오전 7:09로부터 25밀리초 후 경로를 시작합니다.
2번 자동차가 전역 시작 시간 오전 7:09로부터 50밀리초 후 경로를 시작합니다.

두 번째 예제는 정적 생성자가 static readonly 필드를 초기화하는 실용적인 방법을 보여줍니다. 프로그램 시작 시각(globalStartTime)은 애플리케이션 전체에서 단 한 번만 기록되어야 하므로 정적 생성자에서 초기화하기에 ideal한 대상입니다. 이후 생성되는 모든 인스턴스는 이 공유 값을 기준으로 경과 시간을 계산할 수 있습니다.

마무리

정적 생성자는 CLR이 호출 시점을 관리한다는 점에서 일반 인스턴스 생성자와 완전히 다르게 동작합니다. 로깅 설정, 라이브러리 로드, 전역 상태 초기화처럼 애플리케이션 수명 동안 한 번만 수행하면 되는 작업이 있다면 정적 생성자가 좋은 선택지가 될 수 있습니다. 다만 예외 발생 시 형식 전체가 사용 불가능해질 수 있고 런타임 최적화에 영향을 줄 수 있으므로, 가능하다면 간단한 초기화는 필드 선언 시점에 처리하고 정적 생성자는 꼭 필요한 경우에만 사용하는 것이 바람직합니다.