COM STA/MTA 기초 - 스레드 모델과 행(hang)을 피하는 사고방식

· 업데이트: · · COM, Windows 개발, STA, MTA, 스레드

수정 이력(1건, 최종 수정 2026년 09월 01일)

이 글에 적용한 변경 사항의 기록입니다. 보관해 둔 수정 전 버전은 DOI가 부여된 고정 URL에서 읽을 수 있습니다.

일본어 원문의 완역으로 다시 번역했습니다. 기존 한국어판은 원문의 일부만 옮긴 축약본이라 절, 표, Mermaid 그림, 그림 설명, FAQ가 빠져 있었습니다. 일본어 원문에 맞춰 이들을 모두 복원했고, 기술적인 주장은 일본어판과 같습니다. 수정 전 버전 보기 (DOI: 10.5281/zenodo.21635081)
최초 공개
이 글을 인용하기(DOI: 10.5281/zenodo.21635080)

이 글은 Zenodo에 보관되어 있습니다. 항상 최신 버전으로 연결되는 DOI와 지금 보고 있는 버전에 고정된 DOI를 아래에 함께 제시합니다.

小村 豪 (2026). 「COM STA/MTA 기초 - 스레드 모델과 행(hang)을 피하는 사고방식」. 합동회사 코무라소프트. https://doi.org/10.5281/zenodo.21635080 https://comcomponent.com/ko/blog/2026/01/31/000-sta-mta-com-relationship/

DOI(최신 버전)
10.5281/zenodo.21635080
DOI(이 버전)
10.5281/zenodo.22217403

COM의 STA/MTA는 Windows 개발이나 .NET에서 COM을 다룰 때 피해 가기 어려운 기초 지식입니다. 특히 검색에서 많이 나오는 질문은 UI 스레드가 왜 STA인지, Apartment를 넘나들면 무슨 일이 일어나는지, 왜 행(hang)이 발생하는지입니다.

목차

  1. 먼저 결론(한마디로)
  2. Apartment 모델의 호출 패턴(그림)
  3. STA(Single-Threaded Apartment)
  4. MTA(Multi-Threaded Apartment)
  5. STA/MTA는 어디서 결정되는가
  6. STA를 잘못 다루면 일어나는 행의 구체적 예
  7. 대략적인 사용 분류
  8. 정리
  9. 참고 자료

COM을 사용할 때 “어느 스레드에서 동작하는가”는 피해 갈 수 없습니다. 그 중심에 있는 것이 Apartment 모델(STA/MTA) 입니다. STA/MTA는 Windows의 일반적인 스레드 개념이 아니라, COM 객체의 호출 규칙을 결정하기 위한 스레드 모델입니다.

이 글에서는 STA와 MTA, COM의 관계를 그림으로 나타내면서, “왜 행이 발생할 수 있는가”까지 이어서 설명합니다.

Apartment 모델의 위치STA/MTA는 Windows의 일반적인 스레드 개념이 아니라 COM 객체의 호출 규칙을 결정하는 스레드 모델인 Apartment 모델의 두 형태임을 보여주는 그림.COM 객체의 호출 규칙Apartment 모델STAMTAWindows 일반의 스레드 개념이 아님

그림 1: STA/MTA는 COM 객체의 호출 규칙을 결정하는 Apartment 모델의 두 형태다.

이 글의 지식 맵

COM의 STA(Single-Threaded Apartment)와 MTA(Multi-Threaded Apartment)는 어느 스레드에서 컴포넌트를 호출해도 되는지를 정하는 Apartment 모델의 두 형태입니다. STA는 1 스레드에 1 Apartment이며, Apartment를 넘는 호출은 Proxy/Stub을 거쳐 마샬링됩니다. 다른 스레드에서 호출되는 STA는 메시지 루프를 돌리고 있지 않으면 호출의 전달을 받을 수 없어 행(hang)이 되고, 동기 호출 중에 콜백이 오는 패턴도 데드락이 되기 쉬운 구조입니다. MTA는 여러 스레드가 1 Apartment를 공유하는 대신 객체 쪽에 스레드 안전 설계를 요구합니다. .NET의 [STAThread] 속성은 이 Apartment 초기화를 설정하는 래퍼에 지나지 않습니다.

COM STA/MTA의 지식 맵STA와 MTA라는 COM의 Apartment 모델이 메시지 루프·마샬링·Proxy/Stub과 어떻게 관계되고, 메시지 루프의 부재가 STA의 행(hang)이나 데드락으로 이어지는 구조를 보여주는 그림이용한다전제로 한다전제로 한다이용한다전제로 한다이용한다이용한다이용한다전제로 한다이용한다에서 구성할 수 있다원인이 될 수 있다방지한다방지한다원인이 될 수 있다원인이 될 수 있다이용한다전제로 한다구현을 담당한다구현을 담당한다STA(Single-Threaded Apartment)MTA(Multi-Threaded Apartment)COM(컴포넌트 오브젝트 모델)COM 아파트먼트 모델(STA/MTA)CoInitializeEx메시지 루프Proxy/Stub스레드 안전 COM 개체 설계Windows Forms.NET의 [STAThread]/[MTAThread] 특성ThreadingModel 레지스트리 값AutomationIDispatchMIDLSTA hang(호출 전달 중단)MsgWaitForMultipleObjects동기 호출 중 콜백으로 인한 데드락.NET(Core 이후)마샬링

그림의 실선은 항상 성립하는 관계, 점선은 조건이 붙는 관계입니다(성립 조건은 상세 페이지의 관계별 설명에 적혀 있습니다). 관계 전체 목록(총 20건, 근거와 확신도 포함)과 주요 개념의 정의는 지식 맵 상세 페이지에 정리되어 있습니다(일본어). 데이터: JSON-LD / Turtle

