Media Foundation이란 무엇인가 - COM과 Windows 미디어 API의 얼굴이 보이는 이유

· 업데이트: · · Media Foundation, COM, C++, Windows 개발

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

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

일본어 원문의 완역으로 다시 번역했습니다. 기존 한국어판은 원문의 일부만 옮긴 축약본이라 절, 표, Mermaid 그림, 그림 설명, FAQ가 빠져 있었습니다. 일본어 원문에 맞춰 이들을 모두 복원했고, 기술적인 주장은 일본어판과 같습니다.
참고 링크 등에서 세로줄(파이프) 기호가 들어간 줄이 표로 표시되어 링크를 누를 수 없었던 표시 오류를 수정했습니다. 본문 내용은 그대로입니다.
최초 공개
이 글을 인용하기(DOI: 10.5281/zenodo.21635098)

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

小村 豪 (2026). 「Media Foundation이란 무엇인가 - COM과 Windows 미디어 API의 얼굴이 보이는 이유」. 합동회사 코무라소프트. https://doi.org/10.5281/zenodo.21635098 https://comcomponent.com/ko/blog/2026/03/09/002-media-foundation-why-it-feels-like-com/

DOI(최신 버전)
10.5281/zenodo.21635098
DOI(이 버전)
10.5281/zenodo.22217414

Media Foundation을 만지기 시작하면 「Windows의 동영상이나 음성 API를 쓰고 있을 텐데 갑자기 COM 이야기가 늘었다」고 느끼기 쉽습니다. CoInitializeEx, MFStartup, IMFSourceReader, IMFMediaType, IMFTransform, IMFActivate, HRESULT, GUID 등이 한꺼번에 나오면서 분위기가 갑자기 Win32 / COM처럼 바뀌어, Media Foundation이 무엇인지 보이기 어려워지곤 합니다.

이 글에서는 Media Foundation 전체를 사전처럼 망라하지 않고, 다음 세 가지에 초점을 맞춰 씁니다.

  • 왜 Media Foundation을 사용하다 보면 COM 이야기가 자연스럽게 나오는가
  • 어디서 COM의 색이 짙어지는가
  • 처음에는 Source Reader / Sink Writer / Media Session / MFT 중 어디부터 만지면 좋은가

코드 예제는 C++ 기반이지만, 사고방식 자체는 .NET 등에서 래퍼를 통해 다루는 경우에도 거의 동일합니다.

목차

  1. 먼저 결론(한마디로)
  2. 용어와 전체상
    • 2.1. 먼저 의미만 짚을 용어
    • 2.2. Media Foundation의 전체상(그림)
  3. Media Foundation이 COM의 얼굴이 되는 지점
    • 3.1. 초기화에서 CoInitializeExMFStartup이 나란히 나온다
    • 3.2. 오브젝트의 인도가 인터페이스 중심
    • 3.3. 설정이나 형 정보가 IMFAttributes와 GUID 중심
    • 3.4. Activation Object가 나온다
    • 3.5. 비동기·콜백·스레드의 취급도 COM적
  4. 다만 Media Foundation = COM은 아니다
  5. 어디서부터 만질까(입구 선택)
    • 5.1. 우선은 Source Reader부터 들어가는 경우
    • 5.2. 파일로 써낸다면 Sink Writer
    • 5.3. 재생과 동기까지 다룬다면 Media Session
    • 5.4. 독자 부품을 끼워 넣는다면 MFT
  6. 실무 체크리스트
  7. 정리
  8. 참고 자료

이 글의 지식 맵

이 기사는 Media Foundation이 COM을 토대로 한 미디어 처리 플랫폼이라는 것을, 초기화·오브젝트 생성·인도·설정·열거·비동기 처리라는 5가지 지점에서 설명합니다. COM 라이브러리의 초기화(CoInitializeEx)는 Media Foundation 자신의 초기화(MFStartup)의 전제가 되며, IMFSourceReader나 IMFAttributes, IMFTransform 같은 부품은 모두 IUnknown을 기반으로 하는 COM 인터페이스로서 HRESULT를 반환합니다. IMFActivate는 실체를 나중에 만들기 위한 입구로, MFTEnumEx의 열거 결과로부터 ActivateObject를 불러야 비로소 IMFTransform을 얻을 수 있습니다. 비동기 처리는 MTA에서 동작하는 work queue의 스레드에서 호출되기 때문에, STA 쪽의 UI 객체를 직접 다루지 않고 결과만 다리 놓는 설계가 필요해집니다.

Media Foundation과 COM의 관계 지식 맵Media Foundation이 COM을 기반으로 한 미디어 처리 플랫폼이라는 것, 초기화·오브젝트 생성·인도·설정·열거·비동기 처리의 각 지점에서 COM의 성질이 나타나는 것, MTA의 work queue와 UI의 apartment 모델을 다리 놓아야 하는 것을 보여주는 그림이용한다전제로 한다이용한다전제로 한다이용한다이용한다이용한다이용한다이용한다이용한다이용한다이용한다이용한다이용한다이용한다이용한다이용한다권장되는 대응전제로 한다이용한다전제로 한다Media FoundationCOM(컴포넌트 오브젝트 모델)MFStartupCoInitializeExIUnknownHRESULTIMFSourceReader(Source Reader)IMFTransform(MFT)IMFAttributesIMFActivate(Activation Object)IMFSinkWriter(Sink Writer)Media SessionTopology(토폴로지)IMFSourceReaderCallbackwork queueCOM 아파트먼트 모델(STA/MTA)COM 스마트 포인터(ComPtr/wil::com_ptr)미디어 타입 협상(media type negotiation)

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

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

  • Media Foundation은 동영상이나 음성을 다루기 위한 플랫폼이며, API 전체가 그대로 순수한 COM인 것은 아닙니다
  • 다만 source / transform / sink / activation / attributes / callback의 경계는 COM 인터페이스로 표현되므로, 사용하다 보면 IUnknown, HRESULT, GUID, apartment 이야기가 자연스럽게 나옵니다
  • 처음에는 Source Reader / Sink Writer로 들어가고, 재생 제어가 필요해지면 Media Session, 독자 변환기가 필요해지면 MFT로 진행하면 정리하기 쉽습니다

