30. 정적 생성자와 타입 초기화 타이밍(beforefieldinit)
난이도 중내 리뷰 · C#
해설 · C#
해설 — 정적 생성자와 타입 초기화 타이밍(beforefieldinit)
난이도: 중상
예측 출력
start
Config.Load
42
Counter..cctor
101
102
--- mutual ---
2
10
핵심 개념
- 타입 초기화자(type initializer,
.cctor): 정적 필드 이니셜라이저와 명시적static생성자는 컴파일 시 하나의.cctor로 합쳐져 CLR이 타입당 한 번 자동 호출한다. beforefieldinit플래그: 타입에 명시적 정적 생성자가 없고 정적 필드 이니셜라이저만 있으면 컴파일러가beforefieldinit를 붙인다. 이 경우 CLR은 "그 타입의 정적 필드에 처음 접근하기 전 어느 시점"에 초기화하면 되며, 정확한 시점은 구현에 맡겨진다(더 일찍 앞당길 수 있음).- 명시적 정적 생성자가 있으면
beforefieldinit이 없어 초기화가 최초 사용 직전으로 정확히 지연된다. 즉 부수효과 타이밍이 결정적이다.
단계별 내부 동작 (출력 근거)
"start".Config.Value접근 → Config 의.cctor실행:Load()가Config.Load출력, Value=42. 이어서42.Counter.Next()최초 호출 → Counter 의.cctor실행(Counter..cctor, _n=100).++_n→101.- 두 번째
Counter.Next()→ cctor 재실행 안 함(타입당 1회) →102. --- mutual ---.Mutual.A접근 → Mutual 초기화 시작..cctor는 필드를 소스 순서로 초기화한다(A 먼저, B 나중).A = ComputeA()=Mutual2.Bv + 1.Mutual2.Bv접근 → Mutual2 초기화 시작. 소스 순서로Bv먼저:Bv = ComputeB()=Mutual.B + 1.Mutual.B를 읽는데 Mutual 은 아직 초기화 중(A 이니셜라이저 실행 도중이며 B 는 아직 미실행). 같은 스레드의 재귀적 초기화이므로 CLR은 다시 돌리지 않고 부분 초기화 상태를 보게 한다 →Mutual.B == 0(기본값). 따라서ComputeB() = 0 + 1 = 1,Mutual2.Bv = 1.
- 이어서
Av = 5. Mutual2 초기화 완료.
- 돌아와서
ComputeA() = Mutual2.Bv(1) + 1 = 2. 즉Mutual.A = 2.
- 다음 필드
B = 10. Mutual 초기화 완료(A=2, B=10). - 출력
2.
Mutual.B→ 이미 초기화됨 →10.
흔한 오해·함정
- "정적 생성자는 프로그램 시작 시 실행된다" — 틀림. 명시적 cctor는 최초 사용 직전 지연 실행되고, beforefieldinit 타입은 더 일찍 앞당겨질 수 있어 부수효과 타이밍을 신뢰하면 안 된다.
- 빈 정적 생성자를 굳이 넣으면 beforefieldinit 이 사라져 지연 초기화가 강제되고, 정적 필드 접근마다 초기화 여부 체크 비용이 붙어 핫패스에서 성능에 불리할 수 있다.
- 순환 정적 초기화는 위 6번처럼 부분 초기값(0/null)을 조용히 관측하게 만든다. 값이 접근 순서에 의존해 재현이 어려운 버그가 된다 — 정적 초기화 사이의 상호 의존을 피하라.
- 타입 초기화 중 예외가 나면
TypeInitializationException으로 감싸지고, 그 타입은 해당 프로세스에서 사실상 사용 불가가 된다.
면접 포인트
- beforefieldinit 이 무엇이고 왜 타이밍/성능에 영향을 주는가. 빈 static ctor 의 부작용.
- 타입 초기화가 "정확히 언제" 트리거되는가(정적 멤버 접근·인스턴스 생성·정적 메서드 호출 직전).
- 순환 정적 초기화가 부분 초기값을 노출하는 메커니즘과, 그 값이 접근 순서에 의존하는 이유.