C++/MFC 프로그램 중복 실행 막기: CreateMutex로 인스턴스 하나만 띄우는 법

Prompt Architect 편집팀 · 2026-06-18 · 7분

TL;DR — CreateMutex와 ERROR_ALREADY_EXISTS를 활용해 Windows MFC 애플리케이션을 단일 인스턴스로 제한하는 방법을 단계별 코드와 흔한 실수까지 정리했습니다.

같은 프로그램이 두 번 떠서 생기는 문제

데스크톱 애플리케이션을 만들다 보면 "사용자가 실행 아이콘을 두 번 클릭해서 같은 프로그램이 두 개 떠 버리는" 상황을 자주 만나게 됩니다. 단순히 창이 두 개 보이는 것으로 끝나면 다행이지만, 실무에서는 다음과 같은 더 골치 아픈 문제로 번집니다.

  • 같은 설정 파일이나 로그 파일에 두 프로세스가 동시에 쓰면서 데이터가 깨짐
  • 직렬 포트, 데이터베이스 연결, 특정 TCP 포트처럼 한 번에 하나만 점유할 수 있는 리소스를 두고 충돌
  • 하드웨어 제어 프로그램에서 두 인스턴스가 서로 다른 명령을 보내 장비가 오작동
  • 트레이 아이콘이 두 개 생기는 등 사용자 경험 저하

이런 문제를 막는 가장 고전적이면서도 확실한 방법이 바로 Windows의 명명된 뮤텍스(Named Mutex) 를 이용한 단일 인스턴스(single instance) 제한입니다. 이 글에서는 C++/MFC 환경을 기준으로, CreateMutex API를 활용해 "이미 실행 중이면 두 번째 실행을 즉시 종료"하는 패턴을 처음부터 끝까지 다뤄 보겠습니다.

왜 뮤텍스로 중복 실행을 막을 수 있을까

핵심 원리는 이름이 같은 커널 객체는 시스템 전체에서 단 하나만 존재한다는 점입니다. CreateMutex에 특정 문자열 이름을 주고 뮤텍스를 만들면, 그 뮤텍스 객체는 운영체제 커널 영역에 등록됩니다.

이때 동작이 영리합니다.

  • 첫 번째 프로세스가 그 이름으로 CreateMutex를 호출하면 → 뮤텍스가 새로 만들어짐
  • 두 번째 프로세스가 똑같은 이름으로 호출하면 → 새로 만드는 대신 기존 뮤텍스의 핸들을 돌려주고, GetLastError()ERROR_ALREADY_EXISTS를 반환

즉, "방금 내가 만든 건가, 아니면 이미 누가 만들어 둔 건가?"를 GetLastError() 값 하나로 구분할 수 있습니다. 이 신호가 곧 "다른 인스턴스가 이미 떠 있다"는 증거가 됩니다.

뮤텍스를 굳이 잠금(lock) 용도로 쓰지 않는다는 점에 주목하세요. 여기서는 상호 배제(mutual exclusion) 기능을 쓰는 게 아니라, 단지 "이름의 존재 여부"를 플래그처럼 활용하는 것입니다.

단계별 구현 흐름

MFC 애플리케이션에서는 프로그램 진입점인 CXxxApp::InitInstance()가장 앞부분에 검사 코드를 넣는 것이 정석입니다. 메인 윈도우를 만들고 리소스를 초기화하기 전에 중복 여부를 판단해야 불필요한 작업을 막을 수 있기 때문입니다.

전체 절차는 다음 네 단계로 요약됩니다.

  1. 뮤텍스 생성 — 고유한 이름으로 CreateMutex 호출
  2. 생성 실패 검사 — 반환값이 NULL이면 권한 등의 이유로 실패한 것이므로 종료
  3. 중복 실행 검사GetLastError()ERROR_ALREADY_EXISTS면 이미 떠 있는 것
  4. 정리 및 종료 — 두 번째 인스턴스라면 핸들을 닫고 InitInstance에서 FALSE 반환

실제 코드 예제

아래는 위 절차를 그대로 옮긴 MFC용 예제입니다. 원본에서 흔히 보던 형태를 실무 변수명과 한국어 주석으로 다시 정리했습니다.