요컨대 Media Foundation은 미디어 처리 플랫폼이며, 그 경계면에 COM이 깊게 들어 있다는 것입니다.

이를 먼저 짚어 두면 「왜 갑자기 COM의 얼굴이 되는가」의 전망이 훨씬 좋아집니다.

Media Foundation과 COM의 관계Media Foundation의 본체는 미디어 처리 플랫폼이며 API 전체가 순수한 COM은 아니지만, 부품끼리의 경계가 COM 인터페이스로 표현되므로 IUnknown이나 HRESULT나 GUID 이야기가 자연스럽게 나온다는 것을 보여준다.Media Foundation미디어 처리 플랫폼부품의 경계는 COM 인터페이스IUnknown·HRESULT·GUID가 나온다API 전체가 순수한 COM은 아니다

그림 1: 본체는 미디어 처리 플랫폼이며, 그 경계면에 COM이 깊게 들어 있다.

2. 용어와 전체상

COM 이야기에 들어가기 전에, 이 글에서 사용하는 용어와 Media Foundation의 큰 틀만 먼저 짚어 둡니다.

2.1. 먼저 의미만 짚을 용어

용어 여기서의 의미
Media Source 미디어 데이터를 파이프라인에 넣는 입구. 파일, 네트워크, 캡처 디바이스 등
MFT Media Foundation Transform. 디코더, 인코더, 영상 변환기 등의 공통 모델
Media Sink 미디어 데이터가 가는 곳. 화면 표시, 음성 출력, 파일 써내기 등
Media Session 파이프라인 전체의 흐름을 관리하는 구조. 재생이나 동기를 담당한다
Topology source / transform / sink를 어떻게 연결할지를 나타내는 접속도
Activation Object 본체를 나중에 만들기 위한 헬퍼 오브젝트. IMFActivate로 표현된다
Attributes GUID를 키로 한 key/value 스토어. Media Foundation 전체에서 널리 쓰인다
apartment COM이 스레드를 묶는 단위. 「이 오브젝트는 어느 스레드에서 호출해도 되는가」의 약속으로, CoInitializeEx의 인수로 정해진다(3.1, 3.5)
STA / MTA apartment의 종류. STA(Single-Threaded Apartment)는 스레드 하나에 묶이며, 다른 스레드에서의 호출은 메시지 펌프를 거쳐 들어온다. MTA(Multi-Threaded Apartment)는 여러 스레드가 같은 apartment를 공유하며 직접 호출할 수 있다. 자세한 내용은 COM의 STA/MTA에서 행을 피하기 위한 기초 지식 참고
work queue Media Foundation이 비동기 처리를 돌리기 위해 가진 스레드 구조. callback은 여기의 스레드에서 호출된다(3.5)

이 부분을 먼저 용어로 익혀 두면 문서를 읽을 때의 걸림돌이 꽤 줄어듭니다.

apartment는 3.5에서 본격적으로 다루지만, 여기서는 「Media Foundation의 callback은 자신이 ReadSample을 호출한 스레드와는 다른 스레드(MTA의 work queue)에서 올 수 있다」는 것만 기억해 두면 충분합니다.

2.2. Media Foundation의 전체상(그림)

Media Foundation은 크게 보면 미디어 파이프라인의 이야기입니다. COM 이야기도 중요하지만, 먼저 전체상을 보는 편이 정리하기 쉽습니다.

앱이 데이터를 직접 다루는 모델Source Reader (+ decoder)Media SourceSink Writer (+ encoder)Media Sink파이프라인 전체를 사용하는 모델MFTMedia SourceMedia SinkMedia Session

그림 2: 두 가지 사용법. Media Session에 파이프라인을 맡기는 모델과, Reader / Writer로 앱이 데이터를 직접 다루는 모델.

Media Foundation에는 크게 다음 두 가지 사용법이 있습니다.

  • 파이프라인 전체를 사용하는 모델
    • source / transform / sink를 연결하고, Media Session이 데이터 흐름이나 A/V 동기를 관리합니다
  • 앱이 데이터를 직접 다루는 모델
    • Source Reader로 source에서 데이터를 꺼내고, Sink Writer로 sink에 흘려 넣습니다

후자가 프레임이나 샘플을 직접 처리하고 싶은 상황에서는 들어가기 쉽습니다. 반면 재생이나 동기까지 포함해 플랫폼에 맡기고 싶다면 전자가 본줄기입니다.

짚어 둘 것은, Media Foundation의 정체는 미디어 처리 플랫폼이며, COM 오브젝트의 모음을 직접 만지는 감각과는 조금 다르다는 것입니다.

다만 그 부품끼리의 경계를 보기 시작하면 갑자기 COM의 얼굴이 짙어집니다. 다음 장에서 그 지점을 차례로 살펴봅니다.

3. Media Foundation이 COM의 얼굴이 되는 지점

COM의 색이 짙어지는 지점은 대략 다음 5가지로 정리할 수 있습니다.

지점 무엇이 나오는가 먼저 이해하고 싶은 것
3.1. 초기화 CoInitializeEx, MFStartup COM 초기화와 Media Foundation 초기화는 별개
3.2. 오브젝트 생성·인도 IMFSourceReader, IMFMediaType, IMFTransform 대부분이 인터페이스 포인터 + HRESULT
3.3. 설정 IMFAttributes, GUID 설정값이나 형 정보가 key/value + GUID로 표현된다
3.4. 열거·지연 생성 IMFActivate, ActivateObject 열거 결과가 그대로 본체가 아닌 경우가 있다
3.5. 비동기 IMFSourceReaderCallback, work queue callback과 apartment를 의식할 필요가 있다
(4장) 재생 제어 topology, Media Session 파이프라인 전체의 흐름은 Media Foundation 고유의 개념

마지막 재생 제어만은 성격이 달라서, COM의 일반론이 아니라 Media Foundation 자체의 기능입니다. 그래서 4장에서 따로 다룹니다.

아래에서 순서대로 살펴봅니다. 코드는 완전한 샘플이 아니라 어디서 COM의 얼굴이 되는지 알 수 있는 정도의 발췌만 싣습니다.

3.1. 초기화에서 CoInitializeExMFStartup이 나란히 나온다