1. 먼저 결론(한마디로)

  • COM 객체는 “어느 Apartment에 속하는가”로 호출 규칙이 결정됩니다
  • STA는 1 스레드에 1 Apartment, MTA는 여러 스레드가 1 Apartment라고 생각하면 이해하기 쉽습니다
  • Apartment를 넘는 호출은 COM이 Proxy/Stub을 거쳐 마샬링합니다
STA와 MTA와 Apartment 넘기의 관계STA는 1 스레드에 1 Apartment, MTA는 여러 스레드가 1 Apartment라는 형태를 취하며, Apartment를 넘는 호출은 COM이 Proxy/Stub을 거쳐 마샬링한다는 결론을 보여주는 그림.Apartment를 넘는 호출Apartment를 넘는 호출STA(1 스레드에 1 Apartment)Proxy / Stub을 거쳐 마샬링MTA(여러 스레드가 1 Apartment)

그림 2: 소속된 Apartment가 호출 규칙을 결정하며, Apartment를 넘을 때만 마샬링이 끼어든다.

2. Apartment 모델의 호출 패턴(그림)

COM 객체의 호출에는 크게 세 가지 패턴이 있습니다.

호출의 세 가지 패턴COM 객체의 호출에는 동일 STA 스레드 안에서의 호출, 동일 MTA 안에서의 호출, Apartment를 넘는 호출이라는 세 가지 패턴이 있음을 보여주는 그림.COM 객체의 호출패턴1: 동일 STA 스레드 안패턴2: 동일 MTA 안패턴3: Apartment를 넘음

그림 3: 호출 패턴은 세 가지로 나뉘며, 어디에 해당하는지에 따라 오버헤드와 주의점이 달라진다.

2.1. 패턴1: 동일 STA 스레드 안에서의 호출

같은 STA 스레드 안이라면 직접 호출할 수 있습니다. 오버헤드는 없습니다.

STA 스레드직접 호출호출 코드COM 객체

그림 4: 같은 STA 스레드 안의 호출은 직접 호출이 되어 오버헤드가 없다.

2.2. 패턴2: 동일 MTA 안에서의 호출

MTA 안의 여러 스레드에서는 어느 스레드에서든 직접 호출할 수 있습니다. 다만 객체 쪽은 스레드 안전 설계가 필수입니다.

MTA(하나의 Apartment)직접 호출직접 호출워커 스레드1COM 객체워커 스레드2

그림 5: 동일 MTA 안이라면 어느 스레드에서든 직접 호출할 수 있지만, 객체 쪽에 스레드 안전 설계가 필요하다.

2.3. 패턴3: Apartment를 넘는 호출

서로 다른 Apartment 사이에서는 COM이 Proxy/Stub을 사용해 전달합니다. 표준적인 인터페이스라면 COM 런타임이 처리해 줍니다.

이 뒤에 나오는 표에는 COM 특유의 용어가 설명 없이 등장합니다. 먼저 한 줄씩 정리해 둡니다.

용어 의미
마샬링 Apartment나 프로세스의 경계를 넘을 때, 호출과 인수를 “그대로 전달할 수 있는 형태”로 다시 담아 옮기는 것입니다. 경계 너머에서 원래 형태로 되돌려집니다
Proxy / Stub 마샬링을 담당하는 한 쌍의 부품입니다. 호출하는 쪽에 서는 것이 Proxy(진짜인 척하며 호출을 받는 역할), 호출받는 쪽에 서는 것이 Stub(받은 호출을 진짜 객체에 전달하는 역할)입니다
IDispatch 메서드 이름을 문자열로 질의해 번호로 호출하기 위한 COM 인터페이스입니다. 스크립트 언어나 VBA에서 COM을 쓸 수 있는 것은 이 구조 덕분입니다
Automation IDispatch와 거기서 쓸 수 있는 제한된 데이터 타입(BSTR, VARIANT 등)을 전제로 한 COM 사용 방식의 총칭입니다. 이 범위 안에 있으면 마샬링은 OS 쪽의 oleaut32.dll이 담당합니다
타입 라이브러리 인터페이스의 형태(메서드, 인수 타입)를 기계가 읽을 수 있는 형태로 적어 둔 데이터입니다. .tlb 파일 단독으로 두거나 DLL / EXE에 내장된 형태로 둡니다
타입 라이브러리 마샬러 타입 라이브러리를 읽어 그 자리에서 마샬링을 수행하는 COM의 표준 기능입니다. 전용 Proxy/Stub을 만들지 않아도 되는 것은 이 덕분입니다
MIDL 인터페이스 정의(.idl)로부터 Proxy/Stub 코드 등을 생성하는 Microsoft의 컴파일러입니다

주의: Proxy/Stub이 무엇이든 자동으로 준비되는 것은 아니지만, 실무에서는 명시적으로 생성하지 않아도 되는 경우가 대부분입니다.

패턴 Proxy/Stub 준비
IDispatch 기반(Automation) 불필요. oleaut32.dll이 처리
타입 라이브러리 등록됨 불필요. 타입 라이브러리 마샬러가 처리
.NET COM Interop 보통 불필요. 타입 라이브러리를 거쳐 동작
IUnknown 직접 파생 커스텀 IF MIDL로 Proxy/Stub 생성·등록 필요

즉, MIDL로 Proxy/Stub 생성이 필요해지는 것은 IDispatch를 쓰지 않고 IUnknown을 직접 파생한 인터페이스를 만드는 경우입니다. .NET이나 스크립트 언어에서 사용하는 일반적인 COM 컴포넌트에서는 이 작업이 필요해지는 일이 드뭅니다.

