COM STA/MTA의 기초 지식 - 스레드 모델과 hang을 피하는 사고방식
· 업데이트: · Go Komura · COM, Windows 개발, STA, MTA, 스레드
수정 이력(8건, 최종 수정 2026년 09월 03일)
이 글에 적용한 변경 사항의 기록입니다. 보관해 둔 수정 전 버전은 DOI가 부여된 고정 URL에서 읽을 수 있습니다.
- permalink·저자 표기·지식 맵 래퍼·깨진 내부 링크 등 CI가 지적한 표시용 수정을 반영했습니다. 본문의 기술적인 주장은 바꾸지 않았습니다.
- 일본어 원문의 완역으로 다시 번역했습니다. 기존 한국어판은 원문의 일부만 옮긴 축약본이라 절, 표, Mermaid 그림, 그림 설명, FAQ가 빠져 있었습니다. 일본어 원문에 맞춰 이들을 모두 복원했고, 기술적인 주장은 일본어판과 같습니다. 수정 전 버전 보기 (DOI: 10.5281/zenodo.21635081)
- 리뷰 지적에 따라, 오늘 추가한 그림 가운데 폭이 너무 컸던 것을 세로 구성으로 고치고, 일부 그림과 캡션 표현을 본문 서술과 맞게 바로잡았습니다. 본문 문장은 바꾸지 않았습니다.
- Apartment가 정해지는 방식, 호출 전달, hang에 이르는 흐름, 회피책의 분기 등을 그림으로도 따라갈 수 있도록 Mermaid 그림을 16점 추가하고, 기존 그림에 일련번호 캡션을 붙였습니다(본문 500~750자당 1그림 규약에 맞춘 것입니다). 본문 문장은 바꾸지 않았습니다.
- 글 맨 앞에 「이 글의 지식 맵」 절을 추가했습니다. 본문에서 다루는 개념과 그 관계를 요약·그림·상세 페이지 링크로 모은 것입니다. 본문의 주장은 바꾸지 않았습니다.
- 외부 리뷰(1283건)에 대한 대응으로 본문을 갱신했습니다. 개별 변경 내용은 아래 이력을 참조하세요.
- STA hang 재현 코드에서, 재현되지 않은 경우에도 프로세스가 끝나지 않던 상태를 고쳤습니다. STA 스레드는 `done.WaitOne()`으로 기다리는 포그라운드 스레드이므로, `obj.AnyMethod()`가 돌아온 경우(agile한 클래스였거나, 호출이 전달되지 않은 경우 등)에는 아무도 `done`을 설정하지 않아 프로세스가 종료되지 않습니다. 재현된 때와 재현되지 않은 때 모두 밖에서 보이는 증상이 「프로세스가 끝나지 않는다」가 되어, 재현 조건을 확인하는 도구가 조건의 성패를 가리고 있었습니다. `try`/`finally`에서 `done.Set()`과 `staThread.Join()`을 반드시 거치도록 했습니다.
- hang 이유가 여러 곳에 흩어져 있던 것을 6.2로 모으고, 중복 설명을 그림 참조로 바꿨습니다. 의사 코드였던 재현 예를 `CoInitializeEx` 선언을 포함해 `Type.GetTypeFromProgID`를 쓰는 실제로 동작하는 형태로 다시 쓰고, 재현 조건과 확인 방법을 표로 명시했습니다. 용어표와 판단표를 추가하고, 응답 시간 수치는 출처 있는 실측값이 아니라는 점과 직접 측정하기 위한 계측 코드를 덧붙였습니다.
- 최초 공개
이 글을 인용하기(DOI: 10.5281/zenodo.21635080)
이 글은 Zenodo에 보관되어 있습니다. 항상 최신 버전으로 연결되는 DOI와 지금 보고 있는 버전에 고정된 DOI를 아래에 함께 제시합니다.
Go Komura (2026). 「COM STA/MTA의 기초 지식 - 스레드 모델과 hang을 피하는 사고방식」. 합동회사 코무라소프트. https://doi.org/10.5281/zenodo.21635080 https://comcomponent.com/ko/blog/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 Model의 호출 패턴(그림)
- 3. STA(Single-Threaded Apartment)
- 4. MTA(Multi-Threaded Apartment)
- 5. STA/MTA는 어디에서 정해지는가
- 6. STA를 잘못 다루면 생기는 hang의 구체 예
- 7. 대략적인 구분
- 8. 정리
- 9. 참고 자료
COM을 쓸 때 「어느 스레드에서 동작하는가」는 피해 갈 수 없습니다. 그 중심에 있는 것이 Apartment Model(STA/MTA) 입니다. STA/MTA는 Windows의 일반적인 스레드 개념이 아니라, COM 객체의 호출 규칙을 정하기 위한 스레드 모델입니다.
이 글에서는 STA와 MTA와 COM의 관계를 그림으로 따라가면서, 「왜 hang이 나기도 하는가」까지 이어서 설명합니다.
flowchart TB
accTitle: Apartment Model의 위치
accDescr: STA/MTA는 Windows의 일반적인 스레드 개념이 아니라, COM 객체의 호출 규칙을 정하기 위한 스레드 모델인 Apartment Model의 두 형태임을 나타내는 그림.
com["COM 객체의 호출 규칙"] --> am["Apartment Model"]
am --> sta["STA"]
am --> mta["MTA"]
am -.-> note["Windows 일반 스레드 개념이 아님"]
그림 1: STA/MTA는 COM 객체의 호출 규칙을 정하는 Apartment Model의 두 형태.
이 글의 지식 맵
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 초기화를 설정하는 래퍼에 지나지 않습니다.
flowchart LR
accTitle: COM STA/MTA의 지식 맵
accDescr: STA와 MTA라는 COM의 Apartment 모델이 메시지 루프·마샬링·Proxy/Stub과 어떻게 관계되고, 메시지 루프의 부재가 STA의 행(hang)이나 데드락으로 이어지는 구조를 보여주는 그림
sta["STA(Single-Threaded Apartment)"]
mta["MTA(Multi-Threaded Apartment)"]
com["COM(컴포넌트 오브젝트 모델)"]
com_apartment_model["COM 아파트먼트 모델(STA/MTA)"]
coinitializeex["CoInitializeEx"]
message_loop["메시지 루프"]
proxy_stub["Proxy/Stub"]
thread_safe_com_object["스레드 안전 COM 개체 설계"]
windows_forms["Windows Forms"]
stathread_attribute[".NET의 [STAThread]/[MTAThread] 특성"]
threadingmodel_registry["ThreadingModel 레지스트리 값"]
com_automation["Automation"]
idispatch["IDispatch"]
midl["MIDL"]
sta_hang["STA hang(호출 전달 중단)"]
msgwaitformultipleobjects["MsgWaitForMultipleObjects"]
sta_callback_deadlock["동기 호출 중 콜백으로 인한 데드락"]
dotnet[".NET(Core 이후)"]
com_marshaling["마샬링"]
com -->|"이용한다"| com_apartment_model
com_apartment_model -->|"전제로 한다"| coinitializeex
sta -->|"전제로 한다"| message_loop
sta -.->|"이용한다"| proxy_stub
mta -->|"전제로 한다"| thread_safe_com_object
windows_forms -.->|"이용한다"| sta
windows_forms -->|"이용한다"| message_loop
stathread_attribute -->|"이용한다"| coinitializeex
threadingmodel_registry -.->|"전제로 한다"| sta
com_automation -->|"이용한다"| idispatch
proxy_stub -.->|"에서 구성할 수 있다"| midl
sta -.->|"원인이 될 수 있다"| sta_hang
message_loop -->|"방지한다"| sta_hang
msgwaitformultipleobjects -.->|"방지한다"| sta_hang
sta -.->|"원인이 될 수 있다"| sta_callback_deadlock
dotnet -.->|"원인이 될 수 있다"| sta_hang
mta -.->|"이용한다"| proxy_stub
stathread_attribute -->|"전제로 한다"| com_apartment_model
proxy_stub -->|"구현을 담당한다"| com_marshaling
com_automation -->|"구현을 담당한다"| com_marshaling
그림의 실선은 항상 성립하는 관계, 점선은 조건이 붙는 관계입니다(성립 조건은 상세 페이지의 관계별 설명에 적혀 있습니다). 관계 전체 목록(총 20건, 근거와 확신도 포함)과 주요 개념의 정의는 지식 맵 상세 페이지에 정리되어 있습니다(일본어). 데이터: JSON-LD / Turtle
1. 먼저 결론(한 마디로)
- COM 객체는 「어느 Apartment에 속하는가」로 호출 규칙이 정해진다
- STA는 스레드 하나당 Apartment 하나, MTA는 여러 스레드가 Apartment 하나라고 보면 이해하기 쉽습니다
- Apartment를 넘는 호출은 COM이 Proxy/Stub을 거쳐 마샬링합니다
flowchart TB
accTitle: STA와 MTA와 Apartment 경계를 넘는 관계
accDescr: STA는 스레드 하나당 Apartment 하나, MTA는 여러 스레드가 Apartment 하나라는 형태를 취하고, Apartment를 넘는 호출은 COM이 Proxy/Stub을 거쳐 마샬링한다는 결론을 나타내는 그림.
sta["STA(스레드 하나당 Apartment 하나)"] -->|"Apartment를 넘는 호출"| m["Proxy / Stub을 거쳐 마샬링"]
mta["MTA(여러 스레드가 Apartment 하나)"] -->|"Apartment를 넘는 호출"| m
그림 2: 소속 Apartment가 호출 규칙을 정하고, Apartment를 넘을 때만 마샬링이 끼어든다.
2. Apartment Model의 호출 패턴(그림)
COM 객체 호출에는 크게 세 가지 패턴이 있습니다.
flowchart TB
accTitle: 호출의 세 가지 패턴
accDescr: COM 객체 호출에는 같은 STA 스레드 안에서의 호출, 같은 MTA 안에서의 호출, Apartment를 넘는 호출이라는 세 가지 패턴이 있음을 나타내는 그림.
caller["COM 객체의 호출"] --> p1["패턴 1: 같은 STA 스레드 안"]
caller --> p2["패턴 2: 같은 MTA 안"]
caller --> p3["패턴 3: Apartment를 넘음"]
그림 3: 호출 패턴은 세 가지로 나뉘며, 어디에 해당하는가에 따라 오버헤드와 주의점이 달라진다.
2.1. 패턴 1: 같은 STA 스레드 안에서의 호출
같은 STA 스레드 안이라면 직접 호출할 수 있습니다. 오버헤드는 없습니다.
flowchart LR
subgraph STA[STA 스레드]
Caller[호출하는 쪽 코드]
Obj[COM 객체]
Caller -->|직접 호출| Obj
end
그림 4: 같은 STA 스레드 안의 호출은 직접 호출이 되며, 오버헤드가 없다.
2.2. 패턴 2: 같은 MTA 안에서의 호출
MTA 안의 여러 스레드에서는 어느 스레드에서든 직접 호출할 수 있습니다. 다만 객체 쪽에는 스레드 안전 설계가 필수입니다.
flowchart LR
subgraph MTA["MTA(Apartment 하나)"]
Thread1[워커 스레드 1]
Thread2[워커 스레드 2]
Obj[COM 객체]
Thread1 -->|직접 호출| Obj
Thread2 -->|직접 호출| Obj
end
그림 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 직접 파생의 커스텀 인터페이스 |
MIDL로 Proxy/Stub 생성·등록이 필요 |
즉, MIDL로 Proxy/Stub 생성이 필요해지는 것은 IDispatch를 쓰지 않고 IUnknown에서 직접 파생한 인터페이스를 만드는 경우입니다.
.NET이나 스크립트 언어에서 쓰는 일반적인 COM 컴포넌트에서는 이 작업이 필요해지는 일이 적습니다.
flowchart TB
accTitle: Proxy/Stub을 직접 준비할지 여부의 갈림
accDescr: IDispatch 기반이나 타입 라이브러리 마샬러 등 COM 표준 마샬링으로 되는 경우에는 Proxy/Stub 생성이 불필요하고, 그것으로 안 되는 IUnknown 직접 파생 커스텀 인터페이스일 때만 MIDL로 Proxy/Stub 생성과 등록이 필요해진다는 갈림을 나타내는 그림.
q{"표준 마샬링으로 되는가"} -->|"예"| auto["Proxy/Stub 생성은 불필요"]
q -->|"아니요"| midl["MIDL로 Proxy/Stub을 생성·등록"]
auto -.-> how["IDispatch나 타입 라이브러리가 처리"]
auto -.-> few["실무에서는 이쪽인 경우가 대부분"]
그림 6: Proxy/Stub을 명시적으로 생성하는 것은, 표준 마샬링으로 안 되는 IUnknown 직접 파생 커스텀 인터페이스일 때이다.
flowchart LR
subgraph STA[STA 스레드]
StaCaller[호출하는 쪽 코드]
end
subgraph RT["COM 런타임(자동)"]
Proxy[Proxy]
RPC[RPC/IPC]
Stub[Stub]
Proxy --> RPC --> Stub
end
subgraph MTA[MTA 스레드]
MtaObj[COM 객체]
end
StaCaller -->|호출| Proxy
Stub -->|전달| MtaObj
그림 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: 메모리 접근 한 번 정도
- 다른 Apartment: 시스템 콜 한 번 정도
- 다른 프로세스: 로컬 호스트로의 네트워크 통신 정도
루프에서 1만 번 호출하는 장면에서는 이 차이가 뚜렷이 나타납니다.
이 수치를 다루는 방식
위 표는 자릿수(order)의 감각을 잡기 위한 것이며, 출처가 있는 실측값이 아닙니다. 공개된 통일 벤치마크가 있는 것도 아니므로, 이 수치 자체를 설계 판단의 근거로 쓰지 마십시오.
한편 「같은 Apartment 안은 직접 호출이고, 경계를 넘으면 반드시 마샬링이 끼어든다」는 구조는 Microsoft 문서에 적힌 사양입니다. Single-Threaded Apartments 항에는, 같은 Apartment 안이라면 인터페이스 포인터를 마샬링 없이 넘길 수 있다는 것, Apartment를 넘는 경우에는 같은 프로세스 안이라도 프로세스 간과 같은 마샬링 구조를 쓴다는 것, 그리고 호출이 숨겨진 창(OleMainThreadWndClass)으로의 윈도우 메시지로 도착한다는 것이 명시되어 있습니다. 자릿수가 바뀌는 것은 이 구조 때문이며, 수치 자체는 환경과 인수의 복잡도에 따라 움직입니다.
자기 사례로 판단하려면, 같은 인터페이스를 같은 Apartment에서 호출한 경우와 다른 Apartment에서 호출한 경우로 측정해 비교하는 것이 확실합니다.
flowchart TB
accTitle: 자릿수가 바뀌는 구조상의 이유
accDescr: 같은 Apartment 안은 직접 호출인 데 비해, Apartment를 넘으면 같은 프로세스 안이라도 반드시 마샬링이 끼어들고, 목적지가 STA이면 호출이 숨겨진 창으로의 윈도우 메시지로 도착한다는, 자릿수가 바뀌는 구조상의 이유를 나타내는 그림.
same["같은 Apartment 안의 호출"] --> direct["직접 호출"]
cross["Apartment를 넘는 호출"] --> marshal["반드시 마샬링이 끼어든다"]
marshal -.-> msg["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스레드 = 1Apartment」 모델입니다.
- 그 Apartment 안의 COM 객체는 기본적으로 그 스레드에서만 실행
- 다른 스레드에서 호출하면 COM이 메시지 큐/RPC로 호출을 전달
- UI 스레드(WinForms/WPF)에서 자주 쓰인다(UI도 「thread affinity + 메시지 루프」라 궁합이 좋다)
flowchart TB
accTitle: STA의 실행 모델
accDescr: STA에서는 Apartment 안의 COM 객체가 기본적으로 생성한 스레드에서만 실행되고, 다른 스레드에서의 호출은 COM이 메시지 큐나 RPC로 그 스레드에 전달함을 나타내는 그림.
obj["STA의 COM 객체"] --> own["생성한 스레드에서만 실행"]
other["다른 스레드에서의 호출"] --> fwd["메시지 큐 / RPC로 전달"]
fwd --> own
그림 9: STA 객체는 주인 스레드에서만 움직이고, 바깥에서의 호출은 전달되어 도착한다.
3.1. UI 스레드에서 STA가 쓰이는 이유
UI 스레드와 STA는 설계가 일치하기 때문입니다.
- UI 컨트롤은 스레드 안전하지 않다 버튼이나 텍스트 박스 등은 생성한 스레드에서만 안전하게 조작할 수 있다
- STA도 마찬가지로 「thread affinity」 COM 객체는 생성한 스레드에서만 직접 실행된다
- UI 스레드는 반드시 메시지 루프를 돌린다 윈도우 이벤트를 처리하기 위해 필수. STA의 전제(메시지 펌프)와 일치한다
그래서 WinForms/WPF의 UI 스레드는 기본값이 STA입니다.
flowchart TB
accTitle: UI 스레드와 STA 설계의 일치
accDescr: UI 컨트롤은 생성한 스레드에서만 안전하게 조작할 수 있고, STA도 thread affinity이며, UI 스레드가 반드시 돌리는 메시지 루프는 STA의 전제인 메시지 펌프와 일치하므로 UI 스레드는 기본값이 STA가 됨을 나타내는 그림.
ui["UI 컨트롤의 thread affinity"] --> match["설계가 일치"]
sta["STA의 thread affinity"] --> match
loop["UI 스레드의 메시지 루프"] --> pre["STA의 전제(메시지 펌프)"]
pre --> match
match --> def["UI 스레드는 기본값이 STA"]
그림 10: thread affinity와 메시지 루프라는 두 점에서 설계가 일치하므로, UI 스레드는 STA가 되어 있다.
포인트: STA는 thread affinity가 높은 대신, 호출하는 쪽이 많으면 혼잡해지기 쉽다.
4. MTA(Multi-Threaded Apartment)
MTA는 「여러 스레드가 1Apartment」 모델입니다.
- COM 객체는 여러 스레드에서 동시에 호출된다
- 객체 쪽에 스레드 안전 설계가 필수
- 서버 사이드 처리나 백그라운드 처리에 맞다
포인트: MTA는 병렬성이 높지만, 객체 구현의 책임이 무겁다.
flowchart TB
accTitle: MTA의 실행 모델
accDescr: MTA는 여러 스레드가 Apartment 하나를 공유하고, COM 객체는 여러 스레드에서 동시에 호출되므로 객체 쪽에 스레드 안전 설계가 필수가 됨을 나타내는 그림.
mta["MTA(여러 스레드가 1Apartment)"] --> par["여러 스레드에서 동시에 호출된다"]
par --> safe["객체 쪽의 스레드 안전 설계가 필수"]
mta -.-> use["서버 사이드나 백그라운드 처리에 맞다"]
그림 11: MTA는 병렬로 호출할 수 있는 대신, 안전성의 책임이 객체 구현 쪽으로 옮겨 간다.
5. STA/MTA는 어디에서 정해지는가
COM의 Apartment는 스레드마다 초기화해서 정해집니다.
CoInitialize/CoInitializeEx를 호출한 순간에 그 스레드의 Apartment가 정해진다- STA:
COINIT_APARTMENTTHREADED - MTA:
COINIT_MULTITHREADED
flowchart TB
accTitle: Apartment가 정해지는 순간
accDescr: 스레드가 CoInitialize 또는 CoInitializeEx를 호출한 순간에 그 스레드의 Apartment가 정해지며, COINIT_APARTMENTTHREADED이면 STA, COINIT_MULTITHREADED이면 MTA가 됨을 나타내는 그림.
th["스레드"] --> init["CoInitialize / CoInitializeEx를 호출"]
init -->|"COINIT_APARTMENTTHREADED"| sta["STA로 정해진다"]
init -->|"COINIT_MULTITHREADED"| mta["MTA로 정해진다"]
그림 12: Apartment는 스레드마다 초기화를 호출한 순간의 지정으로 정해진다.
5.1. .NET에서의 STA/MTA
.NET에도 [STAThread] / [MTAThread] 속성과 ApartmentState가 있지만, 이들은 COM의 Apartment Model을 설정하기 위한 래퍼입니다.
[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를 바꿀 수는 없습니다. 최초 초기화가 전부입니다.
flowchart TB
accTitle: .NET에서 Apartment를 설정하는 위치
accDescr: Main 메서드에는 STAThread나 MTAThread 속성을 붙이고, 추가로 만드는 스레드에는 시작 전에 Thread.SetApartmentState로 설정하며, 어느 쪽이든 최초 초기화로 Apartment가 확정되어 나중에 바꿀 수 없음을 나타내는 그림.
main["Main 메서드"] --> attr["〔STAThread〕/〔MTAThread〕 속성"]
newth["추가로 만드는 스레드"] --> setap["시작 전에 Thread.SetApartmentState"]
attr --> fixed["최초 초기화로 Apartment가 확정"]
setap --> fixed
fixed -.-> nochange["나중에 바꿀 수 없다"]
그림 13: 엔트리 포인트는 속성, 추가 스레드는 SetApartmentState로, 어느 쪽이든 첫 한 번에 확정된다.
6. STA를 잘못 다루면 생기는 hang의 구체 예
다음 같은 구성은 실제로 hang이 나기 쉽습니다.
6.1. 흔히 있는 상황
- 백그라운드에서 STA 스레드를 만들어 COM 객체를 생성
- 그 스레드는 메시지 루프를 돌리지 않는다
- 다른 스레드(STA/MTA를 가리지 않음)에서 그 COM 객체를 호출
flowchart TB
accTitle: hang이 나기 쉬운 구성
accDescr: 백그라운드에서 만든 STA 스레드가 COM 객체를 생성했는데 메시지 루프를 돌리지 않고, 그쪽으로 다른 스레드에서 그 COM 객체를 호출하는, hang이 나기 쉬운 구성을 나타내는 그림.
bg["백그라운드의 STA 스레드"] --> gen["COM 객체를 생성"]
bg --> noloop["메시지 루프를 돌리지 않는다"]
other["다른 스레드(STA / MTA를 가리지 않음)"] -->|"호출한다"| gen
그림 14: 「루프를 돌리지 않는 STA가 가진 객체를, 다른 스레드에서 호출한다」는 조합이 위험하다.
6.2. 무엇이 일어나는가
hang의 이유는 STA의 두 전제로 모입니다. 이 절이 이유 설명이고, 이후 절에서는 반복하지 않습니다.
- COM 객체는 생성한 STA 스레드에서 처리된다
호출하는 쪽이 STA이든 MTA이든, 다른 스레드에서의 호출은 반드시 그 STA 스레드로 전달됩니다. 전달은 COM이 그 Apartment에 만드는 숨겨진 창(윈도우 클래스
OleMainThreadWndClass)으로의 윈도우 메시지로 도착합니다 - 그 전달을 받으려면 STA 스레드가 메시지 펌프를 돌리고 있어야 한다 Microsoft 문서에도 「각 STA는 다른 프로세스나 같은 프로세스 안의 다른 Apartment에서의 호출을 처리하기 위해 메시지 루프를 가져야 한다」고 명시되어 있습니다
따라서 메시지를 돌리지 않는 STA 스레드는 호출을 받지 못하고, 호출하는 쪽은 답을 계속 기다리며, 결과적으로 hang이 됩니다.
flowchart TB
accTitle: hang에 이르는 흐름
accDescr: 다른 스레드에서의 호출은 생성한 STA 스레드로 숨겨진 창으로의 윈도우 메시지로 전달되지만, STA 스레드가 메시지 펌프를 돌리지 않으면 받지 못해 호출하는 쪽이 답을 계속 기다리며 hang이 되는 흐름을 나타내는 그림.
caller["다른 스레드에서의 호출"] --> fwd["생성한 STA 스레드로 전달"]
fwd -.-> hidden["숨겨진 창으로의 메시지로 도착"]
fwd --> nopump["메시지 펌프가 돌지 않는다"]
nopump --> norecv["STA 쪽이 호출을 받지 못한다"]
norecv --> wait["호출하는 쪽은 답을 계속 기다린다"]
wait --> hang["hang"]
그림 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 쪽은 메시지를 처리하지 않으므로, 여기서 hang이 나기 쉽다
dynamic obj = comObj;
obj.AnyMethod();
}
finally
{
// 호출이 돌아온 경우 ── agile한 클래스였거나,
// 호출이 전달되지 않았거나, AnyMethod가 바로 돌아온 경우 ── 라도
// 반드시 STA 스레드를 해제한다. 여기를 빼면 done을 기다리는
// 포그라운드 스레드가 남아, 「재현되지 않은」 때도
// 프로세스가 끝나지 않는다. 재현 여부를 증상으로 구별할 수 없게 된다
done.Set();
staThread.Join();
}
}
}
이 코드는 ThreadingModel=Apartment(= STA)로 등록된 COM 클래스의 ProgID를 넘기는 전제입니다. AnyMethod는 그 클래스가 실제로 가진 메서드 이름으로 바꾸십시오. 어떤 COM 클래스에서든 재현되는 것은 아닙니다. 재현에 필요한 조건은 다음 세 가지입니다.
| 조건 | 확인 방법 |
|---|---|
대상 클래스의 ThreadingModel이 Apartment |
레지스트리 HKEY_CLASSES_ROOT\CLSID\{CLSID}\InprocServer32의 ThreadingModel 값을 봅니다. Both나 Free이면 호출이 전달되지 않아 재현되지 않습니다 |
| STA 스레드가 메시지를 돌리지 않는다 | 위 예의 done.WaitOne()이 그에 해당합니다 |
| 호출을 다른 스레드에서 한다 | 같은 STA 스레드 안에서 호출하는 한 직접 호출이 되므로 hang이 나지 않습니다 |
obj.AnyMethod()를 try / finally로 감싸고 done.Set()과 staThread.Join()을 반드시 통과시키는 것은, 이 「재현되지 않는 경우」를 구별하기 위해서입니다. STA 스레드는 기본이 포그라운드 스레드이므로, done이 설정되지 않는 한 프로세스는 끝나지 않습니다. finally를 빼면 재현된 때(호출이 돌아오지 않음)와 재현되지 않은 때(호출은 돌아왔지만 done을 아무도 세우지 않음)에서, 밖에서 보이는 증상이 둘 다 「프로세스가 끝나지 않는다」가 됩니다. 재현 조건을 확인하는 도구가 조건이 맞았는지 틀렸는지를 가려 버리는 셈입니다. 위 형태라면 재현되지 않은 경우에는 그대로 정상 종료합니다.
멈춘 것의 확인은, 디버거로 프로세스를 일시 정지하고 호출하는 쪽 스레드의 스택이 COM 대기에서 멈춰 있는 것과 STA 스레드가 WaitOne에서 멈춰 있는 것을 보는 편이 빠릅니다.
sequenceDiagram
participant Main as 메인 스레드
participant STA as STA 스레드
participant COM as COM 런타임
Main->>STA: 스레드 시작
STA->>STA: CoInitializeEx(STA)
STA->>STA: COM 객체 생성
STA->>Main: ready.Set()
STA->>STA: done.WaitOne()으로 대기
Note over STA: 메시지 루프 없음<br/>여기서 막혀 있다
Main->>COM: CallComObject()
COM->>STA: 호출을 전달하려 한다
Note over COM: 메시지로 전달하지만...
Note over STA: WaitOne 중이므로<br/>메시지를 처리하지 못한다
Note over Main: 호출하는 쪽도 계속 기다린다
Note over Main,STA: 양쪽이 대기 상태 → hang
그림 16: WaitOne으로 멈춘 STA 스레드와, 전달의 답을 기다리는 메인 스레드가 서로 진행하지 못하게 된다.
그림 가운데 「메시지로 전달하지만…」의 두 줄이, 6.2에서 쓴 두 전제가 무너지는 지점입니다.
6.4. 회피의 요점
- 다른 스레드에서의 호출을 받는 경우, STA 스레드는 메시지 루프를 돌려야 한다
- 가능하면 UI 스레드에서 생성·사용한다(UI 스레드는 처음부터 메시지 루프가 있다)
- STA가 필요 없으면 처음부터 MTA로 둔다
보충: 같은 스레드 안에서만 끝나면, 항상 Application.Run()이 필요한 것은 아닙니다.
다만 UI 계열·COM 계열은 다른 스레드에서의 호출이 얽히는 경우가 많아서, 실무상으로는 거의 필수입니다.
flowchart TB
accTitle: hang 회피의 세 방향
accDescr: 다른 스레드에서의 호출을 받으면 STA 스레드에서 메시지 루프를 돌리고, 가능하면 처음부터 메시지 루프를 가진 UI 스레드에서 생성·사용하고, STA가 필요 없으면 처음부터 MTA로 둔다는 세 가지 회피 방향을 나타내는 그림.
q["STA의 hang을 어떻게 피할 것인가"] --> a1["STA 스레드에서 메시지 루프를 돌린다"]
q --> a2["UI 스레드에서 생성·사용한다"]
q --> a3["STA가 필요 없으면 처음부터 MTA"]
a2 -.-> why["UI 스레드는 처음부터 루프가 있다"]
그림 17: 회피책은 루프를 돌린다·루프가 있는 곳으로 모은다·STA를 그만둔다, 세 방향으로 정리할 수 있다.
6.5. 「메시지 루프를 돌린다」는 결국 무엇인가?
Win32 UI 스레드가 하는, 그 익숙한 이것입니다.
while (GetMessage(out var msg, IntPtr.Zero, 0, 0))
{
TranslateMessage(ref msg);
DispatchMessage(ref msg);
}
STA에서는 다른 스레드에서의 호출이 「전달」되어 옵니다. 그 전달을 받아 실행으로 넘기는 것이 이 루프(메시지 펌프)라는 이야기입니다.
flowchart TB
accTitle: 메시지 펌프의 역할
accDescr: 다른 스레드에서 전달된 호출을 GetMessage로 받고, DispatchMessage로 실hang에 넘기는 반복이 메시지 펌프임을 나타내는 그림.
fwd["전달되어 온 호출"] --> gm["GetMessage로 받는다"]
gm --> dm["DispatchMessage로 실hang에 넘긴다"]
dm --> gm
그림 18: 「메시지 루프를 돌린다」는 것은, 전달을 받아 실hang에 넘기는 이 반복을 말한다.
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 호출도 놓치지 않는」 형태로 만들고 싶을 때의 정석입니다.
flowchart TB
accTitle: 백그라운드 STA에서 루프를 돌리는 세 수단
accDescr: 백그라운드 STA에서 메시지 루프를 돌리려면 WinForms 참조가 필요한 Application.Run을 쓰거나, GetMessage와 DispatchMessage 루프를 직접 쓰거나, 메시지와 동기 객체를 둘 다 기다릴 수 있는 MsgWaitForMultipleObjects를 쓴다는 세 수단이 있음을 나타내는 그림.
want["백그라운드 STA에서 루프를 돌린다"] --> ar["Application.Run"]
want --> gml["GetMessage 루프를 직접 쓴다"]
want --> mw["MsgWaitForMultipleObjects"]
ar -.-> dep["WinForms 참조와 종료 수단이 필요"]
mw -.-> both["메시지와 동기 객체를 둘 다 기다릴 수 있다"]
그림 19: 루프를 돌리는 방법은 세 가지이며, 종료 방식과 의존의 무게로 고르게 된다.
6.7. 또 하나의 hang 예: 동기 호출 중의 콜백
STA는 「호출이 전달된다」뿐 아니라, 상황에 따라서는 반대 방향(서버→클라이언트)으로 콜백이 옵니다. 그중에서도 동기 호출 중에 콜백이 발생하는 패턴은 데드락의 정석입니다.
sequenceDiagram
participant UI as UI 스레드(STA)
participant Server as COM 서버
UI->>Server: DoWork()(동기 호출)
Note over UI: DoWork의 반환을 기다리는 중<br/>(메시지를 처리하지 않음)
Server->>UI: ProgressCallback()(콜백)
Note over UI: 대기 중이므로<br/>콜백을 받을 수 없다
Note over Server: 콜백 완료를 기다리는 중
Note over UI,Server: 서로가 상대를 기다린다 → 데드락
그림 20: 동기 호출의 반환을 기다리는 UI 스레드와, 콜백 완료를 기다리는 서버가 서로를 기다린다.
왜 데드락이 되기 쉬운가:
- UI 스레드가
DoWork()를 동기 호출(블로킹) - UI 스레드는 반환을 기다리는 중(메시지를 처리하지 않음)
- 서버가
ProgressCallback()을 UI 스레드에 보낸다 - UI 스레드는 대기 중이므로 콜백을 받을 수 없다
- 서버는 콜백 완료를 기다리고 있다
- 서로가 상대를 기다린다 → 영원히 진행되지 않는다
처리 시간의 길이는 관계없습니다. 동기 호출 중에 콜백이 온다는 패턴 자체가 문제가 되기 쉽습니다.
보충: COM에는 상황에 따라 메시지를 돌리거나 재진입하는 구조도 있어, 컴포넌트나 호출 형태에 따라 동작이 달라집니다. 반드시 데드락이 되는 것은 아니지만, 이 패턴은 피하는 편이 무난합니다.
7. 대략적인 구분
| 상황 | 고르는 것 | 판단 이유 | 함께 필요한 것 |
|---|---|---|---|
| UI가 얽힘(WinForms / WPF) | STA | UI 컨트롤이 thread affinity이고, 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는 최초 초기화로 확정되고 나중에 바꿀 수 없으므로, 여기만은 구현을 시작하기 전에 정해 둡니다.
flowchart LR
accTitle: 헤맬 때 보는 순서
accDescr: STA와 MTA 구분에 헤맬 때는 쓰는 COM 컴포넌트 쪽의 요구, UI 유무, 병렬성 순으로 확인하면 정해지기 쉬움을 나타내는 그림.
p1["상대의 요구"] -->|"다음에"| p2["UI 유무"]
p2 -->|"다음에"| p3["병렬성"]
그림 21: 구분에 헤매면 상대의 요구·UI 유무·병렬성 순으로 확인한다.
8. 정리
STA/MTA는 COM을 위한 스레드 모델이며, STA는 1스레드 = 1Apartment, MTA는 여러 스레드가 1Apartment라는 형태를 취합니다. Apartment를 넘는 호출은 COM이 Proxy/Stub을 거쳐 전달해 줍니다(표준 인터페이스 이외는 MIDL 등으로 생성·등록이 필요)만, 거기에는 마샬링 오버헤드가 따르므로 고빈도 호출이 예상되는 장면에서는 Apartment 설계를 신중히 정하고 싶습니다.
hang의 관점에서는 「다른 스레드에서의 호출을 받는 STA 스레드는 메시지 펌프를 돌리는 것이 전제」라는 한 점으로 모입니다. 메시지를 돌리지 않는 STA 스레드에 호출하면 hang이 나기 쉽고, 동기 호출 중에 콜백이 오는 패턴도 데드락이 되기 쉽습니다. UI 스레드는 「thread affinity」과 「메시지 루프」를 처음부터 가지고 있어 이 전제를 추가 구현 없이 충족하므로, 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
관련 기사
같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.
클립보드와 드래그 앤 드롭의 구조 ── 업무 앱에서 OLE 데이터 전송을 올바르게 다루기
Excel 표를 붙여 넣으면 서식이 무너지고, 원본을 닫으면 붙여 넣을 수 없다── 원인은 같은 내용을 여러 형식으로 두는 클립보드입니다. 표준 형식·지연 렌더링·OLE 드래그 앤 드롭부터 기록과 클라우드 동기 정책까지 설명합니다.
Windows 셸 통합의 현재 ── 컨텍스트 메뉴, 파일 연결, Windows 11의 변화
Windows 11에서 컨텍스트 메뉴가 「더 많은 옵션 표시」 뒤로 숨는 이유를, 확장자→ProgID→verb라는 파일 연결의 기본, 기존형 셸 확장의 주의점, IExplorerCommand와 MSIX/sparse package의 새 방식까지 이...
Windows on Arm에서 업무 앱은 동작하는가 ── x64 에뮬레이션(Prism)과 네이티브 DLL·COM의 현실
「Windows on Arm에서 업무 앱은 동작하는가」에 개발자와 사내 IT를 위해 답합니다. x64 에뮬레이션(Prism)의 구조, 드라이버처럼 동작하지 않는 계층, .NET의 AnyCPU와 P/Invoke 문제, Arm 대응 체크리스트까지 정...
Windows 앱 외주·수탁 개발을 의뢰하기 전에 정리할 점
Windows 앱 외주·수탁 개발을 의뢰하기 전에, 기존 소프트웨어 수정, 장치 연동, COM/ActiveX, 배포·업데이트, 유지보수를 정리하는 포인트를 설명합니다.
COM/OCX/ActiveX 개발에서 막히기 쉬운 등록과 bitness의 함정
COM, OCX, ActiveX 개발에서 자주 막히는 32bit/64bit, Visual Studio 2022, regsvr32/Regasm, 관리자 권한, HKCR, STA/MTA를 실무 관점에서 정리합니다.
관련 토픽
이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.
Windows 기술 토픽
Windows 개발, 장애 조사, 기존 자산 활용에 관한 KomuraSoft LLC 기사를 모은 토픽 허브입니다.
ActiveX 이관
COM / ActiveX / OCX 자산을 유지할지, 감쌀지, 교체할지의 단계적 판단을 정리한 토픽 페이지입니다.
UI 스레드 & 타이머
WPF / WinForms UI 스레드, async 흐름, Dispatcher 사용, 타이머 판단을 정리한 토픽 페이지입니다.
이 주제와 연결되는 서비스
이 기사는 다음 서비스 페이지로 이어집니다. 가까운 입구부터 확인해 주세요.
기술 상담 & 설계 리뷰
STA / MTA, 메시지 루프, 마샬링을 정리하는 일은 구현 전의 책임 분할이나 스레드 경계 리뷰와 바로 이어집니다.
기존 자산 활용 & 이관 지원
COM을 포함한 기존 자산을 다룰 때 피해 가기 어려운 기초이므로, 기존 자산 활용·이전 지원 상담과도 잘 맞습니다.
자주 묻는 질문
이 기사 주제에 대해 상담 시 자주 나오는 질문을 모았습니다.
- STA와 MTA 중 어느 쪽을 골라야 하나요?
- UI가 얽히는 처리라면 STA, 대량 병렬 처리라면 MTA가 기본적인 구분입니다. STA는 스레드 하나당 Apartment 하나라 thread affinity가 높은 반면, 호출자가 많으면 혼잡해지기 쉽습니다. MTA는 여러 스레드가 Apartment 하나를 공유하므로 병렬성은 높지만, COM 객체 쪽에 스레드 안전 설계가 필수입니다. 어느 쪽도 아니면, 쓰는 기존 라이브러리나 COM 서버의 요구에 맞추는 편이 현실적입니다.
- UI 스레드는 왜 STA인가요?
- UI 스레드와 STA의 설계가 일치하기 때문입니다. 버튼이나 텍스트 박스 같은 UI 컨트롤은 스레드 안전하지 않아, 생성한 스레드에서만 안전하게 조작할 수 있습니다. STA도 마찬가지로 thread affinity 모델입니다. 게다가 UI 스레드는 윈도우 이벤트 처리를 위해 반드시 메시지 루프를 돌리므로, STA의 전제인 메시지 펌프를 추가 구현 없이 충족합니다. 이 때문에 WinForms/WPF의 UI 스레드는 기본값이 STA입니다.
- STA의 COM 객체를 호출하면 hang이 나는 이유는 무엇인가요?
- STA COM 객체에 대한 호출은 생성한 STA 스레드에서 처리됩니다. 다른 스레드에서의 호출은 COM이 메시지/RPC로 전달하지만, STA 스레드가 메시지 루프를 돌리지 않으면 전달을 받지 못해 호출자가 계속 기다리며 hang이 됩니다. 피하려면 다른 스레드에서 호출되는 STA 스레드에서 메시지 루프를 돌리거나, UI 스레드에서 생성·사용하거나, STA가 필요 없으면 처음부터 MTA로 두면 됩니다.
- .NET의 [STAThread] 속성은 무엇을 위한 것인가요?
- COM의 Apartment Model을 설정하기 위한 래퍼입니다. Main 메서드에 붙이면 COM을 쓸 때 그 스레드가 STA로 초기화됩니다. 다만 실제로 COM을 호출하기 전에는 초기화되지 않으며, COM을 쓰지 않는 앱에서는 효과가 없습니다. 추가로 만드는 스레드에는 적용되지 않으므로, 스레드 시작 전에 Thread.SetApartmentState로 설정합니다. Apartment는 최초 초기화로 정해지며 나중에 바꿀 수 없다는 점도 주의가 필요합니다.