Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일

C# - 상호 참조하는 경우의 정적 생성자 동작 방식

정적 생성자가 싱글톤(Singleton) 객체를 생성하는 데 가장 최적의 장소가 된 배경에는 CLR의 노력이 있습니다. 왜냐하면 정적 생성자에 대한 실행을 CLR 측에서 단 하나의 스레드만 접근하도록 보장해 주기 때문입니다.

이 때, 재미있는 현상이 하나 나오는데요. 정적 생성자가 서로 상호 참조했을 때 어떤 현상이 발생할까입니다. 일단, 예를 들기 위해 다음과 같은 코드를 만들어,

using System;

class Program
{
    static void Main(string[] args)
    {
        BClass.Initialize();
    }
}

class AClass
{
    public static int Flag = 5;
    static AClass()
    {
        Flag = 10;
    }

    internal static void Initialize() { }
}

class BClass
{
    static BClass()
    {
        AClass.Initialize();
        Console.WriteLine($"{nameof(AClass)}.Flag == " + AClass.Flag); // 출력 값: AClass.Flag == 10 
    }

    internal static void Initialize() { }
}

실행해 보면, AClass의 정적 생성자 실행으로 인해 화면에는 10의 값이 출력됩니다. 그런데, 이것이 언제나 그런 것은 아닙니다. 바로 정적 생성자에서 상호 참조되는 경우인데, 예제를 다음과 같이 바꿔보면 알 수 있습니다.

using System;

class Program
{
    static void Main(string[] args)
    {
        AClass.Initialize();
    }
}

class AClass
{
    public static int Flag = 5;
    static AClass()
    {
        Console.WriteLine($"{nameof(AClass)}.cctor - start");
        BClass.Initialize();
        Flag = 10;
        Console.WriteLine($"{nameof(AClass)}.cctor - end");
    }

    internal static void Initialize() { }
}

class BClass
{
    static BClass()
    {
        Console.WriteLine($"{nameof(BClass)}.cctor - start");
        AClass.Initialize();
        Console.WriteLine($"{nameof(BClass)}.cctor - end: {nameof(AClass)}.Flag == " + AClass.Flag);
    }

    internal static void Initialize() { }
}

보시는 바와 같이, BClass의 정적 생성자는 (로직상) 코드가 바뀌지 않았지만, 이 코드를 실행해 보면 다음과 같은 결과를 볼 수 있습니다.

AClass.cctor - start
BClass.cctor - start
BClass.cctor - end: AClass.Flag == 5
AClass.cctor - end

즉, CLR은 정적 생성자에서 상호 참조로 인한 cctor가 실행되는 경우 dead-lock 상황을 유발하지 않는 대신 중첩시켜 실행을 해버립니다. 물론, 그 부작용으로 개발자가 당연하다고 기대했던 AClass.Flag의 값이 10에서 5로 바뀌어 버렸습니다. 간단히 말해, 전에는 잘 동작했던 코드가 어느 순간 오동작을 하는 것처럼 바뀔 수 있다는 것입니다.

"제프리 리처의 CLR via C# - 4판"에 보면 바로 이와 같은 상황때문에 정적 생성자에서는 가능한 다른 정적 생성자를 호출하는 상황에 주의를 기울이라고 나옵니다.

역시... 제프리 리처의 책은 믿고 보게 됩니다. ^^

(첨부한 파일은 위의 예제 코드를 담고 있습니다.)




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]







[최초 등록일: ]
[최종 수정일: 9/2/2015]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 



2015-09-03 01시45분
[이성환] 어떻게 보면 당연한 결과인 것 처럼 느끼지는데요.
아래 예제에서는 AClass의 정적 생성자 호출이 끝나기 전에 Console.WriteLine($"{nameof(BClass)}.cctor - end: {nameof(AClass)}.Flag == " + AClass.Flag); 를 호출했으니
- 아직 AClass.Flag = 10 구문 호출 전이기 때문에 - 당연히 AClass.Flag == 5 가 찍히는 것일테고
AClass의 정적 생성자 호출이 완료된 시점에서 다시 찍어보면 AClass.Flag == 10 으로 나옵니다.

글의 요지는 정적 생성자 호출이 완료되기 전에 다른 정적 생성자를 호출한 값을 할당하고 사용하는 것에 주의를 기울이라는 의미인가요?

"CLR은 정적 생성자에서 상호 참조로 인한 cctor가 실행되는 경우 dead-lock 상황을 유발하지 않는 대신 중첩시켜 실행을 해버립니다. " 라는 문장이 살짝 이해가 되지 않아 질문 남겨봅니다...
[guest]
2015-09-03 02시18분
가령 BClass를 B 개발자가 만들고 있다고 생각해 보겠습니다. BClass는 당연히 AClass의 정적 멤버를 호출하면서 정적 생성자의 실행이 완료되었다고 생각할 것이고, 당연히 AClass.Flag의 값은 10이어야 합니다.

그런데, 어느날 AClass를 만든 A 개발자가 필요에 의해서 BClass의 정적 멤버를 AClass의 정적 생성자에서 호출하는 코드를 넣어버린 것입니다. 그 순간 B 개발자도 모르게 BClass의 정적 생성자가 오동작을 하게 되는 문제가 발생하는 것입니다.

사실 명시적으로 호출되는 경우라면 이런 것이 문제가 되지 않을 수 있지만, 정적 생성자라는 것이 암시적으로 JIT 컴파일 시에 호출 코드가 삽입되기 때문에 개발자가 의도치 않게 문제 상황을 접할 수 있음을 설명한 것입니다.