처음에 많은 사람이 위화감을 느끼는 곳이 여기입니다. 파일을 열고 싶다, 카메라에서 가져오고 싶다는 이야기에 앞서 먼저 CoInitializeExMFStartup이 나옵니다.

  • CoInitializeEx는 COM 라이브러리의 초기화입니다
  • MFStartup은 Media Foundation 플랫폼의 초기화입니다

즉, COM 초기화만으로는 부족하고 Media Foundation 쪽의 초기화도 필요합니다. 여기서 「이건 단순한 동영상 API가 아니라 아래에 COM 기반의 계약이 꽤 들어가 있구나」 하고 알게 됩니다.

template <class T>
void SafeRelease(T** pp)
{
    if (pp != nullptr && *pp != nullptr)
    {
        (*pp)->Release();
        *pp = nullptr;
    }
}

HRESULT InitializeMediaFoundationForCurrentThread()
{
    HRESULT hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
    if (FAILED(hr))
    {
        return hr;
    }

    hr = MFStartup(MF_VERSION);
    if (FAILED(hr))
    {
        CoUninitialize();
        return hr;
    }

    return S_OK;
}

void UninitializeMediaFoundationForCurrentThread()
{
    MFShutdown();
    CoUninitialize();
}

CoInitializeExMFStartup이 나란히 있는 이 형태가, Media Foundation을 만지다가 갑자기 COM의 공기가 짙어지는 첫 지점입니다.

초기화와 종료의 2단 구성CoInitializeEx로 COM 라이브러리를 초기화하고 MFStartup으로 Media Foundation 플랫폼을 초기화한 뒤 사용하며, 종료 시에는 MFShutdown과 CoUninitialize를 역순으로 호출하는 2단 구성을 보여준다.CoInitializeEx(COM의 초기화)MFStartup(MF의 초기화)Media Foundation을 사용MFShutdownCoUninitialize

그림 3: COM 초기화만으로는 부족하다. 초기화는 2단 구성이며, 종료는 역순으로 되돌린다.

실무에서는 이 시점에서 다음을 정해 두면 나중이 편합니다.

  • 어느 스레드가 Media Foundation을 사용하는가
  • 그 스레드를 STA로 할지 MTA로 할지
  • MFStartup / MFShutdownCoInitializeEx / CoUninitialize의 책무를 어디가 가질 것인가

구현에서는 다른 계층이 이미 COM 초기화를 담당하고 있는 경우도 있습니다. 그 경우에도 어디가 책무를 가질지를 먼저 고정하는 편이 안전합니다. 이 설계를 애매하게 둔 채 진행하면, 나중에 callback이나 UI 연계 부분에서 알기 어려워집니다.

또한 이 글의 코드에서는 SafeRelease를 직접 작성해 생 인터페이스 포인터를 관리합니다. Microsoft 문서의 샘플이 이런 형태로 작성되어 있어 AddRef / Release가 어디서 작동하는지 보이기 때문입니다. 다만, 실무 코드에서 스마트 포인터를 쓰지 않을 이유는 되지 않습니다. C++로 새로 작성한다면 다음 중 하나에 맞추는 편이 안전합니다.

선택지 실체 비고
Microsoft::WRL::ComPtr<T> <wrl/client.h> Windows SDK에 동봉되어 있어 추가 의존이 필요 없습니다. Get()으로 생 포인터, GetAddressOf() / &로 out 인수, As<U>()QueryInterface를 작성합니다
wil::com_ptr<T> WIL(Windows Implementation Libraries)의 wil/com.h NuGet 등으로 별도 도입합니다. HRESULT를 예외로 변환하는 헬퍼와 함께 쓸 수 있습니다

ComPtr로 작성하면, 3.1 후반에 나오는 것 같은 goto done;SafeRelease의 조합이 필요 없어지고, 스코프를 벗어난 시점에 Release됩니다. 이 글에서는 COM의 작법이 보이는 것을 우선해 생 포인터를 그대로 쓰고 있지만, 신규 코드는 ComPtr부터 시작하는 것을 권합니다.

생 포인터와 스마트 포인터의 구분이 글의 코드는 AddRef와 Release가 작동하는 지점이 보이도록 생 포인터와 SafeRelease로 작성했지만, 실무의 신규 코드는 ComPtr이나 wil::com_ptr에 맞추면 스코프를 벗어난 시점에 Release되어 안전하다는 것을 보여준다.생 포인터와 SafeReleaseRelease가 작동하는 지점이 보이는 작성법ComPtr이나 wil::com_ptr스코프를 벗어나면 Release신규 코드는 ComPtr부터 시작

그림 4: 이 글의 코드는 학습용으로 생 포인터를 그대로 쓴다. 실무의 신규 코드는 스마트 포인터에 맞춘다.

3.2. 오브젝트의 인도가 인터페이스 중심

Media Foundation의 API를 읽어 나가다 보면 반환값이나 out 인수의 대부분이 COM 인터페이스입니다.

  • IMFSourceReader
  • IMFMediaType
  • IMFTransform
  • IMFActivate
  • IMFSample
  • IMFMediaBuffer

특징적인 것은 데이터 본체뿐만 아니라 형 정보나 설정 오브젝트까지 인터페이스로 표현된다는 점입니다.

예를 들어,

  • IMFTransform은 MFT를 나타내는 인터페이스입니다
  • IMFAttributes는 key/value 스토어입니다
  • IMFMediaTypeIMFAttributes를 상속한 「미디어 형식의 설명」입니다

media type처럼 「설정 데이터 같은 것」까지 COM 인터페이스로 가지고 있는 것입니다. 여기서 IUnknown, QueryInterface, AddRef / Release, HRESULT의 맥락이 자연스럽게 들어옵니다.

IUnknownIMFAttributesIMFMediaTypeIMFActivateIMFSourceReaderIMFTransform

그림 5: 주요 인터페이스의 계보. 설정이나 형 정보까지 IUnknown을 정점으로 하는 COM 인터페이스로 표현된다.

여기까지 오면 「Media Foundation은 미디어 API지만 경계를 표현하는 방식은 꽤 COM이구나」 하고 보이기 시작합니다.

3.3. 설정이나 형 정보가 IMFAttributes와 GUID 중심

