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을 유발시키는 식으로 처리하진 않고 대신 '한번만 실행'시킨다는 목적에 충실하게 하는 듯합니다.
정성태

... 31  32  33  34  35  [36]  37  38  39  40  41  42  43  44  45  ...
NoWriterDateCnt.TitleFile(s)
12744정성태7/30/20217594개발 환경 구성: 586. Azure Active Directory에 연결된 App 목록을 확인하는 방법?
12743정성태7/30/20218307.NET Framework: 1083. Azure Active Directory - 외부 Token Cache 저장소를 사용하는 방법파일 다운로드1
12742정성태7/30/20217507개발 환경 구성: 585. Azure AD 인증을 위한 사용자 인증 유형
12741정성태7/29/20218747.NET Framework: 1082. Azure Active Directory - Microsoft Graph API 호출 방법파일 다운로드1
12740정성태7/29/20217402오류 유형: 747. SharePoint - InvalidOperationException 0x80131509
12739정성태7/28/20217371오류 유형: 746. Azure Active Directory - IDW10106: The 'ClientId' option must be provided.
12738정성태7/28/20217986오류 유형: 745. Azure Active Directory - Client credential flows must have a scope value with /.default suffixed to the resource identifier (application ID URI).
12737정성태7/28/20216933오류 유형: 744. Azure Active Directory - The resource principal named api://...[client_id]... was not found in the tenant
12736정성태7/28/20217492오류 유형: 743. Active Azure Directory에서 "API permissions"의 권한 설정이 "Not granted for ..."로 나오는 문제
12735정성태7/27/20218014.NET Framework: 1081. C# - Azure AD 인증을 지원하는 데스크톱 애플리케이션 예제(Windows Forms) [2]파일 다운로드1
12734정성태7/26/202123996스크립트: 20. 특정 단어로 시작하거나/끝나는 문자열을 포함/제외하는 정규 표현식 - Look-around
12733정성태7/23/202111261.NET Framework: 1081. Self-Contained/SingleFile 유형의 .NET Core/5+ 실행 파일을 임베딩한다면? [1]파일 다운로드2
12732정성태7/23/20216548오류 유형: 742. SharePoint - The super user account utilized by the cache is not configured.
12731정성태7/23/20217749개발 환경 구성: 584. Add Internal URLs 화면에서 "Save" 버튼이 비활성화 된 경우
12730정성태7/23/20219275개발 환경 구성: 583. Visual Studio Code - Go 코드에서 입력을 받는 경우
12729정성태7/22/20218235.NET Framework: 1080. xUnit 단위 테스트에 메서드/클래스 수준의 문맥 제공 - Fixture
12728정성태7/22/20217676.NET Framework: 1079. MSTestv2 단위 테스트에 메서드/클래스/어셈블리 수준의 문맥 제공
12727정성태7/21/20218700.NET Framework: 1078. C# 단위 테스트 - MSTestv2/NUnit의 Assert.Inconclusive 사용법(?) [1]
12726정성태7/21/20218505VS.NET IDE: 169. 비주얼 스튜디오 - 단위 테스트 선택 시 MSTestv2 외의 xUnit, NUnit 사용법 [1]
12725정성태7/21/20217197오류 유형: 741. Failed to find the "go" binary in either GOROOT() or PATH
12724정성태7/21/20219956개발 환경 구성: 582. 윈도우 환경에서 Visual Studio Code + Go (Zip) 개발 환경 [1]
12723정성태7/21/20217582오류 유형: 740. SharePoint - Alternate access mappings have not been configured 경고
12722정성태7/20/20217398오류 유형: 739. MSVCR110.dll이 없어 exe 실행이 안 되는 경우
12721정성태7/20/20218002오류 유형: 738. The trust relationship between this workstation and the primary domain failed. - 세 번째 이야기
12720정성태7/19/20217364Linux: 43. .NET Core/5+ 응용 프로그램의 Ubuntu (Debian) 패키지 준비
12719정성태7/19/20216551오류 유형: 737. SharePoint 설치 시 "0x800710D8 The object identifier does not represent a valid object." 오류 발생
... 31  32  33  34  35  [36]  37  38  39  40  41  42  43  44  45  ...