Proxy/Stub을 직접 준비할지 여부의 갈림길IDispatch 기반이나 타입 라이브러리 마샬러 등 COM 표준 마샬링으로 해결되는 경우는 Proxy/Stub 생성이 불필요하고, 그것으로 해결되지 않는 IUnknown 직접 파생 커스텀 인터페이스일 때만 MIDL로 Proxy/Stub을 생성·등록해야 한다는 갈림길을 보여주는 그림.아니오표준 마샬링으로 해결되는가Proxy/Stub 생성 불필요MIDL로 Proxy/Stub 생성·등록IDispatch나 타입 라이브러리가 처리실무에서는 대부분 이쪽

그림 6: Proxy/Stub을 명시적으로 생성하는 것은, 표준 마샬링으로 해결되지 않는 IUnknown 직접 파생 커스텀 인터페이스일 때다.

MTA 스레드COM 런타임(자동)STA 스레드호출전달COM 객체ProxyRPC/IPCStub호출 코드

그림 7: Apartment를 넘는 호출은 COM 런타임이 자동으로 준비하는 Proxy·RPC·Stub을 거쳐 전달된다.

포인트: Apartment를 넘으면 마샬링 오버헤드가 발생합니다. 고빈도 호출에서는 성능에 영향을 주므로 설계 시점에 고려가 필요합니다.

2.4. 마샬링 오버헤드의 기준

아래는 일반적인 기준값입니다(실측값이 아니며, 상황이나 파라미터의 복잡도에 따라 크게 달라집니다).

호출 패턴 대략의 시간 상대적인 감각
동일 Apartment 내(직접) 10~100나노초 일반적인 함수 호출과 거의 동일
다른 Apartment(동일 프로세스) 1~10마이크로초 직접 호출의 100~1000배
다른 프로세스(Out-of-proc) 100~1000마이크로초 직접 호출의 1만~10만 배

상대적인 비교:

  • 동일 Apartment: 메모리 접근 1회 정도
  • 다른 Apartment: 시스템 호출 1회 정도
  • 다른 프로세스: 로컬호스트로의 네트워크 통신 정도

루프에서 1만 번 호출하는 상황에서는 이 차이가 뚜렷하게 나타납니다.

이 수치를 다루는 방법에 대해

위 표는 자릿수(오더) 감각을 잡기 위한 것이며, 출처가 있는 실측값이 아닙니다. 공개된 통일 벤치마크가 있는 것도 아니므로, 이 수치 자체를 설계 판단의 근거로 삼지 마십시오.

한편 “동일 Apartment 내는 직접 호출, 경계를 넘으면 반드시 마샬링이 끼어든다”는 구조는 Microsoft 문서에 명시된 사양입니다. Single-Threaded Apartments 항목에는 같은 Apartment 안이라면 인터페이스 포인터를 마샬링 없이 전달할 수 있다는 것, Apartment를 넘는 경우에는 동일 프로세스 안에서도 프로세스 간과 같은 마샬링 구조를 쓴다는 것, 그리고 호출이 숨겨진 창(OleMainThreadWndClass)으로의 윈도우 메시지로서 도달한다는 것이 명시되어 있습니다. 자릿수가 바뀌는 것은 이 구조가 이유이며, 수치 자체는 환경과 인수의 복잡도에 따라 달라집니다.

자신의 케이스로 판단하고 싶을 때는, 같은 인터페이스를 동일 Apartment에서 호출한 경우와 다른 Apartment에서 호출한 경우를 직접 측정해 비교하는 것이 확실합니다.

자릿수가 바뀌는 구조적 이유동일 Apartment 내는 직접 호출인 데 비해, Apartment를 넘으면 동일 프로세스 안에서도 반드시 마샬링이 끼어들고, 대상이 STA인 경우 호출이 숨겨진 창으로의 윈도우 메시지로 도달한다는, 자릿수가 바뀌는 구조적 이유를 보여주는 그림.동일 Apartment 내 호출직접 호출Apartment를 넘는 호출반드시 마샬링이 끼어듦STA 대상은 숨겨진 창으로 도달

그림 8: 자릿수가 바뀌는 것은 경계를 넘으면 반드시 마샬링이 끼어든다는 사양상의 구조가 이유다.

// C#. 같은 호출을 N번 반복해 1회당 시간을 구한다
static void Measure(string label, Action call, int iterations = 100_000)
{
    call(); // 최초 지연(JIT, 프록시 생성, 연결 확립)은 측정에서 제외

    var sw = System.Diagnostics.Stopwatch.StartNew();
    for (int i = 0; i < iterations; i++)
    {
        call();
    }
    sw.Stop();

    double perCallNs = sw.Elapsed.TotalMilliseconds * 1_000_000.0 / iterations;
    Console.WriteLine($"{label}: {perCallNs:F1} ns/call");
}

인수가 적은 메서드와 문자열이나 배열을 전달하는 메서드 양쪽으로 측정해 보면, “마샬링하는 양에 따라 효과가 달라진다”는 것도 알 수 있습니다.

3. STA(Single-Threaded Apartment)

STA는 “1 스레드 = 1 Apartment”라는 모델입니다.

  • 해당 Apartment 안의 COM 객체는 기본적으로 그 스레드에서만 실행됩니다
  • 다른 스레드에서 호출하면 COM이 메시지 큐/RPC를 거쳐 호출을 전달합니다
  • UI 스레드(WinForms/WPF)에서 자주 쓰입니다(UI도 “1 스레드 친화성 + 메시지 루프”라서 궁합이 좋습니다)
STA의 실행 모델STA에서는 Apartment 안의 COM 객체가 기본적으로 생성한 스레드에서만 실행되고, 다른 스레드에서의 호출은 COM이 메시지 큐나 RPC를 거쳐 그 스레드로 전달함을 보여주는 그림.STA의 COM 객체생성한 스레드에서만 실행다른 스레드에서의 호출메시지 큐 / RPC를 거쳐 전달