Media Foundation을 만지다 보면 설정이 갑자기 GUID투성이로 보이는 지점이 있습니다. 그 중심이 IMFAttributes이며, GUID를 키로 한 key/value 스토어입니다. 이것이 Media Foundation 전체에서 매우 자주 쓰입니다.

특히 중요한 것이 IMFMediaType으로, IMFAttributes를 상속하고 있으며 미디어 형식의 정보를 속성으로 가집니다.

예를 들어 다음과 같은 정보입니다.

  • major type(음성인지 영상인지)
  • subtype(H.264, AAC, RGB32, PCM 등)
  • 프레임 사이즈
  • 프레임 레이트
  • 샘플 레이트
  • 채널 수
IMFMediaTypeMF_MT_MAJOR_TYPEMF_MT_SUBTYPE사이즈 / FPS / 샘플 레이트 등

그림 6: IMFMediaType은 속성 스토어이며, major type이나 subtype 같은 형식 정보를 GUID 키로 가진다.

이 부분을 「GUID의 숲」으로 느끼기 쉽지만, 실제로 하는 일은 꽤 단순합니다.

  • 속성 스토어를 사용해 설정을 가진다
  • media type도 속성 스토어로 표현한다
  • source / transform / sink 사이에서 그 속성을 보면서 형식을 맞춘다

설정과 형 정보의 표현에 COM적인 인터페이스와 GUID가 쓰이고 있다는 이야기일 뿐입니다.

여기까지를 코드로 보면 다음과 같습니다. Source Reader로 동영상에서 프레임 1개를 읽기만 하는 예입니다.

HRESULT ReadOneVideoSample(PCWSTR path)
{
    IMFSourceReader* pReader = nullptr;
    IMFMediaType* pType = nullptr;
    IMFSample* pSample = nullptr;

    HRESULT hr = MFCreateSourceReaderFromURL(path, nullptr, &pReader);
    if (FAILED(hr)) goto done;

    hr = MFCreateMediaType(&pType);
    if (FAILED(hr)) goto done;

    hr = pType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video);
    if (FAILED(hr)) goto done;

    hr = pType->SetGUID(MF_MT_SUBTYPE, MFVideoFormat_RGB32);
    if (FAILED(hr)) goto done;

    hr = pReader->SetCurrentMediaType(
        MF_SOURCE_READER_FIRST_VIDEO_STREAM,
        nullptr,
        pType);
    if (FAILED(hr)) goto done;

    DWORD streamFlags = 0;
    LONGLONG timestamp = 0;

    hr = pReader->ReadSample(
        MF_SOURCE_READER_FIRST_VIDEO_STREAM,
        0,
        nullptr,
        &streamFlags,
        &timestamp,
        &pSample);
    if (FAILED(hr)) goto done;

    // pSample에서 IMFMediaBuffer를 꺼내 처리한다

done:
    SafeRelease(&pSample);
    SafeRelease(&pType);
    SafeRelease(&pReader);
    return hr;
}

여기서 보이는 것은 다음과 같은 점입니다.

  • reader도 media type도 COM 인터페이스
  • 설정은 GUID 기반
  • 반환값은 HRESULT
  • 동기 모드에서는 ReadSample이 블로킹한다

「그저 프레임 1개를 읽고 싶다」는 것만으로도, Media Foundation의 경계에서는 꽤 COM적인 얼굴이 됩니다. 마지막의 동기 모드 이야기는 3.5에서 다룹니다.

media type negotiation의 절차(6장의 체크리스트에서 「가장 먼저 봐두고 싶은 3가지」에 드는 항목입니다)

위 코드는 「RGB32로 받고 싶다」고 선언하고 있을 뿐이므로, 실제 실무에서는 그 앞뒤로 절차가 필요합니다. Source Reader의 경우 Microsoft 문서가 제시하는 흐름은 다음 4단계입니다.

  1. 네이티브 타입을 열거한다IMFSourceReader::GetNativeMediaType(streamIndex, typeIndex, &pType)typeIndex를 0부터 늘려가며 호출합니다. 범위를 넘으면 MF_E_NO_MORE_TYPES가 반환되므로 그것이 열거의 끝입니다(streamIndex가 범위 밖이면 MF_E_INVALIDSTREAMNUMBER). 파일이라면 스트림 하나당 한 종류인 경우가 많지만, 웹캠은 여러 형식을 가집니다
  2. major type을 확인한다 — 열거로 얻은 media type에서 MF_MT_MAJOR_TYPE을 읽어 음성인지 영상인지를 판단합니다. 이를 확인하지 않고 고정으로 진행하면 음성 스트림에 영상 설정을 던지게 됩니다
  3. 원하는 출력 형식을 구성해 설정한다MFCreateMediaType으로 새 media type을 만들고 MF_MT_MAJOR_TYPEMF_MT_SUBTYPE을 설정한 뒤 SetCurrentMediaType을 호출합니다. 압축된 채로 받고 싶다면 절차 1에서 얻은 타입을 그대로 넘기고, 디코드해서 받고 싶다면 비압축 형식(MFVideoFormat_RGB32, MFAudioFormat_PCM 등)을 지정합니다. 디코더는 Source Reader가 자동으로 읽어 들입니다
  4. 확정된 형식을 다시 읽는다SetCurrentMediaType 뒤에 GetCurrentMediaType을 호출해, 실제로 확정된 형식의 세부 사항(프레임 사이즈, stride, 샘플 레이트 등)을 얻습니다. 절차 3에서 넘기는 것은 부분적인 지정이므로 확정값은 이쪽에서 읽는 것이 올바른 순서입니다

이 4단계를 건너뛰고 「아마 이 형식이겠지」로 진행하면 MF_E_INVALIDMEDIATYPE이 반환되거나, 통과하더라도 예상과 다른 형식의 버퍼를 읽게 됩니다.

media type negotiation의 4단계GetNativeMediaType으로 네이티브 타입을 열거하고, major type을 확인하고, 원하는 출력 형식을 구성해 SetCurrentMediaType으로 설정하고, 마지막으로 GetCurrentMediaType으로 확정된 형식을 다시 읽는 4단계를 보여준다.GetNativeMediaType으로 열거major type을 확인한다원하는 형식을 SetCurrentMediaTypeGetCurrentMediaType으로 확정값을 읽는다MF_E_NO_MORE_TYPES가 열거의 끝

