COM STA/MTA 기초 - 스레드 모델과 행(hang)을 피하는 사고방식
COM STA/MTA 기초 - 스레드 모델과 행(hang)을 피하는 사고방식
COM의 STA/MTA는 Windows 개발이나 .NET에서 COM을 만질 때 피하기 어려운 기초 지식입니다.
특히 검색에 자주 나오는 질문은 UI 스레드가 왜 STA인지, Apartment를 넘나들면 무엇이 일어나는지, 왜 행이 발생하는지입니다.
목차
- 먼저 결론(한마디로)
- Apartment 모델의 호출 패턴(그림)
- STA(Single-Threaded Apartment)
- MTA(Multi-Threaded Apartment)
- STA/MTA는 어디서 결정되는가
- STA를 잘못 다루면 일어나는 행의 구체적 예
- 대략적인 사용 분류
- 정리
- 참고 자료
COM을 사용할 때 “어느 스레드에서 동작하는가”는 피해 갈 수 없습니다.
그 중심에 있는 것이 Apartment 모델(STA/MTA) 입니다.
STA/MTA는 COM을 위한 스레드 모델입니다.
Windows의 일반적인 스레드 개념이 아니라, COM 객체의 호출 규칙을 결정하는 구조입니다.
이 글에서는 STA와 MTA, COM의 관계를 그림으로 정리하고, “왜 행이 일어나는가” 까지 이어서 설명합니다.
1. 먼저 결론(한마디로)
- COM 객체는 “어느 Apartment에 속하는가”로 호출 규칙이 결정된다
- STA는 1 스레드 = 1 Apartment, MTA는 여러 스레드 = 1 Apartment 로 이해하면 편합니다
- Apartment를 넘나드는 호출은 COM이 Proxy/Stub을 거쳐 마샬링 합니다
2. Apartment 모델의 호출 패턴(그림)
COM 객체의 호출에는 크게 세 가지 패턴이 있습니다.
2.1. 패턴1: 동일 STA 스레드 안에서의 호출
같은 STA 스레드 안이라면 직접 호출이 가능합니다. 오버헤드는 없습니다.
flowchart LR
subgraph STA[STA 스레드]
Caller[호출 코드]
Obj[COM 객체]
Caller -->|직접 호출| Obj
end
2.2. 패턴2: 동일 MTA 안에서의 호출
MTA 내의 여러 스레드로부터 어느 스레드에서든 직접 호출할 수 있습니다.
다만 객체 쪽은 스레드 안전 설계가 필수 입니다.
flowchart LR
subgraph MTA[MTA(하나의 Apartment)]
Thread1[워커 스레드 1]
Thread2[워커 스레드 2]
Obj[COM 객체]
Thread1 -->|직접 호출| Obj
Thread2 -->|직접 호출| Obj
end
2.3. 패턴3: Apartment를 넘는 호출
다른 Apartment 사이에서는 COM이 Proxy/Stub을 통해 전달합니다.
표준 인터페이스라면 COM 런타임이 알아서 처리해 줍니다.
주의: Proxy/Stub은 “무엇이든 자동 생성되는” 것은 아닙니다.
다만 실무에서는 대개 명시적 생성이 필요 없습니다.
| 패턴 | Proxy/Stub 준비 |
|---|---|
IDispatch 기반(Automation) |
불필요. oleaut32.dll이 처리 |
| 타입 라이브러리 등록됨 | 불필요. 타입 라이브러리 마샬러가 처리 |
| .NET COM Interop | 보통 불필요. 타입 라이브러리 경유로 동작 |
IUnknown 직접 파생 커스텀 IF |
MIDL로 Proxy/Stub 생성·등록 필요 |
즉, MIDL로 Proxy/Stub 생성이 필요해지는 것은, IDispatch를 쓰지 않고 IUnknown 직접 파생 인터페이스를 만드는 경우 입니다.
.NET이나 스크립트 언어에서 쓰는 일반적인 COM 컴포넌트에서는 이 작업이 필요해지는 경우가 드뭅니다.
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
포인트:
Apartment를 넘으면 마샬링 오버헤드가 발생합니다.
고빈도 호출에서는 성능에 영향을 주므로 설계 시점에 고려가 필요합니다.
2.4. 마샬링 오버헤드의 기준
아래는 일반적인 기준값입니다(실측이 아니며 상황과 파라미터 복잡도에 따라 크게 달라집니다).
| 호출 패턴 | 대략의 시간 | 상대적 감각 |
|---|---|---|
| 동일 Apartment 내(직접) | 10~100 나노초 | 일반 함수 호출과 거의 동등 |
| 다른 Apartment(동일 프로세스) | 1~10 마이크로초 | 직접 호출의 100~1000배 |
| 다른 프로세스(Out-of-proc) | 100~1000 마이크로초 | 직접 호출의 1만~10만 배 |
상대 비교:
- 동일 Apartment: 1회 메모리 접근 수준
- 다른 Apartment: 1회 시스템 호출 수준
- 다른 프로세스: 로컬 호스트 네트워크 통신 수준
루프에서 1만 번 호출하는 장면에서는 이 차이가 두드러집니다.
3. STA(Single-Threaded Apartment)
STA는 “1 스레드 = 1 Apartment” 모델입니다.
- 해당 Apartment 내의 COM 객체는 원칙적으로 그 스레드에서만 실행
- 다른 스레드에서 호출하면 COM이 메시지 큐/RPC 경유로 전달
- UI 스레드(WinForms/WPF)에서 자주 쓰임(UI도 “1 스레드 친화성 + 메시지 루프”이므로 궁합이 좋음)
3.1. 왜 UI 스레드에서 STA가 쓰이는가
UI 스레드와 STA는 설계가 일치하기 때문입니다.
- UI 컨트롤은 스레드 안전하지 않다
버튼이나 텍스트 박스는 생성한 스레드에서만 안전하게 조작 가능 - STA도 “1 스레드 친화성”
COM 객체는 생성한 스레드에서만 직접 실행됨 - UI 스레드는 반드시 메시지 루프를 돌린다
윈도우 이벤트 처리를 위해 필수. STA의 전제(메시지 펌프)와 일치
그래서 WinForms/WPF의 UI 스레드는 기본값이 STA 입니다.
포인트:
STA는 스레드 친화성이 높은 대신 호출자가 많으면 병목이 되기 쉽다 는 특성이 있습니다.
4. MTA(Multi-Threaded Apartment)
MTA는 “여러 스레드 = 1 Apartment” 모델입니다.
- COM 객체가 여러 스레드에서 동시에 호출됨
- 객체 쪽에서 스레드 안전 설계가 필수
- 서버 사이드 처리나 백그라운드 처리에 적합
포인트:
MTA는 병렬성이 높지만 객체 구현의 책임이 무겁다.
5. STA/MTA는 어디서 결정되는가
COM의 Apartment는 스레드마다 초기화함으로써 결정됩니다.
CoInitialize/CoInitializeEx를 호출한 순간, 그 스레드의 Apartment가 정해진다- STA:
COINIT_APARTMENTTHREADED - MTA:
COINIT_MULTITHREADED
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 그 자체 입니다.
.NET 고유의 스레드 모델이 아니라 COM Interop을 위한 장치입니다.
중요:
나중에 Apartment를 바꿀 수는 없습니다. 최초 초기화가 전부 입니다.
6. STA를 잘못 다루면 일어나는 행의 구체적 예
다음과 같은 구성은 실제로 행을 일으키기 쉽습니다.
6.1. 자주 있는 상황
- 백그라운드에서 STA 스레드를 만들고 COM 객체를 생성
- 그 스레드는 메시지 루프를 돌리지 않고 있다
- 다른 스레드(STA/MTA 무관)에서 그 COM 객체를 호출
6.2. 무엇이 일어나는가
STA의 COM 객체는 호출을 그 STA 스레드에서 처리해야 합니다.
호출자가 STA든 MTA든, 다른 스레드라면 COM이 메시지/RPC로 전달 합니다.
하지만 STA 스레드가 메시지를 처리하지 않는 상태 라면, 호출은 계속 대기하게 되고 결과적으로 행 이 됩니다.
6.3. 의사 코드(전형적인 실패 패턴)
var ready = new AutoResetEvent(false);
var done = new AutoResetEvent(false);
object comObj = null;
var staThread = new Thread(() =>
{
// STA로 초기화
CoInitializeEx(IntPtr.Zero, COINIT_APARTMENTTHREADED);
comObj = new SomeStaComObject();
ready.Set();
// 메시지 루프 없이 대기 -> 여기가 치명타
done.WaitOne();
});
staThread.SetApartmentState(ApartmentState.STA);
staThread.Start();
ready.WaitOne();
// 다른 스레드(STA/MTA 무관)에서 부르면 호출이 STA로 전달된다
// 그러나 STA 쪽은 메시지를 처리하지 않으므로 여기서 행이 되기 쉽다
CallComObject(comObj);
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: 메시지 루프 없음
여기서 막혀 있음
Main->>COM: CallComObject()
COM->>STA: 호출을 전달하려 함
Note over COM: 메시지로 전달하지만...
Note over STA: WaitOne 중이라
메시지를 처리할 수 없음
Note over Main: 호출자도 계속 대기
Note over Main,STA: 양쪽이 대기 상태 → 행
요컨대:
여기서 말하는 “전제”는 “STA에서 다른 스레드 호출이 행이 되는 이유”를 설명하기 위한 전제입니다.
STA의 전제는 다음 두 가지 입니다.
- COM 객체는 생성한 STA 스레드에서 처리된다
다른 스레드로부터의 호출은 반드시 그 STA 스레드로 전달된다 - 그 전달을 받기 위해 STA 스레드는 메시지 펌프를 돌린다
돌리지 않으면 호출을 받을 수 없다
그래서,
- 메시지를 돌리지 않는 STA 스레드는 호출을 받을 수 없다
- 받을 수 없기 때문에 호출자가 계속 기다리고, 결과적으로 행 이 된다
한편 UI 스레드는 윈도우 이벤트 처리를 위해 처음부터 메시지 루프를 돌리고 있으므로, STA의 요건을 추가 구현 없이 충족합니다.
그래서 UI 스레드는 STA COM 객체를 돌릴 장소로 자연스러운 선택지가 됩니다.
6.4. 회피의 요점
- 다른 스레드로부터의 호출을 받는 경우, STA 스레드는 메시지 루프를 돌려야 한다
- 가능하면 UI 스레드 위에서 생성·사용한다(UI 스레드는 처음부터 메시지 루프가 있음)
- STA가 필요 없다면 처음부터 MTA로 한다
보충: 같은 스레드 안에서만 완결된다면, 반드시 Application.Run()이 필요한 것은 아닙니다.
다만 UI 계열·COM 계열은 다른 스레드로부터의 호출이 얽히는 경우가 많아, 실무상 거의 필수입니다.
6.5. “메시지 루프를 돌린다”가 결국 무엇인가
Win32의 UI 스레드가 하고 있는, 그 유명한 패턴입니다.
while (GetMessage(out var msg, IntPtr.Zero, 0, 0))
{
TranslateMessage(ref msg);
DispatchMessage(ref msg);
}
STA에서는 다른 스레드로부터의 호출이 “전달” 되어 옵니다.
그 전달을 받아서 실행에 돌리는 것이 이 루프(메시지 펌프)의 역할입니다.
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 호출을 잊으면 바로 사고가 납니다)
6.7. 또 하나의 행 예: 동기 호출 중의 콜백
STA는 “호출이 전달되어 오는” 것뿐 아니라, 상황에 따라서는 역방향(서버 → 클라이언트)으로 콜백이 옵니다.
동기 호출 중에 콜백이 발생하는 패턴은 데드락의 원인이 되기 쉽습니다.
sequenceDiagram
participant UI as UI 스레드(STA)
participant Server as COM 서버
UI->>Server: DoWork()(동기 호출)
Note over UI: DoWork 반환을 기다리는 중
(메시지 처리 안 함)
Server->>UI: ProgressCallback()(콜백)
Note over UI: 대기 중이라
콜백을 받을 수 없음
Note over Server: 콜백 완료를 기다리는 중
Note over UI,Server: 서로가 상대를 기다림 → 데드락
왜 데드락이 되기 쉬운가:
- UI 스레드가
DoWork()를 동기 호출(블로킹) - UI 스레드는 반환을 기다리는 중(메시지 처리 안 함)
- 서버가
ProgressCallback()을 UI 스레드에 보낸다 - UI 스레드는 대기 중이라 콜백을 받을 수 없다
- 서버는 콜백 완료를 기다리고 있다
- 서로가 상대를 기다림 → 영원히 진행 안 됨
처리 시간의 길이는 관계없습니다. 동기 호출 중에 콜백이 오는 패턴 자체가 문제가 되기 쉽습니다.
보충: COM에는 상황에 따라 메시지를 돌리거나 재진입하는 구조도 있고, 컴포넌트나 호출 형태에 따라 동작이 달라집니다.
반드시 데드락이 되는 것은 아니지만, 이 패턴은 피하는 것이 무난합니다.
7. 대략적인 사용 분류
- UI가 얽힘 → STA
- 대량 병렬 처리 → MTA
- 어느 쪽도 아니다 → 기존 라이브러리나 COM 서버의 요구에 맞춘다
8. 정리
STA/MTA란:
- STA/MTA는 COM을 위한 스레드 모델 (Windows의 일반 스레드 개념이 아님)
- STA는 1 스레드 = 1 Apartment, MTA는 여러 스레드 = 1 Apartment
- Apartment를 넘으면 COM이 Proxy/Stub 경유로 전달 한다(표준 IF 외에는 MIDL 등으로 생성·등록 필요)
STA의 전제와 함정:
- 다른 스레드로부터의 호출을 받는 경우, STA는 메시지 펌프를 돌리는 것이 전제
- 메시지를 안 돌리는 STA 스레드에 호출하면 행이 되기 쉽다
- 동기 호출 중에 콜백이 오는 패턴은 데드락이 되기 쉽다
UI 스레드와 STA의 관계:
- UI 스레드는 “1 스레드 친화성”과 “메시지 루프”를 처음부터 가진다
- 그래서 STA의 요건을 추가 구현 없이 충족하고, STA COM과 궁합이 좋다
설계 시 주의:
- Apartment를 넘는 호출에는 마샬링 오버헤드가 있다
- 고빈도 호출에서는 성능에 영향을 주므로 Apartment 설계는 신중하게
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
관련 기사
같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.
COM 컴포넌트나 OCX / ActiveX 개발에서 빠지기 쉬운 것 - Visual Studio의 32bit / 64bit, 등록, 관리자 권한의 덫을 정리
COM 컴포넌트와 ActiveX, OCX 개발에서 자주 만나는 0x80040154나 0x80070005를 비트 수, 등록 방식, HKCU와 HKLM 스코프, 관리자 권한이라는 네 축으로 풀어 Visual Studio 2022의 64bit화 시대에...
Reg-Free COM이란 무엇인가 - 등록 불필요로 COM을 쓰는 구조와, 맞는 장면·맞지 않는 장면
Reg-Free COM은 COM 등록 정보를 매니페스트로 가져 앱 단위 액티베이션 컨텍스트로 해결하는 구조입니다. XCOPY 배포·버전 충돌·롤백을 가볍게 하는 한편, bitness·의존 DLL·TLB·설계 시 참조는 별개임을 정리하고 도입 판단...
Excel 장표 출력을 어떻게 만들까 - COM 자동화 / Open XML / 템플릿 방식의 판단표
Excel 장표 출력에서 COM 자동화·Open XML·템플릿 차입·기존 VBA 병용을 어떻게 가려 쓸지를 정리합니다. 사용자 PC와 무인 실행, 레이아웃 재사용 같은 요건별로 권장 구성과 빠지기 쉬운 곳까지 짚어 방식 선정의 판단표로 활용할 수...
COM / ActiveX / OCX란 무엇인가 - 차이와 관계를 정리해서 해설
Windows 레거시 안건에서 자주 마주치는 COM과 ActiveX, OCX의 차이를 토대·부품·파일이라는 축으로 정리해, regsvr32와 32/64bit, IE 모드 같은 키워드를 만났을 때 헷갈리지 않고 조사와 이전 방침을 잡을 수 있도록 ...
ActiveX / OCX를 지금 어떻게 다룰 것인가 - 남길지・감쌀지・교체할지 판단표
ActiveX / OCX 자산을 만났을 때 무작정 폐기하지 말고, 그 부품이 무엇을 떠맡고 있는지를 기준으로 남길지・감쌀지・교체할지를 32bit / 64bit, 등록 방식, 브라우저 의존, 벤더 보수까지 함께 따져 판단하는 표를 정리합니다.
관련 토픽
이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.
Windows 기술 토픽
Windows 개발, 장애 조사, 기존 자산 활용에 관한 KomuraSoft LLC 기사를 모은 토픽 허브입니다.
ActiveX 이관
COM / ActiveX / OCX 자산을 유지할지, 감쌀지, 교체할지의 단계적 판단을 정리한 토픽 페이지입니다.
UI 스레드 & 타이머
WPF / WinForms UI 스레드, async 흐름, Dispatcher 사용, 타이머 판단을 정리한 토픽 페이지입니다.
이 주제와 연결되는 서비스
이 기사는 다음 서비스 페이지로 이어집니다. 가까운 입구부터 확인해 주세요.
Windows 앱 개발
상주 처리, 장비 연동, 운영 로그, 유지 보수 가능한 구조가 필요한 Windows 데스크톱 애플리케이션을 지원합니다.
기존 자산 활용 & 이관 지원
COM / ActiveX / OCX 자산, 네이티브 코드, 32비트 의존성을 유지하면서 단계적인 이관 계획을 지원합니다.