BOOL CMyToolApp::InitInstance()
{
    // 1) 시스템 전역에서 유일한 이름으로 뮤텍스 생성 시도
    //    이름이 같은 커널 객체는 OS 전체에 하나만 존재한다.
    HANDLE hSingleGuard = CreateMutex(
        NULL,                                  // 보안 속성: 기본값
        FALSE,                                 // 소유권을 즉시 잡지 않음
        _T("MyTool-Instance-{D8F1A6C2-Single}")); // 충돌 없는 고유 이름

    // 2) 생성 자체가 실패한 경우 (권한 문제 등) — 안전하게 종료
    if (hSingleGuard == NULL)
    {
        AfxMessageBox(_T("초기화에 실패했습니다. 프로그램을 종료합니다."));
        return FALSE;
    }

    // 3) "이미 같은 이름의 뮤텍스가 있었다" = 다른 인스턴스가 실행 중
    if (GetLastError() == ERROR_ALREADY_EXISTS)
    {
        // 내가 받은 핸들은 닫아준다 (원본 뮤텍스는 첫 인스턴스가 소유)
        CloseHandle(hSingleGuard);
        hSingleGuard = NULL;

        AfxMessageBox(_T("프로그램이 이미 실행 중입니다."));
        return FALSE;  // InitInstance에서 FALSE를 반환하면 앱이 곧바로 종료된다
    }

    // 여기까지 왔다면 "첫 번째 인스턴스"가 확실하다.
    // hSingleGuard 핸들은 프로그램이 끝날 때까지 그대로 둔다.
    // (핸들이 닫히면 뮤텍스도 사라져 다음 실행이 첫 인스턴스로 인식되므로)

    // ... 기존 InitInstance 초기화 코드 (메인 윈도우 생성 등) ...

    return TRUE;
}

코드에서 가장 중요한 두 가지 포인트는 다음과 같습니다.

  • 두 번째 인스턴스는 받은 핸들을 즉시 CloseHandle 합니다. 이 핸들을 닫아도 첫 인스턴스가 보유한 원본 뮤텍스는 살아 있으므로 안전합니다.
  • 첫 번째 인스턴스의 핸들은 절대 중간에 닫지 않습니다. 프로그램이 종료되면 운영체제가 핸들을 자동으로 회수하고, 그 시점에 뮤텍스도 사라집니다.

뮤텍스 이름은 GUID로 만들어라

가장 흔하면서도 치명적인 실수가 뮤텍스 이름을 너무 평범하게 짓는 것입니다. 만약 이름을 "MUTEX_NAME"이나 "MyApp"처럼 흔한 문자열로 지정하면, 우연히 다른 회사의 다른 프로그램이 같은 이름을 쓰고 있을 때 서로를 "이미 실행 중인 자기 자신"으로 오인합니다. 그 결과 정상 실행이 막혀 버리는 어이없는 버그가 생깁니다.

이를 막으려면 이름에 GUID를 포함시키는 것이 정석입니다.

// 좋은 예: 사람이 읽을 수 있는 접두어 + 충돌 거의 없는 GUID
_T("MyTool-{3F2C8A11-7B4D-4E0A-9C6E-AABBCCDDEEFF}")

Visual Studio에서 도구 > GUID 만들기(Create GUID) 메뉴로 무작위 GUID를 생성한 뒤, 프로그램 이름과 조합해 쓰면 사실상 충돌이 불가능합니다.

흔한 실수와 엣지 케이스

실무에서 단일 인스턴스 처리를 할 때 자주 걸려 넘어지는 지점들을 정리했습니다.

1. GetLastError를 다른 호출이 덮어쓰는 문제

GetLastError()직전 API 호출의 결과를 담고 있습니다. CreateMutex 직후에 MessageBox 같은 다른 함수를 먼저 호출하면, 그 함수가 마지막 에러 코드를 덮어써 ERROR_ALREADY_EXISTS 신호를 놓칠 수 있습니다. 반드시 CreateMutex 바로 다음 줄에서 GetLastError()를 확인하세요.

2. 핸들을 끝까지 들고 있어야 한다

일부 예제는 첫 인스턴스에서도 핸들을 닫아 버리는 실수를 합니다. 그러면 뮤텍스가 사라져, 두 번째 실행이 다시 "첫 인스턴스"로 인식됩니다. 첫 인스턴스의 핸들은 프로그램 수명 동안 살려두어야 합니다.

3. 전역(Global) vs 세션(Local) 네임스페이스