그림 7: 형식 맞추기는 4단계. 넘기는 것은 부분적인 지정이므로 확정값은 마지막에 다시 읽는다.

3.4. Activation Object가 나온다

Media Foundation의 COM다움이 특히 드러나는 것이 activation object입니다.

IMFActivate는 나중에 본체를 만들기 위한 헬퍼 오브젝트입니다. 감각적으로는 COM의 class factory에 가까운 것으로 보면 이해하기 쉽습니다.

이것이 나오는 상황에서는 열거 API의 반환값이 「그대로 쓸 수 있는 본체」가 아니라, 먼저 IMFActivate*의 배열로 되어 있는 경우가 있습니다. 그리고 필요한 것만 ActivateObject로 실체화합니다.

IMFTransform / Sink 등IMFActivate열거 APIIMFTransform / Sink 등IMFActivate열거 API열거를 호출한다IMFActivate* 의 배열속성을 확인한다ActivateObject(...)실체의 COM 오브젝트

그림 8: 열거 API가 반환하는 것은 IMFActivate이며, ActivateObject를 호출해야 비로소 실체의 COM 오브젝트를 얻는다.

이 형태는 Media Foundation이 교체 가능한 부품을 나중에 찾아 조합하는 설계로 되어 있는 것과 궁합이 좋습니다.

또한 activation object 자체가 attributes를 가질 수 있으므로, 「먼저 후보의 속성을 본다」「필요하면 설정한다」「나중에 실체화한다」는 흐름이 되기 쉽습니다. 여기도 꽤 COM적입니다.

실제로 MFTEnumEx로 MFT를 열거해서 실체화하면 다음과 같습니다.

HRESULT FindH264Decoder(IMFTransform** ppTransform)
{
    *ppTransform = nullptr;

    IMFActivate** ppActivate = nullptr;
    UINT32 count = 0;

    MFT_REGISTER_TYPE_INFO inputType = {};
    inputType.guidMajorType = MFMediaType_Video;
    inputType.guidSubtype = MFVideoFormat_H264;

    HRESULT hr = MFTEnumEx(
        MFT_CATEGORY_VIDEO_DECODER,
        MFT_ENUM_FLAG_SYNCMFT | MFT_ENUM_FLAG_LOCALMFT,
        &inputType,
        nullptr,
        &ppActivate,
        &count);
    if (FAILED(hr))
    {
        return hr;
    }

    if (count == 0)
    {
        CoTaskMemFree(ppActivate);
        return MF_E_TOPO_CODEC_NOT_FOUND;
    }

    hr = ppActivate[0]->ActivateObject(
        __uuidof(IMFTransform),
        reinterpret_cast<void**>(ppTransform));

    for (UINT32 i = 0; i < count; ++i)
    {
        ppActivate[i]->Release();
    }
    CoTaskMemFree(ppActivate);

    return hr;
}

열거 결과가 처음부터 IMFTransform*가 아니라 IMFActivate**로 돌아오고, ActivateObject를 호출해야 비로소 실체의 IMFTransform을 얻습니다. 이 흐름이 Media Foundation의 「갑자기 COM의 얼굴이 된다」는 느낌을 꽤 잘 나타냅니다.

MFTEnumEx에서 디코더 실체화까지의 흐름MFTEnumEx로 디코더 후보를 열거하면 IMFActivate의 배열이 반환되고, 후보가 없으면 MF_E_TOPO_CODEC_NOT_FOUND로 하고, 있으면 ActivateObject로 실체화해 IMFTransform을 얻은 뒤 각 IMFActivate를 Release하고 배열 본체를 CoTaskMemFree로 해제하는 흐름을 보여준다.없다있다MFTEnumEx로 후보를 열거IMFActivate의 배열이 반환된다후보가 있는가MF_E_TOPO_CODEC_NOT_FOUNDActivateObject로 실체화IMFTransform을 얻는다각 요소를 Release배열은 CoTaskMemFree

그림 9: 열거→후보 확인→실체화→해제라는 흐름. 열거 결과는 그대로 쓸 수 있는 본체가 아니다.

3.5. 비동기·콜백·스레드의 취급도 COM적

Media Foundation의 실무에서 간과하기 쉬운 것이 비동기 처리와 스레드 모델입니다.

예를 들어 Source Reader는 기본값으로는 동기 모드입니다. 동기 모드에서는 ReadSample이 블로킹합니다. 파일이나 네트워크, 디바이스의 상태에 따라서는 그 대기가 눈에 보이는 시간이 되는 경우도 있습니다.

비동기 모드로 하고 싶은 경우에는 Source Reader 작성 시에 callback을 넘깁니다. IMFSourceReaderCallback을 구현한 오브젝트를 준비하고, MF_SOURCE_READER_ASYNC_CALLBACK 속성에 설정한 뒤 작성하는 흐름입니다.

HRESULT CreateSourceReaderAsync(
    PCWSTR path,
    IMFSourceReaderCallback* pCallback,
    IMFSourceReader** ppReader)
{
    IMFAttributes* pAttributes = nullptr;

    HRESULT hr = MFCreateAttributes(&pAttributes, 1);
    if (FAILED(hr))
    {
        return hr;
    }

    hr = pAttributes->SetUnknown(MF_SOURCE_READER_ASYNC_CALLBACK, pCallback);
    if (SUCCEEDED(hr))
    {
        hr = MFCreateSourceReaderFromURL(path, pAttributes, ppReader);
    }

    SafeRelease(&pAttributes);
    return hr;
}

즉,

  • callback 자체가 COM 인터페이스
  • 비동기 설정이 IMFAttributes 경유
  • 모드는 작성 시에 정해진다

는 형태입니다.

여기에 더해 다소 중요한 것이 apartment입니다. Media Foundation의 비동기 처리는 work queue를 사용하며, work queue의 스레드는 MTA입니다. 그래서 애플리케이션 측도 MTA로 다루면 구현이 단순해집니다.