그림 9: STA의 객체는 소유자 스레드에서만 동작하며, 다른 곳에서의 호출은 전달되어 도달한다.

3.1. 왜 UI 스레드에서 STA가 쓰이는가

UI 스레드와 STA는 설계가 일치하기 때문입니다.

  • UI 컨트롤은 스레드 안전하지 않습니다 버튼이나 텍스트 박스 등은 생성한 스레드에서만 안전하게 조작할 수 있습니다
  • STA도 마찬가지로 “1 스레드 친화성”입니다 COM 객체는 생성한 스레드에서만 직접 실행됩니다
  • UI 스레드는 반드시 메시지 루프를 돌립니다 윈도우 이벤트를 처리하기 위해 필수입니다. STA의 전제(메시지 펌프)와 일치합니다

그래서 WinForms/WPF의 UI 스레드는 기본값이 STA입니다.

UI 스레드와 STA의 설계 일치UI 컨트롤은 생성한 스레드에서만 안전하게 조작할 수 있고, STA도 1 스레드 친화성을 가지며, UI 스레드가 반드시 돌리는 메시지 루프는 STA의 전제인 메시지 펌프와 일치하기 때문에 UI 스레드가 기본값으로 STA가 됨을 보여주는 그림.UI 컨트롤의 1 스레드 친화성설계가 일치STA의 1 스레드 친화성UI 스레드의 메시지 루프STA의 전제(메시지 펌프)UI 스레드는 기본값이 STA

그림 10: 1 스레드 친화성과 메시지 루프라는 두 지점에서 설계가 일치하기 때문에 UI 스레드는 STA가 되어 있다.

포인트: STA는 스레드 친화성이 높은 대신, 호출자가 많으면 병목이 되기 쉽습니다.

4. MTA(Multi-Threaded Apartment)

MTA는 “여러 스레드가 1 Apartment”라는 모델입니다.

  • COM 객체가 여러 스레드에서 동시에 호출됩니다
  • 객체 쪽에서 스레드 안전 설계가 필수입니다
  • 서버 사이드 처리나 백그라운드 처리에 적합합니다

포인트: MTA는 병렬성이 높지만, 객체 구현의 책임이 무겁습니다.

MTA의 실행 모델MTA는 여러 스레드가 하나의 Apartment를 공유하고, COM 객체가 여러 스레드에서 동시에 호출되기 때문에 객체 쪽에 스레드 안전 설계가 필수가 됨을 보여주는 그림.MTA(여러 스레드가 1 Apartment)여러 스레드에서 동시에 호출됨객체 쪽의 스레드 안전 설계가 필수서버 사이드나 백그라운드 처리에 적합

그림 11: MTA는 병렬로 호출할 수 있는 대신, 안전성의 책임이 객체 구현 쪽으로 옮겨간다.

5. STA/MTA는 어디서 결정되는가

COM의 Apartment는 스레드마다 초기화함으로써 결정됩니다.

  • CoInitialize / CoInitializeEx를 호출한 순간, 그 스레드의 Apartment가 정해집니다
  • STA: COINIT_APARTMENTTHREADED
  • MTA: COINIT_MULTITHREADED
Apartment가 정해지는 순간스레드가 CoInitialize 또는 CoInitializeEx를 호출한 순간 그 스레드의 Apartment가 정해지며, COINIT_APARTMENTTHREADED면 STA, COINIT_MULTITHREADED면 MTA가 됨을 보여주는 그림.COINIT_APARTMENTTHREADEDCOINIT_MULTITHREADED스레드CoInitialize / CoInitializeEx를 호출STA로 정해짐MTA로 정해짐

그림 12: Apartment는 스레드마다 초기화를 호출한 순간의 지정으로 정해진다.

5.1. .NET에서의 STA/MTA

.NET에도 [STAThread] / [MTAThread] 속성이나 ApartmentState가 있지만, 이들은 COM의 Apartment 모델을 설정하기 위한 래퍼입니다.

  • [STAThread]Main 메서드(엔트리 포인트)에 붙입니다. COM을 쓸 때 STA로 초기화됩니다
  • [MTAThread] → 마찬가지로 Main 메서드용입니다. MTA로 초기화됩니다
  • Thread.SetApartmentState(ApartmentState.STA)추가로 만드는 스레드용입니다. 스레드 시작 전에 설정해야 합니다

주의점:

  • [STAThread]가 있어도 실제로 COM을 호출하기 전까지는 초기화되지 않습니다(COM을 쓰지 않으면 효과가 없습니다)
  • 추가 스레드에는 [STAThread]가 적용되지 않습니다. Thread.SetApartmentState를 사용합니다

즉, .NET의 STA/MTA는 COM의 STA/MTA 그 자체이며, COM Interop을 위해 마련된 장치입니다.

중요: 나중에 Apartment를 변경할 수는 없습니다. 최초 초기화가 전부입니다.

.NET에서 Apartment를 설정하는 위치Main 메서드에는 STAThread나 MTAThread 속성을 붙이고, 추가로 만드는 스레드에는 시작 전에 Thread.SetApartmentState로 설정하며, 둘 다 최초 초기화에서 Apartment가 확정되어 나중에 변경할 수 없음을 보여주는 그림.Main 메서드〔STAThread〕/〔MTAThread〕 속성추가로 만드는 스레드시작 전에 Thread.SetApartmentState최초 초기화에서 Apartment가 확정나중에 변경할 수 없음

그림 13: 엔트리 포인트는 속성으로, 추가 스레드는 SetApartmentState로 지정하며, 둘 다 최초 1회에 확정된다.

6. STA를 잘못 다루면 일어나는 행의 구체적 예

다음과 같은 구성은 실제로 행을 일으키기 쉽습니다.