기본 뮤텍스 이름은 현재 터미널 세션 안에서만 유효합니다. 여러 사용자가 동시에 로그인하는 서버나 터미널 서비스 환경에서 모든 세션에 걸쳐 단 하나만 띄우려면 이름 앞에 Global\ 접두어를 붙입니다. 반대로 사용자별로 하나씩 허용하려면 Local\(기본값)을 그대로 둡니다.

// 모든 세션을 통틀어 단일 인스턴스로 제한
_T("Global\\MyTool-{3F2C8A11-7B4D-4E0A-9C6E-AABBCCDDEEFF}")

4. 기존 창을 앞으로 가져오기

사용자 입장에서는 "이미 실행 중입니다"라는 메시지보다, 이미 떠 있는 창이 앞으로 튀어나오는 것이 훨씬 자연스럽습니다. 이를 구현하려면 두 번째 인스턴스가 종료되기 전에 첫 인스턴스의 윈도우를 찾아(FindWindow) SetForegroundWindow로 활성화하는 코드를 추가합니다. 메시지박스만 띄우는 것보다 한 단계 더 완성도 높은 처리입니다.

5. 권한 상승(UAC)이 섞인 경우

일반 권한 프로세스와 관리자 권한 프로세스는 서로 다른 보안 컨텍스트에서 실행될 수 있어, 기본 이름의 뮤텍스가 서로 보이지 않을 수 있습니다. 권한 수준이 다른 실행까지 막아야 한다면 Global\ 네임스페이스와 적절한 보안 디스크립터(SECURITY_ATTRIBUTES) 설정을 함께 고려해야 합니다.

요약

  • 명명된 뮤텍스는 시스템에 이름당 하나만 존재하므로 단일 인스턴스 판별에 안성맞춤입니다.
  • CreateMutexGetLastError() == ERROR_ALREADY_EXISTS 패턴이 핵심이며, MFC에서는 InitInstance() 맨 앞에 넣습니다.
  • 뮤텍스 이름에는 반드시 GUID를 포함해 다른 프로그램과의 충돌을 차단하세요.
  • 두 번째 인스턴스는 핸들을 닫고 FALSE 반환, 첫 인스턴스는 핸들을 끝까지 유지합니다.
  • GetLastError는 직후에 확인, 세션 범위가 필요하면 Global\ 네임스페이스를 활용합니다.

이 패턴은 MFC뿐 아니라 Win32 API 기반의 모든 데스크톱 애플리케이션, 나아가 C# 같은 다른 언어에서도 동일한 원리(Mutex 클래스)로 그대로 적용됩니다.

AI에게 물어볼 때 (프롬프트 팁)

이 주제를 ChatGPT나 Claude 같은 AI에게 효과적으로 물어보면 코드 작성 시간을 크게 줄일 수 있습니다. 좋은 답을 얻으려면 환경·언어·제약 조건을 구체적으로 명시하는 것이 핵심입니다.

예시 1 — 코드 생성 요청

C++/MFC 데스크톱 앱을 단일 인스턴스로 제한하고 싶습니다.
CreateMutex와 ERROR_ALREADY_EXISTS를 사용하고,
뮤텍스 이름에는 GUID를 포함해 주세요.
InitInstance()에 넣을 코드와 각 줄의 의도를 한국어 주석으로 달아 주세요.

예시 2 — 더 나은 UX로 확장

위 코드를 개선해서, 두 번째 인스턴스가 실행될 때
"이미 실행 중" 메시지만 띄우는 대신
이미 떠 있는 첫 번째 인스턴스의 메인 윈도우를 찾아
앞으로 가져오도록(SetForegroundWindow) 만들어 주세요.
FindWindow에 쓸 윈도우 클래스/제목 처리 방법도 함께 설명해 주세요.

예시 3 — 엣지 케이스 점검

제 단일 인스턴스 코드가 일반 권한과 관리자 권한(UAC 상승)으로
각각 실행했을 때는 중복을 못 막습니다.
원인이 뭔지, Global\ 네임스페이스와 SECURITY_ATTRIBUTES를
어떻게 설정해야 권한이 다른 실행까지 막을 수 있는지 알려 주세요.

이처럼 "환경 → 요구사항 → 출력 형식(주석/설명 포함)" 순서로 질문을 구조화하면 AI가 훨씬 정확하고 바로 쓸 수 있는 답을 내놓습니다. 프롬프트를 어떻게 짜야 더 좋은 답이 나오는지 점검하고 싶다면 Prompt Architect의 프롬프트 분석기로 질문 문장을 진단해 보는 것도 방법입니다.