IMFSourceReaderCallbackMF work queue (MTA)Source Reader앱 스레드IMFSourceReaderCallbackMF work queue (MTA)Source Reader앱 스레드ReadSample(...)바로 돌아온다내부에서 처리OnReadSample(...)

그림 10: 비동기 모드의 ReadSample은 바로 돌아오며, work queue의 스레드에서 OnReadSample이 호출된다.

callback 주변에서 유의하고 싶은 것은 다음과 같은 점입니다.

  • UI 스레드의 STA 오브젝트를 그대로 callback 쪽에서 만지지 않는다
  • callback 구현은 스레드 세이프로 한다
  • UI 업데이트가 필요하면 결과만 UI 스레드로 되돌린다
  • 「Media Foundation의 callback은 어느 스레드에서 오는가」를 처음에 고정해서 생각한다

Media Foundation은 STA 오브젝트의 사정을 알아서 흡수해 주는 것이 아닙니다. 그래서 Media Foundation을 사용하는 워커는 MTA로 맞추고, UI와는 명시적으로 다리를 놓는 편이 정리하기 쉽습니다.

callback과 UI 스레드의 다리 놓는 법callback은 MTA의 work queue 스레드에서 오므로 구현을 스레드 세이프로 하고, STA의 UI 오브젝트를 직접 만지지 않으며, UI 업데이트가 필요하면 결과만 UI 스레드로 되돌린다는 정리를 보여준다.callback은 MTA의 work queue에서 온다구현은 스레드 세이프로 한다STA의 UI 오브젝트를 직접 만지지 않는다결과만 UI 스레드로 되돌린다

그림 11: Media Foundation을 사용하는 워커는 MTA로 맞추고, UI와는 명시적으로 다리를 놓는다.

4. 다만 Media Foundation = COM은 아니다

여기까지 읽으면 「결국 Media Foundation은 COM 그 자체 아닌가」라고 생각하기 쉽습니다. 하지만 거기는 조금 다릅니다.

Media Foundation에는 COM의 일반론만으로는 끝나지 않는 플랫폼 고유의 개념이 있습니다.

  • MFStartup / MFShutdown
  • Media Session
  • topology
  • topology loader
  • presentation clock
  • Source Reader / Sink Writer

이 부분은 미디어 파이프라인을 어떻게 흘려보낼 것인가라는 Media Foundation 자신의 역할입니다.

예를 들어 Media Session에서는 애플리케이션이 partial topology를 넘기면 topology loader가 필요한 transform을 보충해 full topology로 해결하는 흐름이 있습니다. 이는 COM의 일반적인 이야기라기보다, Media Foundation이 미디어 처리 플랫폼으로서 가지고 있는 기능입니다.

Partial TopologySource -&gt; OutputTopology LoaderFull TopologySource -&gt; Decoder MFT -&gt; Output

그림 12: partial topology를 넘기면 topology loader가 필요한 transform을 보충해 full topology로 해결한다.

Media Foundation은 COM을 사용해 부품의 계약을 표현하면서, 그 위에서 미디어 처리 플랫폼으로 동작하는 것입니다. 이 2단 구조로 보면 헤매기 쉽지 않습니다.

COM 계층과 플랫폼 계층의 2단 구조Media Foundation은 COM으로 부품끼리의 계약을 표현하고, 그 위에 Media Session이나 topology나 presentation clock 같은 미디어 파이프라인 고유의 구조를 가지는 2단 구조임을 보여준다.COM 계층(부품의 계약을 표현)미디어 처리 플랫폼 계층Media Session이나 topology 등COM의 일반론만으로는 끝나지 않는 MF 고유의 개념

그림 13: COM을 다시 구운 것이 아니다. COM 계층 위에, 파이프라인을 흘려보내는 MF 고유의 계층이 얹혀 있다.

5. 어디서부터 만질까(입구 선택)

처음 입구를 정할 때는 다음 그림으로 충분한 경우가 많습니다.

프레임 / 샘플을 읽고 싶다파일로 써내고 싶다재생 제어나 A/V 동기가 필요독자적인 변환기를 넣고 싶다하고 싶은 것처음에 필요한 것은?Source ReaderSink WriterMedia SessionMFT

그림 14: 처음에 필요한 것을 기준으로 입구를 고른다. 읽고 싶다면 Reader, 쓰고 싶다면 Writer, 재생이라면 Session.

표로 정리하면 다음과 같습니다.

하고 싶은 것 먼저 만지는 것 COM의 짙기 보충
파일이나 카메라에서 프레임 / 샘플을 얻고 싶다 Source Reader 필요하면 decoder도 돌봐준다
생성한 음성 / 영상을 파일로 써내고 싶다 Sink Writer 필요하면 encoder와 media sink를 함께 다룰 수 있다
재생, 정지, 시크, A/V 동기, 품질 제어까지 다루고 싶다 Media Session topology와 session의 이해가 필요
독자적인 변환기나 codec적인 부품을 끼워 넣고 싶다 MFT IMFTransform을 중심으로 생각한다
열거한 후보를 보고 나서 필요한 것만 실체화하고 싶다 IMFActivate 돌아오는 것이 본체가 아니라 activation object인 경우가 있다

5.1. 우선은 Source Reader부터 들어가는 경우

Source Reader는 파일이나 디바이스에서 데이터를 꺼내고 싶을 때의 입구로서 꽤 사용하기 쉽습니다.

어울리는 것은 예를 들어 다음과 같은 경우입니다.

  • 동영상 파일에서 프레임을 얻고 싶다
  • 음성 파일을 디코드해서 샘플을 얻고 싶다
  • 카메라에서 프레임을 얻고 싶다
  • Media Foundation의 source를 자체 처리 파이프라인에 연결하고 싶다

Source Reader는 필요에 따라 decoder를 읽어 들여 애플리케이션에 데이터를 건네줍니다. 반면 프레젠테이션 클록의 관리나 A/V 동기, 화면 묘화 자체까지는 돌보지 않습니다.

「재생한다」가 아니라 「데이터를 얻는다」를 위한 입구로 생각하면 이해하기 쉽습니다.