6.1. 자주 있는 상황

  • 백그라운드에서 STA 스레드를 만들어 COM 객체를 생성합니다
  • 그 스레드는 메시지 루프를 돌리고 있지 않습니다
  • 다른 스레드(STA/MTA 상관없이)에서 그 COM 객체를 호출합니다
행이 발생하기 쉬운 구성백그라운드에서 만든 STA 스레드가 COM 객체를 생성했지만 메시지 루프를 돌리고 있지 않고, 거기에 다른 스레드가 그 COM 객체를 호출한다는 행이 발생하기 쉬운 구성을 보여주는 그림.호출백그라운드의 STA 스레드COM 객체를 생성메시지 루프를 돌리고 있지 않음다른 스레드(STA / MTA 상관없이)

그림 14: “루프를 돌리지 않는 STA가 가진 객체를, 다른 스레드에서 호출한다”는 조합이 위험하다.

6.2. 무엇이 일어나는가

행이 발생하는 이유는 STA의 두 가지 전제로 정리할 수 있습니다. 이 절이 그 이유를 설명하는 부분이며, 이후 절에서는 반복하지 않습니다.

  • COM 객체는 생성한 STA 스레드에서 처리됩니다 호출자가 STA든 MTA든, 다른 스레드에서의 호출은 반드시 그 STA 스레드로 전달됩니다. 전달은 COM이 그 Apartment에 만드는 숨겨진 창(윈도우 클래스 OleMainThreadWndClass)으로의 윈도우 메시지로서 도달합니다
  • 그 전달을 받으려면 STA 스레드가 메시지 펌프를 돌리고 있어야 합니다 Microsoft 문서에도 “각 STA는 다른 프로세스나 동일 프로세스 내의 다른 Apartment로부터의 호출을 처리하기 위해 메시지 루프를 가지고 있어야 한다”고 명시되어 있습니다

따라서 메시지를 돌리고 있지 않은 STA 스레드는 호출을 받을 수 없고, 호출자는 응답을 계속 기다리다가 결과적으로 행이 됩니다.

행에 이르는 흐름다른 스레드로부터의 호출은 생성한 STA 스레드로 숨겨진 창으로의 윈도우 메시지로서 전달되지만, STA 스레드가 메시지 펌프를 돌리고 있지 않으면 받을 수 없어 호출자가 응답을 계속 기다리다 행이 되는 흐름을 보여주는 그림.다른 스레드에서의 호출생성한 STA 스레드로 전달숨겨진 창으로의 메시지로 도달메시지 펌프가 돌고 있지 않음STA 쪽이 호출을 받을 수 없음호출자는 응답을 계속 기다림

그림 15: 전달은 메시지로 도달하므로, 펌프가 멈춰 있는 STA에는 받을 사람이 없어진다.

또한 이는 .NET에서도 마찬가지입니다. Single-Threaded Apartments 문서에는 STA 스레드를 Task.Wait(), Task.Result, Thread.Sleep(), ManualResetEvent.WaitOne() 등으로 블로킹하면 COM 콜백이나 Apartment를 넘는 호출이 완료되지 못해 데드락이 된다는 경고가 실려 있습니다. 다음 6.3의 실패 예에서 WaitOne()으로 멈추어 있는 것은 바로 이 형태입니다.

반면 UI 스레드는 윈도우 이벤트를 처리하기 위해 처음부터 메시지 루프를 돌리고 있으므로, STA의 요건을 추가 구현 없이 충족합니다. UI 스레드가 STA COM 객체를 움직이는 장소로 자연스러운 선택지가 되는 것은 이 때문입니다.

6.3. 의사 코드(전형적인 실패 패턴)

using System;
using System.Runtime.InteropServices;
using System.Threading;

internal static class StaHangDemo
{
    private const uint COINIT_APARTMENTTHREADED = 0x2;

    [DllImport("ole32.dll")]
    private static extern int CoInitializeEx(IntPtr pvReserved, uint dwCoInit);

    [DllImport("ole32.dll")]
    private static extern void CoUninitialize();

    public static void Run(string progId)
    {
        var ready = new AutoResetEvent(false);
        var done = new AutoResetEvent(false);

        object comObj = null;

        var staThread = new Thread(() =>
        {
            // STA로 초기화
            CoInitializeEx(IntPtr.Zero, COINIT_APARTMENTTHREADED);

            // ThreadingModel=Apartment로 등록된 COM 클래스를 가정
            Type type = Type.GetTypeFromProgID(progId, throwOnError: true);
            comObj = Activator.CreateInstance(type);
            ready.Set();

            // 메시지 루프 없이 대기 -> 여기가 치명상
            done.WaitOne();

            CoUninitialize();
        });

        staThread.SetApartmentState(ApartmentState.STA);
        staThread.Start();

        ready.WaitOne();

        try
        {
            // 다른 스레드(STA/MTA 상관없이)에서 호출하면 호출이 STA로 전달된다
            // 하지만 STA 쪽은 메시지를 처리하지 않으므로 여기서 행이 되기 쉽다
            dynamic obj = comObj;
            obj.AnyMethod();
        }
        finally
        {
            // 호출이 돌아온 경우 ── agile한 클래스였다,
            // 호출이 전달되지 않았다, AnyMethod가 즉시 반환했다 ── 이더라도
            // 반드시 STA 스레드를 해제한다. 이것을 생략하면 done을 계속
            // 기다리는 포그라운드 스레드가 남아, "재현되지 않았을" 때도
            // 프로세스가 끝나지 않는다. 재현 여부를 증상으로 구별할 수 없게 된다
            done.Set();
            staThread.Join();
        }
    }
}

이 코드는 ThreadingModel=Apartment(= STA)로 등록된 COM 클래스의 ProgID를 전달한다는 전제입니다. AnyMethod는 그 클래스가 실제로 가진 메서드 이름으로 바꿔 주십시오. 어떤 COM 클래스에서도 재현되는 것은 아닙니다. 재현에 필요한 조건은 다음 세 가지입니다.