만약 저런 부작용을 없애려면 대상 정적 생성자가 완전히 실행 완료될 때까지 대기해야 하는데, 그렇게 되면 AClass의 cctor에서는 BClass의 cctor가 실행완료될 때까지 기다려야 하고, BClass의 cctor 또한 AClass의 cctor가 완료될 때까지 대기하게 되면 dead-lock이 발생하게 됩니다.
정성태
2015-09-03 07시47분
[이성환] 네 설명해주신 부분 이해했습니다. "제프리 리처의 CLR via C# - 4판" 에서는 아주 짦막하게 언급하고 지나간 부분이었는데 이렇게 설명해주시니 더 이해가 잘 되네요. =ㅂ=b

그런데 궁금한 건 "제프리 리처의 CLR via C# - 4판" 에서는 정적 생성자가 완료될 때까지 lock을 획득하는 방식으로 처리된다고 되어 있는데

그럼 본문에서 설명하신 " dead-lock 상황을 유발하지 않는 대신 중첩시켜 실행을 해버린다"의 의미는 완료되지 않은 정적 생성자를 다시 수행하지 않고 곧바로 현재 정적 멤버에 접근하다는 얘기인가요?
(이 예제에서는 같은 스레드라 상관없어 보이긴 하지만서도...)

테스트를 돌려보면 그렇게 동작하는 거 같은데 정확한 의미인지 궁금해서 다시 질문드립니다.
[guest]
2015-09-03 07시53분
@이성환, 네 사실 ^^ 저도 제프리의 (lock을 획득한다는) 그 부분이 정말인지 확인하려고 위의 테스트를 했던 것입니다. (첨부 파일을 다운로드해보시면 다중 스레드에서도 테스트 한 예제가 있습니다.) 하지만 실제 테스트를 해보니, lock을 건다고 해서 dead-lock을 유발시키는 식으로 처리하진 않고 대신 '한번만 실행'시킨다는 목적에 충실하게 하는 듯합니다.
정성태

... 106  107  108  [109]  110  111  112  113  114  115  116  117  118  119  120  ...
NoWriterDateCnt.TitleFile(s)
11199정성태5/16/201721186.NET Framework: 657. CultureInfo.GetCultures가 반환하는 값
11198정성태5/10/201722678.NET Framework: 656. Windows Forms의 오류(Exception) 처리 방법에 대한 차이점 설명
11197정성태5/8/201719600개발 환경 구성: 315. VHD 파일의 최소 크기파일 다운로드1
11196정성태5/4/201720803오류 유형: 384. Msvm_ImageManagementService WMI 객체를 사용할 때 오류 상황 정리 [1]
11195정성태5/3/201721102.NET Framework: 655. .NET Framework 4.7 릴리스
11194정성태5/3/201723271오류 유형: 383. net use 명령어로 네트워크 드라이브 연결 시 "System error 67 has occurred." 오류 발생
11193정성태5/3/201721592Windows: 141. 설치된 Windows로부터 설치 이미지를 만드는 방법
11192정성태5/2/201722156Windows: 140. unattended.xml/autounattend.xml 파일을 마련하는 방법
11191정성태5/2/201722877Windows: 139. Dell Venue 8 Pro 태블릿에 USB를 이용한 윈도우 운영체제 설치 방법
11190정성태5/2/201728223Windows: 138. Windows 운영체제의 ISO 설치 파일에 미리 Device driver를 준비하는 방법
11189정성태5/2/201720238Windows: 137. Windows 7 USB/DVD DOWNLOAD TOOL로 98%에서 실패하는 경우
11188정성태4/27/201722704VC++: 118. Win32 HANDLE 자료형의 이모저모 [1]
11187정성태4/26/201723269개발 환경 구성: 314. C# - PowerPoint 확장 Add-in 만드는 방법 [1]파일 다운로드1
11186정성태4/24/201721051VS.NET IDE: 117. Visual Studio 확장(VSIX)을 이용해 사용자 매크로를 추가하는 방법 [1]파일 다운로드1
11185정성태4/22/201718978VS.NET IDE: 116. Visual Studio 확장(VSIX)을 이용해 사용자 메뉴 추가하는 방법 (2) - 동적 메뉴 구성파일 다운로드1
11184정성태4/21/201720622VS.NET IDE: 115. Visual Studio 확장(VSIX)을 이용해 사용자 메뉴 추가하는 방법파일 다운로드1
11183정성태4/19/201719474.NET Framework: 654. UWP 앱에서 FolderPicker 사용 시 유의 사항파일 다운로드1
11182정성태4/19/201723483개발 환경 구성: 313. Nuget Facebook 라이브러리를 이용해 ASP.NET 웹 폼과 로그인 연동하는 방법
11181정성태4/18/201720394개발 환경 구성: 312. Azure Web Role의 AppPool 실행 권한을 Local System으로 바꾸는 방법
11180정성태4/16/201723422Java: 18. Java의 Memory Mapped File 자원 반환이 안 되는 문제
11179정성태4/13/201716563기타: 64. SVG Converter 스토어 앱 개인정보 보호 정책 안내
11178정성태4/10/201718770개발 환경 구성: 311. COM+ 관리자의 DCOM 구성에 나오는 기준
11177정성태4/7/201719184.NET Framework: 653. C# 7 새로운 문법(1) - 더욱 편리해진 Out 변수 사용파일 다운로드1
11176정성태4/5/201716228VC++: 117. Visual Studio - ATL COM 개체를 단위 테스트 하는 방법
11175정성태4/5/201725919.NET Framework: 652. C# 개발자를 위한 C++ COM 객체의 기본 구현 방식 설명파일 다운로드1
11174정성태4/3/201719448VC++: 116. Visual Studio 단위 테스트 - Failed to set up the execution context to run the test
... 106  107  108  [109]  110  111  112  113  114  115  116  117  118  119  120  ...