Source Reader의 역할 범위Source Reader는 파일이나 카메라에서 데이터를 꺼내 필요에 따라 decoder를 읽어 들여 앱에 건네주지만, 프레젠테이션 클록의 관리나 A/V 동기, 화면 묘화까지는 돌보지 않는다는 역할의 범위를 보여준다.파일·카메라 등의 sourceSource Reader앱에 데이터를 건넨다필요하면 decoder를 읽어 들인다재생이나 동기는 돌보지 않는다

그림 15: Source Reader는 「재생한다」가 아니라 「데이터를 얻는다」를 위한 입구.

5.2. 파일로 써낸다면 Sink Writer

Sink Writer는 음성이나 영상을 파일로 써내고 싶을 때의 입구입니다.

용도로는 다음과 같은 경우가 전형적입니다.

  • 생성한 프레임을 동영상 파일로 저장하고 싶다
  • 음성 샘플을 인코드해서 써내고 싶다
  • 읽어낸 데이터를 다른 형식으로 변환해 저장하고 싶다

Sink Writer는 필요에 따라 encoder를 찾아 읽어 들여 media sink로의 데이터 흐름을 관리합니다. Source Reader와 조합하는 경우도 많지만, 둘은 독립된 부품이므로 반드시 세트로 사용할 필요는 없습니다.

Sink Writer의 역할 범위앱이 생성한 프레임이나 음성 샘플을 Sink Writer에 넘기면, 필요에 따라 encoder를 찾아 읽어 들여 media sink로의 데이터 흐름을 관리해 파일로 써낸다는 것을 보여준다.앱이 생성한 프레임·음성Sink Writer필요하면 encoder를 읽어 들인다media sink로 써낸다Source Reader와 독립된 부품

그림 16: Sink Writer는 인코드와 써내기를 돌보는 입구. Reader와의 세트 사용은 필수가 아니다.

5.3. 재생과 동기까지 다룬다면 Media Session

「파일에서 얻고 싶다」가 아니라 제대로 재생하고 싶다면, Media Session을 중심으로 생각하는 편이 자연스럽습니다.

Media Session이 나설 차례가 되는 것은 다음과 같은 요건이 있을 때입니다.

  • 재생 / 정지 / 시크를 다루고 싶다
  • 음성과 영상의 동기를 플랫폼 측에 맡기고 싶다
  • 품질 제어나 포맷 변경까지 포함해 파이프라인을 다루고 싶다
  • topology를 사용해 source / transform / sink의 흐름을 짜고 싶다

이 레이어에 들어가면 Source Reader / Sink Writer보다 「Media Foundation 본체」에 가까워집니다. 그만큼 topology나 session event 등, Media Foundation 고유의 개념도 늘어납니다.

Media Session을 선택하는 판단재생·정지·시크나 A/V 동기, 품질 제어까지 플랫폼에 맡기고 싶다면 Media Session을 중심으로 생각하고, topology로 흐름을 짜게 되며, 그만큼 Media Foundation 고유의 개념이 늘어난다는 것을 보여준다.재생·시크·동기까지 맡기고 싶다Media Session을 중심으로 생각한다topology로 source부터 sink까지 짠다MF 고유의 개념이 그만큼 늘어난다

그림 17: 「데이터를 얻고 싶다」가 아니라 「제대로 재생하고 싶다」면 Media Session이 본줄기다.

5.4. 독자 부품을 끼워 넣는다면 MFT

MFT는 Media Foundation의 transform의 공통 모델입니다.

여기에 들어가는 것은 다음과 같은 상황입니다.

  • 독자적인 디코더나 인코더를 만들고 싶다
  • 영상 처리나 음성 처리의 부품을 파이프라인에 끼워 넣고 싶다
  • codec이나 변환기를 열거해서 직접 고르고 싶다
  • 기본값의 자동 해결보다 깊이 제어하고 싶다

MFT의 세계에서는 IMFTransform, IMFActivate, media type negotiation, 샘플 / 버퍼 관리 등 COM적인 계약이 꽤 전면에 나옵니다. 그래서 처음 입구로 갑자기 MFT에 들어가기보다, 먼저 Source Reader / Sink Writer / Media Session 중 무엇이 정말로 필요한지를 먼저 보는 편이 이해하기 쉽습니다.

MFT로 진행하기 전 확인독자적인 디코더나 변환기를 파이프라인에 끼워 넣고 싶을 때는 MFT로 진행하지만, COM적인 계약이 전면에 나오므로 먼저 Source Reader나 Sink Writer나 Media Session으로 충분하지 않은지 확인하는 것이 좋다는 것을 보여준다.아니오먼저 다른 세 입구로 충분한지 본다독자적인 변환 부품이 필요한가Reader나 Writer나 Session으로 진행한다MFT(IMFTransform)로 진행한다COM적인 계약이 전면에 나온다

그림 18: MFT는 마지막 입구다. 갑자기 들어가지 말고 다른 세 가지로 충분하지 않은지 확인한 뒤 진행한다.

6. 실무 체크리스트

마지막으로 실무에서 먼저 봐두고 싶은 점을 한 장으로 정리합니다.

항목 봐둘 것 놓치면 일어나기 쉬운 것
초기화 책무 CoInitializeExMFStartup을 어디서 호출할지, 종료 처리를 어디서 가질지 정한다 초기화 누락, 종료 순서의 혼란
apartment MF를 만지는 스레드를 STA / MTA 중 어느 쪽으로 할지 먼저 정한다 callback 주변의 혼란, UI와의 충돌
Source Reader의 모드 동기인지 비동기인지를 작성 시에 정한다 ReadSample이 예상외로 블로킹한다, 나중에 전환할 수 없다
media type negotiation 출력 형식을 열거하고 실제로 사용할 형식을 명시한다. 절차는 3.3의 4단계(GetNativeMediaType으로 열거 → major type 확인 → SetCurrentMediaTypeGetCurrentMediaType으로 확정값을 읽는다) MF_E_INVALIDMEDIATYPE, 기대와 다른 형식이 온다
오브젝트 수명 Release, Unlock, ShutdownObject의 책무를 명확히 한다 메모리 누수, 버퍼 유지, 종료 시의 불일치
activation object 열거 결과가 본체인지 IMFActivate인지를 구별한다 QueryInterface가 될 것이라 생각해 실패한다
topology partial topology와 full topology 중 어느 것을 다루고 있는지 파악한다 「자동으로 연결될 것」이라 생각해 막힌다
에러 확인 HRESULT, stream flags, event를 매번 확인한다 일부만 실패하고 있는데 놓친다
UI 연계 callback에서 직접 UI를 만지지 않고 결과만 UI 스레드로 되돌린다 행, 경합, 알기 어려운 오류