조건 확인 방법
대상 클래스의 ThreadingModelApartment 레지스트리의 HKEY_CLASSES_ROOT\CLSID\{CLSID}\InprocServer32에 있는 ThreadingModel 값을 확인합니다. BothFree이면 호출이 전달되지 않아 재현되지 않습니다
STA 스레드가 메시지를 돌리고 있지 않음 위 예시의 done.WaitOne()이 여기에 해당합니다
호출을 다른 스레드에서 수행함 같은 STA 스레드 안에서 호출하는 한 직접 호출이 되므로 행이 되지 않습니다

obj.AnyMethod()try / finally로 감싸고 done.Set()staThread.Join()을 반드시 거치도록 한 것은, 이 “재현되지 않는 경우”를 구별하기 위해서입니다. STA 스레드는 기본값이 포그라운드 스레드이므로, done이 설정되지 않는 한 프로세스는 끝나지 않습니다. finally를 생략하면 재현되었을 때(호출이 돌아오지 않음)와 재현되지 않았을 때(호출은 돌아왔지만 아무도 done을 설정하지 않음) 모두 겉으로 보이는 증상이 “프로세스가 끝나지 않는다”가 됩니다. 재현 조건을 확인하기 위한 도구가 조건의 성립 여부 자체를 감춰 버리는 것입니다. 위와 같은 형태라면 재현되지 않았을 때는 그대로 정상 종료됩니다.

멈췄는지 확인하려면 디버거로 프로세스를 일시 정지시키고, 호출자 스레드의 스택이 COM 대기 상태에서 멈춰 있는지, STA 스레드가 WaitOne에서 멈춰 있는지를 보는 것이 빠릅니다.

COM 런타임STA 스레드메인 스레드COM 런타임STA 스레드메인 스레드메시지 루프 없음여기서 멈춰 있음메시지로 전달하지만...WaitOne 중이라메시지를 처리할 수 없음호출자도 계속 대기양쪽 모두 대기 상태 → 행스레드 시작CoInitializeEx(STA)COM 객체 생성ready.Set()done.WaitOne()으로 대기CallComObject()호출을 전달하려 함

그림 16: WaitOne에서 멈춘 STA 스레드와, 전달의 응답을 기다리는 메인 스레드가 서로 진행하지 못하게 된다.

그림 가운데의 “메시지로 전달하지만…” 두 줄이, 6.2에서 설명한 두 가지 전제가 무너지는 지점입니다.

6.4. 회피의 요점

  • 다른 스레드로부터의 호출을 받는 경우, STA 스레드는 메시지 루프를 돌려야 합니다
  • 가능하다면 UI 스레드 위에서 생성·사용합니다(UI 스레드는 처음부터 메시지 루프가 있습니다)
  • STA가 필요 없다면 처음부터 MTA로 만듭니다

보충: 같은 스레드 안에서만 완결된다면, 항상 Application.Run()이 필요한 것은 아닙니다. 다만 UI 계열·COM 계열은 다른 스레드로부터의 호출이 얽히는 경우가 많아, 실무상 거의 필수입니다.

행을 피하는 세 가지 방향다른 스레드로부터의 호출을 받는다면 STA 스레드에서 메시지 루프를 돌리고, 가능하다면 처음부터 메시지 루프를 가진 UI 스레드 위에서 생성·이용하며, STA가 필요 없다면 처음부터 MTA로 한다는 세 가지 회피 방향을 보여주는 그림.STA의 행을 어떻게 피할 것인가STA 스레드에서 메시지 루프를 돌린다UI 스레드 위에서 생성·이용한다STA가 필요 없다면 처음부터 MTAUI 스레드는 처음부터 루프가 있다

그림 17: 회피책은 루프를 돌린다·루프가 있는 곳으로 옮긴다·STA를 그만둔다는 세 방향으로 정리할 수 있다.

6.5. “메시지 루프를 돌린다”가 결국 무엇인가

Win32의 UI 스레드가 하고 있는, 바로 그 코드입니다.

while (GetMessage(out var msg, IntPtr.Zero, 0, 0))
{
    TranslateMessage(ref msg);
    DispatchMessage(ref msg);
}

STA에서는 다른 스레드로부터의 호출이 “전달”되어 옵니다. 그 전달을 받아서 실행으로 넘기는 것이 바로 이 루프(메시지 펌프)라는 이야기입니다.

메시지 펌프의 역할다른 스레드로부터 전달되어 온 호출을 GetMessage로 받아 DispatchMessage로 실행에 넘기는 반복이 메시지 펌프임을 보여주는 그림.전달되어 온 호출GetMessage로 받음DispatchMessage로 실행에 넘김

그림 18: “메시지 루프를 돌린다”는 것은, 전달을 받아서 실행으로 넘기는 이 반복을 말한다.

6.6. 올바른 방향의 예(러프하게 쓰면)

“백그라운드 STA에서 COM을 쓰고 싶다”면 다음과 같은 형태가 됩니다.

var ready = new AutoResetEvent(false);
object comObj = null;

var staThread = new Thread(() =>
{
    CoInitializeEx(IntPtr.Zero, COINIT_APARTMENTTHREADED);

    comObj = new SomeStaComObject();
    ready.Set();

    // STA 스레드가 살아 있는 동안은 메시지를 돌린다
    Application.Run();

    CoUninitialize();
});

staThread.SetApartmentState(ApartmentState.STA);
staThread.Start();

ready.WaitOne();
CallComObject(comObj);

(※ CoInitializeEx / CoUninitialize 호출을 잊으면 그대로 사고가 납니다. CoInitializeEx의 P/Invoke 선언은 6.3과 같은 것이 필요합니다)