특히 우선순위가 높은 것은 다음 3가지입니다.

  1. 처음의 입구 API를 틀리지 않을 것
    • 우선 Source Reader / Sink Writer / Media Session 중 무엇이 정말로 필요한지를 나눈다
  2. apartment를 먼저 정할 것
    • STA의 UI와 Media Foundation의 work queue를 섞는다면, 다리를 놓는 방법을 처음에 정한다
  3. media type negotiation을 대충 하지 말 것
    • 「아마 이 형식이겠지」로 진행하면 나중에 꽤 알기 어려워집니다
    • 구체적인 절차는 3.3의 「media type negotiation의 절차」에 정리되어 있습니다
체크리스트에서 우선순위가 높은 3항목처음의 입구 API를 틀리지 않을 것, apartment를 먼저 정할 것, media type negotiation을 대충 하지 않을 것의 3가지가 특히 우선순위가 높으며, 이것으로 이후 구현의 혼란을 피할 수 있다는 것을 보여준다.입구 API를 틀리지 않는다이후 구현의 혼란을 피할 수 있다apartment를 먼저 정한다형식 맞추기를 대충 하지 않는다

그림 19: 체크리스트 중에서도 이 3가지를 먼저 챙기는 것이 효과적이다.

7. 정리

Media Foundation을 만지다가 갑자기 COM 이야기가 늘어나는 것은 우연이 아닙니다.

  • Media Foundation은 미디어 처리 플랫폼이다
  • 그 source / transform / sink / activation / callback 등의 경계는 COM 인터페이스로 표현된다
  • 그래서 IUnknown, HRESULT, GUID, apartment, callback 이야기가 자연스럽게 나온다
  • 다만 Media Foundation의 본체는 Media Session이나 topology를 가진 미디어 파이프라인이며, 단순한 COM의 재탕이 아니다

실무에서는 먼저 다음 순서로 생각하면 꽤 정리하기 쉽습니다.

  1. 애초에 Source Reader / Sink Writer / Media Session / MFT 중 무엇이 필요한지 나눈다
  2. apartment와 callback의 방침을 먼저 정한다
  3. media type negotiation과 오브젝트 수명을 정성껏 다룬다
실무에서 생각하는 순서먼저 어느 입구가 필요한지 나누고, 다음으로 apartment와 callback의 방침을 정하고, 마지막으로 media type negotiation과 오브젝트 수명을 정성껏 다룬다는 실무 정리 순서를 보여준다.어느 입구가 필요한지 나눈다apartment와 callback의 방침을 정한다형식 맞추기와 수명을 정성껏

그림 20: 실무에서 생각하는 순서. 입구 선정, 스레드 방침, 형식과 수명 순으로 정해 나간다.

처음부터 전부를 이해하려 하지 않아도 괜찮습니다. 우선은 「Media Foundation은 미디어 처리 플랫폼이며, COM은 그 경계면에 깊게 들어 있다」고 봐두면, 문서도 코드도 꽤 따라가기 쉬워집니다.

8. 참고 자료

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

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

ActiveX 이관

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

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

자주 묻는 질문

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

Media Foundation이란 무엇인가요? COM과는 다른가요?
Media Foundation은 Windows에서 동영상이나 음성을 다루기 위한 미디어 처리 플랫폼이며, API 전체가 그대로 순수한 COM인 것은 아닙니다. 다만 source / transform / sink / activation / attributes / callback 같은 부품끼리의 경계는 COM 인터페이스로 표현되므로, 사용하다 보면 IUnknown, HRESULT, GUID, apartment 이야기가 자연스럽게 나옵니다. 「미디어 처리 플랫폼이며, 그 경계면에 COM이 깊게 들어 있다」고 보는 것이 정확합니다.
MFStartup과 CoInitializeEx는 왜 둘 다 필요한가요?
역할이 다르기 때문입니다. CoInitializeEx는 COM 라이브러리의 초기화이고, MFStartup은 Media Foundation 플랫폼의 초기화입니다. COM 초기화만으로는 부족하고 Media Foundation 쪽의 초기화도 필요합니다. 실무에서는 어느 스레드가 Media Foundation을 사용할지, STA와 MTA 중 어느 쪽으로 할지, MFStartup / MFShutdown과 CoInitializeEx / CoUninitialize의 책무를 어디가 가질지를 먼저 정해 두면, 나중에 callback이나 UI 연계가 편해집니다.
Source Reader, Sink Writer, Media Session, MFT는 어떻게 구분해서 사용하나요?
파일이나 카메라에서 프레임이나 샘플을 꺼내고 싶다면 Source Reader, 생성한 음성·영상을 파일로 써내고 싶다면 Sink Writer가 입구입니다. 재생·정지·시크나 A/V 동기, 품질 제어까지 플랫폼에 맡기고 싶다면 Media Session을 중심으로 생각합니다. 독자적인 디코더나 변환기를 파이프라인에 끼워 넣고 싶은 경우에는 MFT로 진행하지만, COM적인 계약이 전면에 나오므로 먼저 앞의 세 가지 중 무엇이 정말로 필요한지를 먼저 보는 것을 권합니다.
Media Foundation의 비동기 콜백에서 주의할 점은 무엇인가요?
Media Foundation의 비동기 처리는 work queue를 사용하며 그 스레드는 MTA이므로, 애플리케이션 측도 MTA로 맞추면 구현이 단순해집니다. IMFSourceReaderCallback 구현은 스레드 세이프로 하고, UI 스레드의 STA 오브젝트를 callback 측에서 직접 만지지 않는 것이 중요합니다. UI 업데이트가 필요하면 결과만 UI 스레드로 되돌립니다. 또한 Source Reader의 동기·비동기 모드는 작성 시에 정해지며, 나중에 전환할 수 없다는 점에도 주의가 필요합니다.

저자 프로필

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

Go Komura

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

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

블로그 목록으로 돌아가기