Application.Run()을 인수 없이 쓰는 형태는 폼 없이 메시지 루프만 돌리기 위한 것입니다. 용도로는 바로 이 상황이 상정하는 장면이지만, 다음 두 가지는 짚어 두어야 합니다.

  • System.Windows.Forms에 대한 참조가 필요합니다. 콘솔 앱이라면 프로젝트 파일에 <UseWindowsForms>true</UseWindowsForms>를 추가합니다
  • 이 루프는 스스로 끝나지 않습니다. 멈추려면 그 스레드 위에서 Application.ExitThread()(또는 앱 전체를 종료하는 Application.Exit())를 호출합니다. 위 예시에서 CoUninitialize()까지 도달시키고 싶다면, STA 스레드에 “종료”를 알려 ExitThread를 호출하게 하는 구조가 별도로 필요합니다

WinForms에 의존하고 싶지 않다면, 6.5의 GetMessage / DispatchMessage 루프를 직접 작성하거나, 메시지와 동기화 객체를 동시에 기다릴 수 있는 MsgWaitForMultipleObjects를 사용합니다. 후자는 “이벤트로 종료 통지를 받으면서도 COM 호출을 놓치지 않는” 형태로 만들고 싶을 때의 정석입니다.

백그라운드 STA에서 루프를 돌리는 세 가지 수단백그라운드 STA에서 메시지 루프를 돌리려면 WinForms 참조가 필요한 Application.Run을 쓰거나, GetMessage와 DispatchMessage 루프를 직접 작성하거나, 메시지와 동기화 객체를 동시에 기다릴 수 있는 MsgWaitForMultipleObjects를 쓰는 세 가지 수단이 있음을 보여주는 그림.백그라운드 STA에서 루프를 돌린다Application.RunGetMessage 루프를 직접 작성MsgWaitForMultipleObjectsWinForms 참조와 종료 수단이 필요메시지와 동기화 객체를 동시에 기다릴 수 있음

그림 19: 루프를 돌리는 방법은 세 가지이며, 종료시키는 방법과 의존성의 무게로 선택하게 된다.

6.7. 또 하나의 행 예: 동기 호출 중의 콜백

STA는 “호출이 전달되어 온다”는 것만이 아니라, 상황에 따라서는 반대 방향(서버 → 클라이언트)으로 콜백이 오기도 합니다. 그중에서도 동기 호출 중에 콜백이 발생하는 패턴은 데드락의 단골 원인입니다.

COM 서버UI 스레드(STA)COM 서버UI 스레드(STA)DoWork의 반환을 기다리는 중(메시지를 처리하지 않음)대기 중이므로콜백을 받을 수 없음콜백 완료를 기다리는 중서로가 상대를 기다림 → 데드락DoWork()(동기 호출)ProgressCallback()(콜백)

그림 20: 동기 호출의 반환을 기다리는 UI 스레드와, 콜백 완료를 기다리는 서버가 서로를 기다린다.

왜 데드락이 되기 쉬운가:

  1. UI 스레드가 DoWork()동기 호출(블로킹)합니다
  2. UI 스레드는 반환을 기다립니다(메시지를 처리하지 않습니다)
  3. 서버가 ProgressCallback()을 UI 스레드로 보냅니다
  4. UI 스레드는 대기 중이므로 콜백을 받을 수 없습니다
  5. 서버는 콜백 완료를 기다립니다
  6. 서로가 상대를 기다림 → 영원히 진행되지 않음

처리 시간의 길이는 관계없습니다. 동기 호출 중에 콜백이 오는 패턴 자체가 문제가 되기 쉽습니다.

보충: COM에는 상황에 따라 메시지를 돌리거나 재진입하는 구조도 있어, 컴포넌트나 호출 형태에 따라 동작이 달라집니다. 반드시 데드락이 되는 것은 아니지만, 이 패턴은 피하는 편이 무난합니다.

7. 대략적인 사용 분류

상황 선택할 것 판단 이유 함께 필요한 것
UI가 관련됨(WinForms / WPF) STA UI 컨트롤이 1 스레드 친화성을 가지고, UI 스레드는 원래부터 메시지 루프를 갖기 때문(3.1) 특별히 없습니다. 기본값이 STA입니다
대량의 병렬 처리를 시키고 싶음 MTA 여러 스레드가 하나의 Apartment를 공유해, 전달 없이 직접 호출할 수 있기 때문(2.2) COM 객체 쪽의 스레드 안전 설계. 호출하는 쪽의 배타 제어가 아니라 객체 구현 쪽의 책임입니다
백그라운드에서 STA의 COM을 쓰고 싶음 STA + 메시지 루프 다른 스레드에서 호출되는 이상, 전달을 받을 창구가 필요하기 때문(6.2) Application.Run()이나 GetMessage 루프. 종료 수단(ExitThread 등)도 함께 설계합니다(6.6)
사용할 COM 컴포넌트의 요구 사항이 정해져 있음 상대에게 맞춤 Apartment는 호출 규칙 그 자체로, 나중에 바꿀 수 없기 때문(5장) ThreadingModel 값을 확인합니다. Apartment라면 STA를 전제로 구성합니다
고빈도로 호출함 호출하는 쪽과 같은 Apartment로 맞춤 경계를 넘을 때마다 마샬링이 끼어들기 때문(2.4) 어렵다면 호출 횟수 자체를 줄이는(모아서 전달하는) 설계로 바꿉니다

고민될 때의 우선순위는 “상대의 요구 > UI 유무 > 병렬성” 순으로 보면 정하기 쉽습니다. Apartment는 최초 초기화에서 확정되어 나중에 변경할 수 없으므로, 이 부분만은 구현을 시작하기 전에 정해 둡니다.

고민될 때 확인하는 순서STA와 MTA의 사용 구분이 고민될 때는 사용할 COM 컴포넌트 쪽의 요구, UI 유무, 병렬성 순으로 확인하면 정하기 쉬움을 보여주는 그림.다음으로다음으로상대의 요구UI 유무병렬성

그림 21: 사용 구분이 고민된다면 상대의 요구·UI 유무·병렬성 순으로 확인한다.

8. 정리

STA/MTA는 COM을 위한 스레드 모델로, STA는 1 스레드 = 1 Apartment, MTA는 여러 스레드가 1 Apartment라는 형태를 취합니다. Apartment를 넘는 호출은 COM이 Proxy/Stub을 거쳐 전달해 주지만(표준 IF 이외에는 MIDL 등으로 생성·등록이 필요), 거기에는 마샬링 오버헤드가 따르므로 고빈도 호출이 예상되는 상황에서는 Apartment 설계를 신중하게 정하고 싶은 부분입니다.

행이라는 관점에서는 “다른 스레드로부터의 호출을 받는 STA 스레드는 메시지 펌프를 돌리는 것이 전제”라는 한 가지로 요약됩니다. 메시지를 돌리지 않는 STA 스레드에 호출하면 행이 되기 쉽고, 동기 호출 중에 콜백이 오는 패턴도 데드락이 되기 쉽습니다. UI 스레드는 “1 스레드 친화성”과 “메시지 루프”를 처음부터 가지고 있어 이 전제를 추가 구현 없이 충족하므로, STA의 COM과 궁합이 좋은 것입니다.

9. 참고 자료

  • Apartment Model https://learn.microsoft.com/en-us/windows/win32/com/com-apartments
  • CoInitializeEx https://learn.microsoft.com/en-us/windows/win32/api/objbase/nf-objbase-coinitializeex
  • Single-Threaded Apartments(메시지 루프가 필수인 이유, 숨겨진 창, .NET에서의 데드락 경고) https://learn.microsoft.com/en-us/windows/win32/com/single-threaded-apartments
  • Multithreaded Apartments https://learn.microsoft.com/en-us/windows/win32/com/multithreaded-apartments
  • InprocServer32(ThreadingModel 값) https://learn.microsoft.com/en-us/windows/win32/com/inprocserver32
  • Application.Run 메서드(폼 없이 메시지 루프만 돌리는 방법과 그 중지 방법) https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.application.run
  • MsgWaitForMultipleObjects(메시지와 동기화 객체를 동시에 기다림) https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-msgwaitformultipleobjects

이 글의 Word 파일 다운로드

같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.

이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.

ActiveX 이관

COM / ActiveX / OCX 자산을 유지할지, 감쌀지, 교체할지의 단계적 판단을 정리한 토픽 페이지입니다.

이 기사는 다음 서비스 페이지로 이어집니다. 가까운 입구부터 확인해 주세요.

자주 묻는 질문

이 기사 주제에 대해 상담 시 자주 나오는 질문을 모았습니다.

STA와 MTA 중 어느 쪽을 선택해야 하나요?
UI가 관련된 처리라면 STA, 대량의 병렬 처리라면 MTA를 쓰는 것이 기본적인 구분입니다. STA는 1 스레드에 1 Apartment라는 형태로 스레드 친화성이 높은 대신, 호출자가 많으면 병목이 되기 쉽습니다. MTA는 여러 스레드가 1개의 Apartment를 공유하기 때문에 병렬성이 높지만, COM 객체 쪽에 스레드 안전 설계가 반드시 필요합니다. 둘 다 아니라면, 사용할 기존 라이브러리나 COM 서버의 요구 사항에 맞추는 것이 현실적입니다.
UI 스레드는 왜 STA인가요?
UI 스레드와 STA의 설계가 일치하기 때문입니다. 버튼이나 텍스트 박스 같은 UI 컨트롤은 생성한 스레드에서만 안전하게 조작할 수 있고, STA도 마찬가지로 1 스레드 친화성을 가집니다. 또한 UI 스레드는 윈도우 이벤트 처리를 위해 반드시 메시지 루프를 돌리는데, 이것이 STA의 전제인 메시지 펌프와 일치합니다. 그래서 WinForms/WPF의 UI 스레드는 기본값이 STA입니다.
STA의 COM 객체를 호출하면 왜 행(hang)이 발생하나요?
STA의 COM 객체에 대한 호출은 그 객체를 생성한 STA 스레드에서 처리됩니다. 다른 스레드에서의 호출은 COM이 메시지/RPC를 통해 전달하지만, STA 스레드가 메시지 루프를 돌리고 있지 않으면 그 전달을 받을 수 없어 호출자가 계속 대기하다가 행이 됩니다. 이를 피하려면 다른 스레드에서 호출되는 STA 스레드에서 메시지 루프를 돌리거나, UI 스레드 위에서 생성·사용하거나, STA가 필요 없다면 처음부터 MTA로 만들면 됩니다.
.NET의 [STAThread] 속성은 무엇을 하나요?
[STAThread]는 COM Apartment 모델을 설정하기 위한 래퍼로, Main 메서드(엔트리 포인트)에 붙이면 COM을 쓸 때 그 스레드가 STA로 초기화됩니다. 다만 실제로 COM을 부르기 전에는 초기화되지 않으며, 추가로 만드는 스레드에는 효과가 없으므로 Thread.SetApartmentState(ApartmentState.STA)를 스레드 시작 전에 설정해야 합니다. 한 번 정해진 Apartment는 나중에 바꿀 수 없습니다.

저자 프로필

기사 저자의 프로필 페이지입니다.

Go Komura

합동회사 코무라소프트 대표

Windows 소프트웨어 개발, 기술 상담, 장애 조사를 중심으로 재현이 어려운 장애 조사와 기존 자산이 남아 있는 프로젝트에 강점이 있습니다.

블로그 목록으로 돌아가기