Windows의 MFC란 무엇인가 - 기존 자산을 유지보수하기 위한 기초 지식

· · Windows, MFC, VisualC++, Cpp, Win32, 네이티브 앱, 데스크톱 앱, 레거시 코드, 기존 자산 활용

1. 가장 먼저 파악해야 할 것

Windows 의 오래된 데스크톱 애플리케이션을 유지보수하다 보면, 이런 이름을 만나게 되는 경우가 있습니다.

CWinApp
CWnd
CDialog
CDialogEx
CFrameWnd
CDocument
CView
CString
CFile
CArchive
BEGIN_MESSAGE_MAP
ON_COMMAND
ON_BN_CLICKED
DoDataExchange
UpdateData

이들은 MFC라고 불리는 C++용 Windows 애플리케이션 프레임워크에서 자주 등장하는 것들입니다. MFC는 Microsoft Foundation Classes의 약자로, Win32 API를 C++ 클래스로 다루기 쉽게 만들기 위한 라이브러리입니다.

지금의 Windows 앱 개발에서는 WinUI, WPF, Windows Forms, Electron, Qt, 웹 기술 등 다양한 선택지가 있기 때문에, MFC를 신규 개발의 첫 번째 후보로 다루는 경우는 줄어들고 있습니다. 그렇다고 해서 사라진 기술은 아닙니다. 업무 애플리케이션, 계측기기, 제어 소프트웨어, CAD/CAM, 사내 도구, 오래전부터 있던 패키지 소프트웨어 등에서는 지금도 MFC 코드베이스를 유지보수하는 경우가 있습니다.

MFC를 이해하는 데 있어 가져야 할 시각을 먼저 제시합니다.

MFC는 오래된 Windows 데스크톱 앱을 읽기 위한 중요한 지도이다
MFC는 Win32 API를 감추는 것이 아니라 C++답게 감싼 것으로 생각한다
MFC의 작법을 모르면 코드의 겉모습 이상으로 동작을 잘못 읽게 된다
신규 채택보다 기존 자산의 유지보수・연명・단계적 이전에서 가치가 나온다

이 글에서는 MFC의 개요, 애플리케이션 구조, 메시지 맵, Document/View, 대화상자, DDX/DDV, 리소스, 빌드, 유지보수 시 주의점을 정리합니다.

또한 이 글에 등장하는 코드 조각은, 장별로 파일로 정리한 참조용 코드 모음으로 GitHub에 공개하고 있습니다.

windows-mfc-overview - komurasoft-blog-samples (GitHub)

2. MFC란 무엇인가

MFC는 Windows의 네이티브 데스크톱 애플리케이션을 C++로 만들기 위한 클래스 라이브러리입니다.

Win32 API를 직접 사용할 경우, 일반적으로 다음과 같은 코드를 작성합니다.

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message)
    {
    case WM_PAINT:
        // 그리기 처리
        break;
    case WM_DESTROY:
        PostQuitMessage(0);
        break;
    default:
        return DefWindowProc(hWnd, message, wParam, lParam);
    }
    return 0;
}

Win32 API는 매우 강력하지만, C의 함수, 핸들, 메시지, 콜백을 중심으로 구성하기 때문에, 큰 애플리케이션에서는 가독성이 떨어지기 쉽습니다. MFC는 이를 C++ 클래스로 다룰 수 있게 만든 것으로, 창은 CWnd, 대화상자는 CDialog, 애플리케이션 전체는 CWinApp, 프레임 창은 CFrameWnd, 뷰는 CView와 같은 클래스로 표현됩니다.

class CMainFrame : public CFrameWnd
{
public:
    CMainFrame();

protected:
    afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);
    DECLARE_MESSAGE_MAP()
};

MFC는 Win32 API를 완전히 다른 것으로 대체하는 것이 아니라, Win32 API의 사고방식을 토대로 한, 얇지만 광범위한 C++ 프레임워크입니다. 그렇기 때문에 MFC를 읽으려면, MFC의 클래스뿐 아니라 다음과 같은 지식도 필요합니다.

Windows 메시지
HWND 등의 핸들
GDI/GDI+
리소스 파일
COM/OLE
DLL과 런타임
문자 코드
스레드와 메시지 루프

“Windows를 몰라도 쓸 수 있는 마법 같은 라이브러리”라기보다, “Windows의 동작 방식을 C++ 타입과 프레임워크로 정리한 것”이라는 쪽이 MFC의 실체에 가깝습니다.

3. MFC는 지금도 쓸 수 있는가

MFC는 현재도 Visual Studio에서 사용할 수 있습니다. 다만 위치에 대한 오해는 금물입니다. 지금도 지원되고 있기는 하지만, 활발히 새로운 기능이 추가되어 가는 최신 UI 프레임워크는 아니며, Microsoft의 MFC 문서에도 MFC는 계속 지원되는 한편 새로운 기능 추가나 문서 갱신은 이루어지지 않는다는 안내가 있습니다.

이 때문에 MFC의 위치는 대략 다음과 같습니다.

기존 MFC 앱의 유지보수         -> 현실적으로 자주 있음
기존 MFC 앱의 기능 추가         -> 있을 수 있음
MFC 앱의 빌드 환경 갱신         -> 중요
MFC에서 다른 UI로의 단계적 이전  -> 있을 수 있음
완전 신규의 일반적인 GUI 앱에서 채택 -> 신중하게 판단

특히 오랫동안 사용되어 온 업무 애플리케이션에서는, UI, 인쇄, 파일 입출력, 장치 제어, 독자 프로토콜, COM 연동 등이 MFC 안에 정리되어 있는 경우가 있습니다.

그런 코드베이스에서는, “MFC를 버리는 것”보다 먼저 “MFC를 읽을 수 있는 상태로 만드는 것”이 필요합니다.

4. MFC가 강점을 보인 영역

MFC가 사용되어 온 대표적인 영역은 Windows의 네이티브 데스크톱 애플리케이션입니다.

구체적으로는 다음과 같은 애플리케이션입니다.

대화상자 중심의 업무 도구
파일을 열어 편집하는 SDI 앱
여러 문서를 다루는 MDI 앱
계측기기나 제조 장치의 제어 화면
CAD/CAM 계열의 네이티브 앱
인쇄나 미리보기를 많이 쓰는 앱
ActiveX나 OLE 연동을 포함하는 앱
오래된 Windows API나 COM 자산과 밀접한 앱

MFC의 강점은 Windows 네이티브 구성 요소와 가까운 곳에서 동작한다는 점입니다. 창, 메뉴, 도구 모음, 상태 표시줄, 대화상자, 공용 컨트롤, 인쇄, 파일 대화상자, 레지스트리, GDI 드로잉 등을 C++ 클래스로 다룰 수 있습니다.

한편 MFC의 약점은, 현대적인 UI 구축, 데이터 바인딩, 테스트 용이성, 비동기 처리, 모던한 레이아웃, 고 DPI 대응, 다국어화, 접근성 등을 최근 프레임워크만큼 자연스럽게 작성할 수 없다는 점입니다.

특징을 나열하면 다음과 같습니다.

Windows 네이티브에 가깝다
C++로 직접적으로 제어할 수 있다
기존 자산이 많다
Win32 지식이 필요하다
오래된 작법이 많다
테스트하기 쉬운 구조는 스스로 갖추어야 한다

5. Visual Studio에서 MFC를 사용할 준비

Visual Studio에 C++를 설치했다고 해서 MFC가 반드시 함께 들어있는 것은 아닙니다. MFC는 Visual Studio Installer의 개별 구성 요소로 취급되기 때문입니다. 대표적으로는 다음과 같은 구성 요소를 확인합니다.

C++를 사용한 데스크톱 개발
MSVC v143 - VS 2022 C++ x64/x86 build tools
Windows SDK
C++ MFC for latest v143 build tools
C++ ATL for latest v143 build tools
Spectre Mitigations 버전의 MFC가 필요한지 여부

빌드 시 MFC 관련 파일을 찾을 수 없는 경우, 프로젝트 설정뿐 아니라 Visual Studio 쪽에 MFC 구성 요소가 설치되어 있는지를 확인합니다.

CI 환경이나 빌드 서버에서도 마찬가지로, 로컬 Visual Studio에서는 빌드되는데 CI에서는 실패하는 경우, MFC 구성 요소의 유무나 대상 툴셋 버전의 차이가 원인이 되는 경우가 있습니다.

6. MFC 애플리케이션의 기본 구조

MFC 애플리케이션은 대략 다음과 같은 구조를 가집니다.

CWinApp 파생 클래스
  애플리케이션 전체의 초기화와 종료를 담당한다

CFrameWnd / CMDIFrameWnd / CDialog 파생 클래스
  메인 창이나 대화상자를 담당한다

CView 파생 클래스
  화면 표시와 사용자 조작을 담당한다

CDocument 파생 클래스
  데이터나 파일 저장을 담당한다

리소스 파일
  메뉴, 대화상자, 아이콘, 문자열 등을 보관한다

메시지 맵
  Windows 메시지나 명령을 핸들러 함수와 연결한다

예를 들어, 단순한 MFC 앱에서는 다음과 같은 CWinApp 파생 클래스가 나옵니다.

class CMyApp : public CWinApp
{
public:
    virtual BOOL InitInstance();
};

CMyApp theApp;

BOOL CMyApp::InitInstance()
{
    CWinApp::InitInstance();

    CMainFrame* pFrame = new CMainFrame;
    m_pMainWnd = pFrame;

    pFrame->Create(nullptr, _T("My MFC Application"));
    pFrame->ShowWindow(SW_SHOW);
    pFrame->UpdateWindow();

    return TRUE;
}

CWinApp은 애플리케이션 전체를 나타내는 클래스입니다. MFC 애플리케이션에서는 보통 CWinApp에서 파생된 객체가 하나 존재합니다.

theApp과 같은 전역 객체는, 처음에는 낯설게 느껴질 수 있지만, MFC에서는 이것이 표준적인 구조입니다.

7. CWinApp은 무엇을 하고 있는가

CWinApp은 MFC 애플리케이션의 진입점으로서 중요합니다. 일반적인 Win32 애플리케이션에서는 WinMain, 창 클래스 등록, 메시지 루프 등을 직접 작성하지만, MFC에서는 그 대부분을 프레임워크가 대신 처리합니다. 개발자는 주로 InitInstance를 오버라이드하여 애플리케이션 고유의 초기화를 작성합니다.

BOOL CMyApp::InitInstance()
{
    CWinApp::InitInstance();

    // 설정 읽기
    // COM 초기화
    // 메인 창 생성
    // 문서 템플릿 등록

    return TRUE;
}

InitInstance에 자주 작성되는 것은 다음과 같은 처리입니다.

공용 컨트롤 초기화
레지스트리 키 설정
최근 사용한 파일 목록 읽기
문서 템플릿 등록
메인 프레임 생성
명령줄 인수 처리
COM/OLE 초기화

유지보수 시에는 먼저 CWinApp 파생 클래스를 확인하면, 애플리케이션 전체의 시작 순서가 잘 보이게 됩니다.

8. CWnd는 MFC의 중심에 있는 클래스

MFC UI 클래스의 대부분은, Windows의 창을 나타내는 CWnd를 기반으로 합니다. 다만 CWnd 객체와 HWND는 같은 것이 아닙니다.

HWND
  Windows OS가 관리하는 창 핸들

CWnd
  HWND를 다루기 쉽게 만들기 위한 C++ 래퍼 객체

MFC에서는 CWnd가 내부에 HWND를 가집니다.

HWND hWnd = m_hWnd;

또는 다음과 같이 가져옵니다.

HWND hWnd = GetSafeHwnd();

유지보수 시에 중요한 것은, CWnd*가 존재하고 있어도, 대응하는 HWND가 이미 파괴되어 있을 가능성이 있다는 점입니다.

그렇기 때문에 창이 유효한지 여부는 다음과 같은 형태로 확인합니다.

if (pWnd != nullptr && ::IsWindow(pWnd->GetSafeHwnd()))
{
    pWnd->ShowWindow(SW_SHOW);
}

MFC의 버그 조사에서는 CWnd의 C++ 객체 수명과, 실제 Windows 창 핸들의 수명이 어긋나 있지 않은지를 보는 것이 중요합니다.

9. 메시지 맵이란 무엇인가

MFC스러움이 가장 두드러지는 메커니즘 중 하나가 메시지 맵입니다.

Windows 애플리케이션은 마우스 클릭, 키 입력, 다시 그리기, 창 크기 변경, 메뉴 선택 등을, Windows 메시지로 받습니다.

Win32 API에서는 보통 WndProcswitch문으로 메시지를 처리합니다.

MFC에서는 이를 메시지 맵을 통해 핸들러 함수와 연결합니다.

BEGIN_MESSAGE_MAP(CMyDialog, CDialogEx)
    ON_BN_CLICKED(IDC_BUTTON_OK, &CMyDialog::OnClickedButtonOk)
    ON_WM_CLOSE()
END_MESSAGE_MAP()

void CMyDialog::OnClickedButtonOk()
{
    AfxMessageBox(_T("Clicked"));
}

이 코드의 의미는 다음과 같습니다.

IDC_BUTTON_OK라는 버튼이 클릭되면
CMyDialog::OnClickedButtonOk를 호출한다

MFC를 읽는 데 익숙하지 않으면, 함수가 어디에서 호출되는지 알기 어렵게 느껴집니다. 검색해도 직접적인 호출을 찾을 수 없는 경우에는 메시지 맵을 봅니다.

함수가 직접 호출되지 않는다
그런데 이벤트 시에 실행되고 있다
-> BEGIN_MESSAGE_MAP / ON_... 매크로를 확인한다

MFC 코드 리뷰에서는 핸들러 함수만 보는 것이 아니라, 메시지 맵과 함께 확인하는 것이 중요합니다.

10. 명령 라우팅

MFC에서는 메뉴나 도구 모음의 조작도 명령으로 다루어집니다.

대표적인 것이 ON_COMMAND입니다.

BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd)
    ON_COMMAND(ID_FILE_OPEN, &CMainFrame::OnFileOpen)
END_MESSAGE_MAP()

void CMainFrame::OnFileOpen()
{
    // 파일 열기 처리
}

MFC에는 명령을 적절한 객체로 전달하는 메커니즘이 있습니다.

예를 들어, 같은 ID_EDIT_COPY라도, 현재 활성화된 뷰, 문서, 프레임, 애플리케이션 중 어느 것이 처리하는지가 달라질 수 있습니다.

활성 뷰
문서
프레임 창
애플리케이션

이런 순서로, 처리할 수 있는 객체에게 명령이 전달됩니다.

그렇기 때문에 MFC에서는 “메뉴를 누르면 어떤 함수가 호출되는가”를 단순한 문자열 검색만으로 추적하기 어려운 경우가 있습니다.

유지보수 시에 보아야 할 점은 다음과 같습니다.

명령 ID는 무엇인가
ON_COMMAND는 어느 클래스에 있는가
ON_UPDATE_COMMAND_UI는 어디에 있는가
현재 활성 뷰는 어느 것인가
Document/View 구조를 사용하고 있는가

11. ON_UPDATE_COMMAND_UI란 무엇인가

MFC에서는 메뉴 항목이나 도구 모음 버튼의 활성화/비활성화, 체크 상태, 표시 텍스트 등을 갱신하기 위해 ON_UPDATE_COMMAND_UI를 사용하는 경우가 있습니다.

BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd)
    ON_COMMAND(ID_EDIT_DELETE, &CMainFrame::OnEditDelete)
    ON_UPDATE_COMMAND_UI(ID_EDIT_DELETE, &CMainFrame::OnUpdateEditDelete)
END_MESSAGE_MAP()

void CMainFrame::OnUpdateEditDelete(CCmdUI* pCmdUI)
{
    pCmdUI->Enable(CanDeleteCurrentItem());
}

이를 통해 삭제할 수 있는 상태일 때만 메뉴나 버튼을 활성화할 수 있습니다.

MFC 앱을 사용하다가, 버튼이 왜인지 비활성화되어 있다거나, 메뉴를 누를 수 없다거나, 체크 상태가 바뀐다는 등의 동작을 조사할 때는, ON_UPDATE_COMMAND_UI를 찾아보면 원인을 발견할 수 있는 경우가 있습니다.

12. 대화상자 기반의 MFC 앱

MFC에서 가장 알기 쉬운 형태 중 하나가 대화상자 기반 애플리케이션입니다.

설정 화면, 간단한 업무 도구, 장치 조작 화면 등에서는 대화상자를 중심으로 구성되어 있는 경우가 있습니다.

일반적으로는 CDialog 또는 CDialogEx를 상속합니다.

class CSettingsDialog : public CDialogEx
{
public:
    CSettingsDialog(CWnd* pParent = nullptr);

#ifdef AFX_DESIGN_TIME
    enum { IDD = IDD_SETTINGS_DIALOG };
#endif

protected:
    virtual void DoDataExchange(CDataExchange* pDX);
    virtual BOOL OnInitDialog();

    afx_msg void OnBnClickedOk();
    DECLARE_MESSAGE_MAP()

private:
    CString m_name;
    int m_interval;
};

대화상자 기반 코드에서는 다음 요소가 자주 등장합니다.

IDD_...        대화상자 리소스 ID
IDC_...        컨트롤 ID
OnInitDialog   초기화 처리
DoDataExchange 컨트롤과 멤버 변수의 연결
UpdateData     화면과 변수의 동기화
ON_BN_CLICKED  버튼 클릭 처리

대화상자는 겉모습뿐 아니라, 리소스, 멤버 변수, 메시지 맵, 초기화 처리가 결합되어 동작합니다.

13. DDX와 DDV

MFC 대화상자에서 자주 등장하는 것이 DDXDDV입니다.

DDX = Dialog Data Exchange
DDV = Dialog Data Validation

DDX는 대화상자 위의 컨트롤과 C++ 멤버 변수를 대응시키는 메커니즘이고, DDV는 그 입력값을 검증하는 메커니즘입니다.

void CSettingsDialog::DoDataExchange(CDataExchange* pDX)
{
    CDialogEx::DoDataExchange(pDX);
    DDX_Text(pDX, IDC_EDIT_NAME, m_name);
    DDX_Text(pDX, IDC_EDIT_INTERVAL, m_interval);
    DDV_MinMaxInt(pDX, m_interval, 1, 3600);
}

UpdateData(TRUE)를 호출하면, 화면의 입력값이 멤버 변수에 반영됩니다.

void CSettingsDialog::OnBnClickedOk()
{
    if (!UpdateData(TRUE))
    {
        return;
    }

    // 여기서는 m_name과 m_interval에 화면 입력값이 들어 있다
    SaveSettings(m_name, m_interval);

    CDialogEx::OnOK();
}

반대로 UpdateData(FALSE)를 호출하면, 멤버 변수의 값이 화면에 반영됩니다.

BOOL CSettingsDialog::OnInitDialog()
{
    CDialogEx::OnInitDialog();

    m_name = _T("default");
    m_interval = 60;
    UpdateData(FALSE);

    return TRUE;
}

MFC 대화상자에서 입력값이 이상할 때는 다음을 확인합니다.

DoDataExchange에 DDX가 정의되어 있는가
UpdateData(TRUE)를 호출하고 있는가
UpdateData(FALSE)를 호출하는 시점이 올바른가
DDV에서 입력이 걸러지고 있지 않은가
컨트롤 ID가 리소스와 일치하는가

14. Document/View 아키텍처

MFC의 큰 특징 중 하나가 Document/View 아키텍처입니다.

이는 애플리케이션이 다루는 데이터와 그 표시를 분리하기 위한 구조입니다.

CDocument
  데이터를 보관한다
  파일의 읽기/쓰기를 담당한다
  여러 뷰에 갱신을 통지한다

CView
  데이터를 표시한다
  사용자 조작을 다룬다
  그리기와 선택 상태를 관리한다

예를 들어, 텍스트 편집기, 도형 편집기, 설정 파일 편집 도구, CAD 같은 앱에서는 데이터와 표시를 분리하는 것에 의미가 있습니다.

class CMyDocument : public CDocument
{
public:
    std::vector<Item> m_items;

    virtual BOOL OnOpenDocument(LPCTSTR lpszPathName);
    virtual BOOL OnSaveDocument(LPCTSTR lpszPathName);
};

class CMyView : public CView
{
protected:
    virtual void OnDraw(CDC* pDC);

    CMyDocument* GetDocument() const;
};

뷰 쪽에서는 문서를 가져와 그립니다.

void CMyView::OnDraw(CDC* pDC)
{
    CMyDocument* pDoc = GetDocument();
    if (pDoc == nullptr)
    {
        return;
    }

    for (const auto& item : pDoc->m_items)
    {
        // pDC를 사용하여 그린다
    }
}

Document/View의 장점은 같은 데이터를 여러 뷰에서 표시하기 쉽다는 것입니다.

예를 들어, 같은 데이터를 다음과 같이 보여줄 수 있습니다.

표 형식 뷰
그래프 뷰
상세 뷰
미리보기
인쇄 뷰

다만, 단순한 설정 화면이나 작은 도구에 Document/View를 사용하면 오히려 구조가 무겁게 느껴질 수도 있습니다.

유지보수 시에는, 그 앱이 Document/View를 사용하고 있는지, 대화상자 중심인지를 먼저 파악하면 코드를 따라가기 쉬워집니다.

15. SDI와 MDI

MFC에서는 Document/View와 함께, SDI와 MDI라는 구조가 자주 나옵니다.

SDI = Single Document Interface
MDI = Multiple Document Interface

SDI는 기본적으로 하나의 프레임에서 하나의 문서를 다루는 형태입니다.

메인 창
  하나의 문서
  하나 또는 여러 뷰

MDI는 하나의 부모 창 안에 여러 자식 창을 가지고, 각각이 문서를 다루는 형태입니다.

MDI 부모 프레임
  MDI 자식 프레임 1 -> 문서 1
  MDI 자식 프레임 2 -> 문서 2
  MDI 자식 프레임 3 -> 문서 3

오래된 Windows 애플리케이션에서는 MDI가 자주 사용되었습니다.

유지보수 시에는 클래스 이름을 보면 구조를 짐작할 수 있습니다.

CFrameWnd       SDI 계열의 프레임
CMDIFrameWnd    MDI 부모 프레임
CMDIChildWnd    MDI 자식 프레임
CSingleDocTemplate SDI용 문서 템플릿
CMultiDocTemplate  MDI용 문서 템플릿

MFC 마법사로 만들어진 앱에서는, InitInstance 안에 CSingleDocTemplateCMultiDocTemplate의 등록 처리가 있는 경우가 많습니다.

16. 리소스 파일 이해하기

MFC 앱에서는 .rc 파일이 매우 중요합니다.

.rc는 Windows의 리소스 파일입니다.

여기에는 다음과 같은 것들이 정의되어 있습니다.

대화상자 템플릿
메뉴
액셀러레이터 키
아이콘
비트맵
문자열 테이블
버전 정보
도구 모음

또한 resource.h에는 리소스 ID가 정의됩니다.

#define IDD_SETTINGS_DIALOG  101
#define IDC_EDIT_NAME        1001
#define IDC_EDIT_INTERVAL    1002
#define ID_FILE_OPEN         32771

MFC 코드에서는 이러한 ID를 사용하여 리소스와 C++ 코드를 연결합니다.

DDX_Text(pDX, IDC_EDIT_NAME, m_name);
ON_COMMAND(ID_FILE_OPEN, &CMainFrame::OnFileOpen)

유지보수 시에 흔히 발생하는 문제는 리소스 ID의 불일치입니다.

resource.h의 ID가 바뀌었다
다른 브랜치와의 병합으로 ID가 충돌했다
대화상자 위의 컨트롤 ID와 DDX의 ID가 맞지 않는다
삭제했어야 할 메뉴 ID가 남아 있다
문자열 테이블의 ID가 중복되어 있다

MFC 앱의 동작을 조사할 때는, C++ 코드뿐 아니라 .rcresource.h도 함께 봐야 합니다.

17. Class Wizard와 수작업 코드

MFC에는 Visual Studio의 Class Wizard와 깊이 연결된 역사가 있습니다.

Class Wizard를 사용하면, 메시지 핸들러, DDX 변수, 가상 함수 오버라이드 등을 자동으로 생성할 수 있습니다.

그렇기 때문에 MFC 코드에는 도구가 생성한 형태의 코드가 많이 남아 있습니다.

//{{AFX_DATA(CSettingsDialog)
//}}AFX_DATA

//{{AFX_MSG(CSettingsDialog)
//}}AFX_MSG

새로운 Visual Studio에서는 보이는 형태나 생성되는 형식이 달라진 부분도 있지만, 오래된 코드베이스에서는 이러한 주석 마커가 남아 있는 경우가 있습니다.

유지보수 시에 중요한 것은, 생성된 코드와 수작업 코드의 경계를 함부로 무너뜨리지 않는 것입니다.

메시지 맵을 삭제하지 않는다
DDX의 대응을 깨뜨리지 않는다
리소스 ID를 함부로 바꾸지 않는다
오래된 Class Wizard 전제의 주석을 무턱대고 지우지 않는다

MFC에서는 단순히 C++로서 컴파일이 되는 것만으로는 충분하지 않습니다. Visual Studio의 리소스 편집기나 Class Wizard가 기대하는 형태도 어느 정도 유지해야 합니다.

18. CString과 문자열

MFC에서 자주 등장하는 문자열 클래스가 CString입니다.

CString name = _T("Komura");
CString message;
message.Format(_T("Hello, %s"), name.GetString());

CString은 MFC/ATL 계열 코드에서 자주 사용되는 가변 길이 문자열 클래스입니다. 현대 C++에서는 std::string이나 std::wstring을 사용하는 경우가 많지만, MFC에서는 API나 컨트롤과의 호환성 때문에 CString이 많이 사용됩니다.

유지보수 시에는 문자 코드를 의식해야 합니다.

CString      프로젝트 설정에 따라 CStringA 또는 CStringW에 상당
CStringA     ANSI / MBCS 계열
CStringW     Unicode / UTF-16 계열
LPCTSTR      TCHAR 기반의 문자열 포인터
LPCSTR       char 계열
LPCWSTR      wchar_t 계열
std::string  보통 char 계열
std::wstring wchar_t 계열

지금의 Windows 앱에서는 기본적으로 Unicode를 전제로 하는 것이 안전하지만, 오래된 MFC 앱에서는 MBCS를 전제로 한 코드가 남아 있는 경우가 있습니다.

CString text = _T("한국어");
std::wstring ws(text.GetString());

문자열 변환 버그는 MFC 앱 유지보수에서 자주 발생합니다.

특히 주의해야 할 것은 다음과 같은 경우입니다.

Shift_JIS를 전제로 한 파일을 읽는다
Unicode 빌드로 전환한다
외부 DLL이 char*를 요구한다
COM이 BSTR을 요구한다
std::string으로 안이하게 변환하여 문자가 깨진다

CString을 보면, 단순히 “오래된 문자열 클래스다”라고만 생각하지 말고, 프로젝트의 문자 집합 설정, 외부 API, 파일 형식과 함께 확인하는 것이 중요합니다.

19. CFile과 CArchive

MFC에는 파일 조작이나 직렬화를 위한 클래스도 있습니다. 대표적인 것이 CFileCArchive입니다.

CFile file;
if (file.Open(path, CFile::modeRead))
{
    CArchive ar(&file, CArchive::load);
    // ar에서 읽어들인다
}

CArchive는 MFC의 직렬화 메커니즘에서 자주 사용됩니다.

CDocument 파생 클래스에서는 Serialize를 오버라이드하여, 읽기와 저장을 같은 함수에 작성하는 경우가 있습니다.

void CMyDocument::Serialize(CArchive& ar)
{
    if (ar.IsStoring())
    {
        ar << m_title;
        ar << static_cast<int>(m_items.size());
        for (const auto& item : m_items)
        {
            ar << item.Name;
            ar << item.Value;
        }
    }
    else
    {
        int count = 0;
        ar >> m_title;
        ar >> count;
        m_items.clear();
        for (int i = 0; i < count; ++i)
        {
            Item item;
            ar >> item.Name;
            ar >> item.Value;
            m_items.push_back(item);
        }
    }
}

MFC의 직렬화는 편리하지만, 장기 운용에서는 주의가 필요합니다.

오래된 파일 형식과의 호환성
버전 번호 관리
읽기 실패 시 복구
예외 처리
문자 코드
엔디안
구조체를 그대로 저장하고 있지 않은가

독자적인 바이너리 형식을 오래 사용해 온 MFC 앱에서는, Serialize가 사실상의 파일 사양이 되어 있는 경우도 있습니다.

이 경우, 코드 변경 전에 기존 파일을 읽어들이는 테스트 데이터를 반드시 준비하는 것이 좋습니다.

20. GDI 드로잉과 CDC

MFC의 화면 드로잉에서는, Windows의 Device Context를 다루기 위한 CDC 클래스가 자주 사용됩니다. CView::OnDraw에서는 인자로 CDC*가 전달됩니다.

void CMyView::OnDraw(CDC* pDC)
{
    pDC->TextOut(10, 10, _T("Hello MFC"));
    pDC->Rectangle(10, 40, 200, 120);
}

펜이나 브러시를 사용할 때는 선택과 복원에 주의합니다.

void CMyView::OnDraw(CDC* pDC)
{
    CPen pen(PS_SOLID, 1, RGB(0, 0, 0));
    CPen* pOldPen = pDC->SelectObject(&pen);

    pDC->MoveTo(10, 10);
    pDC->LineTo(100, 100);

    pDC->SelectObject(pOldPen);
}

GDI 객체 관련해서는 다음과 같은 실수가 문제가 됩니다.

SelectObject한 후에 원래대로 되돌리지 않는다
GDI 객체를 대량으로 만들고 해제하지 않는다
OnPaint와 OnDraw의 역할을 혼동한다
더블 버퍼링을 하지 않아 화면이 깜박인다
고 DPI에서 고정 픽셀 전제의 드로잉이 깨진다

MFC 드로잉 버그에서는, C++ 로직뿐 아니라 Windows의 GDI 리소스, 다시 그리기 시점, DPI, 폰트 크기를 확인해야 합니다.

21. 모달 대화상자와 모덜리스 대화상자

MFC에서는 대화상자를 표시하는 방식에도 주의가 필요합니다.

모달 대화상자는 DoModal로 표시합니다.

CSettingsDialog dlg(this);
if (dlg.DoModal() == IDOK)
{
    // OK 시 처리
}

이 경우, 대화상자가 닫힐 때까지 호출한 쪽은 대기합니다.

한편 모덜리스 대화상자는, 생성 후에도 호출한 쪽의 처리가 돌아옵니다.

m_pToolDialog = new CToolDialog(this);
m_pToolDialog->Create(IDD_TOOL_DIALOG, this);
m_pToolDialog->ShowWindow(SW_SHOW);

모덜리스 대화상자에서는 수명 관리가 중요합니다.

new한 대화상자를 언제 delete할 것인가
부모 창이 먼저 파괴되지 않는가
대화상자 쪽에서 PostNcDestroy를 사용하는가
이중 생성되지 않는가
닫은 후의 포인터가 남아 있지 않은가

MFC 충돌 조사에서는, 모덜리스 대화상자의 수명 문제가 원인이 되는 경우가 있습니다.

22. C++ 객체와 Windows 핸들의 수명

MFC에서 매우 중요한 것이, C++ 객체와 Windows 핸들의 수명 차이입니다. 예를 들어 CWnd는 C++ 객체이지만, 실제 창은 Windows가 HWND로 관리하고 있으며, 이 둘은 항상 동시에 생성되고 동시에 사라지는 것은 아닙니다.

CWnd 객체는 있지만 HWND는 아직 없다
HWND는 파괴되었지만 CWnd 객체는 남아 있다
임시 CWnd 래퍼가 만들어져 있다
Attach/Detach로 핸들을 교체하고 있다

예를 들어, 다음과 같은 코드는 주의가 필요합니다.

CWnd* pWnd = GetDlgItem(IDC_SOME_CONTROL);
// pWnd를 멤버에 저장해서 나중에 사용한다

GetDlgItem으로 얻은 포인터를 장기간 보유할 경우, 창이 파괴된 후에 참조해 버릴 위험이 있습니다.

필요하다면, 매번 GetDlgItem을 호출하거나, 컨트롤용 멤버 변수를 DDX로 관리하는 쪽이 더 안전한 경우가 있습니다.

DDX_Control(pDX, IDC_LIST_ITEMS, m_listItems);

MFC에서는 포인터가 null이 아니어도 안전하다고는 할 수 없습니다.

if (m_pDialog != nullptr && ::IsWindow(m_pDialog->GetSafeHwnd()))
{
    m_pDialog->SetWindowText(_T("Running"));
}

이 감각은 MFC 유지보수에서 매우 중요합니다.

23. 스레드와 UI 갱신

Windows의 UI는 기본적으로 생성된 UI 스레드에서 조작해야 하며, MFC 앱에서도 마찬가지입니다. 작업자 스레드에서 직접 UI 컨트롤을 조작하면 불안정한 동작이나 충돌의 원인이 됩니다.

피해야 할 예입니다.

UINT WorkerThreadProc(LPVOID pParam)
{
    CMyDialog* pDlg = static_cast<CMyDialog*>(pParam);

    // 작업자 스레드에서 직접 UI를 다루는 것은 피한다
    pDlg->SetDlgItemText(IDC_STATUS, _T("Done"));

    return 0;
}

일반적으로는 PostMessage 등을 사용하여 UI 스레드에 통지합니다.

constexpr UINT WM_APP_WORK_DONE = WM_APP + 1;

UINT WorkerThreadProc(LPVOID pParam)
{
    HWND hWnd = static_cast<HWND>(pParam);

    // 무거운 처리

    ::PostMessage(hWnd, WM_APP_WORK_DONE, 0, 0);
    return 0;
}

UI 쪽에서는 메시지 맵으로 받습니다.

BEGIN_MESSAGE_MAP(CMyDialog, CDialogEx)
    ON_MESSAGE(WM_APP_WORK_DONE, &CMyDialog::OnWorkDone)
END_MESSAGE_MAP()

LRESULT CMyDialog::OnWorkDone(WPARAM, LPARAM)
{
    SetDlgItemText(IDC_STATUS, _T("Done"));
    return 0;
}

MFC의 스레드 관련해서는 다음을 확인합니다.

UI를 작업자 스레드에서 직접 다루고 있지 않은가
창 파괴 후에 PostMessage하고 있지 않은가
스레드 종료 대기로 UI 스레드를 멈추고 있지 않은가
공유 데이터의 잠금은 적절한가
AfxBeginThread의 반환값과 수명을 잘못 이해하고 있지 않은가

24. MFC DLL과 모듈 상태

MFC로 DLL을 만들 경우, 모듈 상태라는 개념이 나옵니다.

MFC DLL에서 리소스를 읽어들이거나, 대화상자를 표시하거나, 확장 DLL을 만드는 등의 경우, 어느 모듈의 리소스를 참조할지가 문제가 됩니다.

MFC DLL의 함수 진입점에서, 다음과 같은 매크로를 볼 수 있습니다.

AFX_MANAGE_STATE(AfxGetStaticModuleState());

이는 MFC가 올바른 모듈 상태를 사용할 수 있도록 하기 위한 것입니다.

이 매크로를 잊으면 다음과 같은 문제로 이어집니다.

DLL 내의 대화상자 리소스를 찾을 수 없다
문자열 리소스가 다른 모듈에서 읽힌다
아이콘이나 메뉴를 찾을 수 없다
디버그 시에만 동작하고 릴리스에서 깨진다

MFC DLL을 유지보수할 때는, EXE, 일반 DLL, 확장 DLL, 리소스 DLL의 관계를 정리하는 것이 중요합니다.

특히 비 MFC 앱에서 MFC DLL을 호출하는 경우나, 플러그인 구성인 경우는 주의가 필요합니다.

25. MFC를 정적으로 링크할 것인가 공유 DLL로 사용할 것인가

MFC 앱에서는 프로젝트 설정에 “Use of MFC”가 있습니다.

대표적인 선택지는 다음 두 가지입니다.

Use MFC in a Shared DLL
Use MFC in a Static Library

공유 DLL을 사용할 경우, 실행 환경에 대응하는 MFC 런타임이나 Visual C++ 런타임이 필요합니다.

정적 링크의 경우, 배포물이 단순해 보일 수 있지만, 실행 파일 크기, 갱신, 보안 수정 반영, 라이선스나 재배포 조건 등을 고려해야 합니다.

어느 쪽이 항상 정답이라고는 할 수 없습니다.

판단 기준은 다음과 같습니다.

배포 대상에 Visual C++ Redistributable을 설치할 수 있는가
앱을 단일 exe에 가깝게 만들고 싶은가
보안 업데이트를 어떻게 반영할 것인가
여러 앱에서 같은 런타임을 공유할 것인가
설치 프로그램을 준비할 수 있는가
대상 Windows 버전은 무엇인가

유지보수에서는 먼저 현재 설정을 확인하는 것이 중요합니다.

Configuration Properties
  General
    Use of MFC

더불어 Runtime Library 설정도 확인합니다.

/MD   Multi-threaded DLL
/MDd  Multi-threaded Debug DLL
/MT   Multi-threaded
/MTd  Multi-threaded Debug

MFC와 CRT의 링크 설정이 혼재하면, 라이브러리 경계에서 메모리 할당/해제 문제가 발생할 수 있습니다.

26. Unicode, MBCS, TCHAR

오래된 MFC 코드에서는 TCHAR, LPCTSTR, _T() 매크로가 자주 등장합니다.

CString title = _T("설정");
SetWindowText(title);

이는 Unicode 빌드와 MBCS 빌드 양쪽에 대응하기 위한 작성 방식입니다.

Unicode 빌드
  TCHAR    -> wchar_t
  LPCTSTR  -> const wchar_t*
  _T("...") -> L"..."

MBCS 빌드
  TCHAR    -> char
  LPCTSTR  -> const char*
  _T("...") -> "..."

지금은 Unicode 빌드가 일반적이지만, 오래된 앱에서는 MBCS를 전제로 한 처리가 남아 있는 경우가 있습니다.

특히 외부 파일, 통신 프로토콜, 오래된 DLL, 데이터베이스 연결, 시리얼 통신 등에서는 문자 코드에 대한 전제를 확인해야 합니다.

주의해야 할 것은, Unicode화를 단순한 치환 작업으로 생각하지 않는 것입니다.

char 배열의 크기는 바이트 수인가 문자 수인가
strlen을 사용하고 있지 않은가
sizeof(buffer)를 문자 수로 사용하고 있지 않은가
외부 API는 UTF-16을 받는가 Shift_JIS를 받는가
파일 저장 형식은 바뀌어도 되는가

MFC의 문자열 관련을 수정할 때는, 화면 표시뿐 아니라 파일 호환성과 외부 연동까지 확인합니다.

27. MFC와 COM/OLE/ActiveX

MFC는 COM, OLE, ActiveX와 관계가 깊은 애플리케이션에서도 사용되어 왔습니다.

오래된 업무 앱에서는 다음과 같은 요소가 남아 있는 경우가 있습니다.

OLE Automation
ActiveX Control
COM 서버
COM 클라이언트
IDispatch
BSTR
VARIANT
COleDispatchDriver
COleVariant

MFC 앱의 시작 처리에는 다음과 같은 코드가 나올 수 있습니다.

if (!AfxOleInit())
{
    AfxMessageBox(_T("OLE initialization failed"));
    return FALSE;
}

COM/OLE를 사용하고 있는 경우, MFC의 문제로 보이지만 실제로는 COM의 초기화, 스레드 모델, 참조 카운트, 등록 정보, 32bit/64bit의 차이가 원인인 경우가 있습니다.

특히 주의해야 할 것은 32bit ActiveX나 COM 구성 요소입니다.

32bit MFC 앱에서는 32bit COM을 사용한다
64bit MFC 앱에서는 64bit COM을 사용한다
32bit/64bit의 COM 등록은 별개이다
오래된 ActiveX가 64bit에 대응하지 않는 경우가 있다

MFC 앱을 x64화할 때는 UI 코드뿐 아니라 COM/OLE 의존성도 반드시 확인합니다.

28. 고 DPI 대응과 현대의 Windows

오래된 MFC 앱을 현대의 Windows에서 실행하면, 고 DPI 환경에서 표시가 깨지는 경우가 있습니다.

예를 들어, 다음과 같은 문제입니다.

글자가 잘린다
버튼이 너무 작다
고정 픽셀 드로잉이 어긋난다
여러 모니터에서 확대율이 다르면 깨진다
오래된 비트맵이 흐릿하게 보인다
대화상자 레이아웃이 밀집된다

Windows 데스크톱 앱에서는 애플리케이션이 DPI 대응 모드를 명시적으로 지정해야 합니다.

MFC 앱에서도 매니페스트, 리소스, 드로잉 코드, 폰트, 레이아웃을 확인해야 합니다.

특히 고정 좌표를 직접 코드에 쓴 다음과 같은 코드는 고 DPI에서 문제가 되기 쉽습니다.

pDC->TextOut(10, 10, _T("Status"));
pDC->Rectangle(10, 40, 200, 80);

고정 픽셀을 전제로 한 좌표는, DPI가 바뀌면 겉모습이 깨집니다.

유지보수 시의 확인 관점은 다음과 같습니다.

애플리케이션 매니페스트의 DPI 설정
대화상자 리소스의 폰트
고정 픽셀 드로잉
이미지 리소스의 해상도
여러 모니터에서의 동작
Windows 10 / Windows 11에서의 표시

MFC의 고 DPI 대응은, 단순히 프로젝트 설정을 바꾸는 것만으로 끝나지 않는 경우가 많습니다. 오래된 UI에서는 실제 화면 확인과 레이아웃 수정이 필요해집니다.

29. 예외 처리와 오류 처리

MFC에는 독자적인 예외 클래스와 오류 처리 작법이 있습니다.

오래된 코드에서는 다음과 같은 매크로를 볼 수 있습니다.

TRY
{
    // 처리
}
CATCH(CFileException, e)
{
    e->ReportError();
}
END_CATCH

현대 C++의 try / catch와 혼재되어 있는 코드도 있습니다.

try
{
    DoSomething();
}
catch (const std::exception& ex)
{
    // 로그 출력
}

MFC 유지보수에서 주의해야 할 점은 다음과 같습니다.

MFC 예외와 C++ 표준 예외가 혼재하지 않는가
예외 객체의 수명을 잘못 이해하고 있지 않은가
오래된 THROW/CATCH 매크로를 이해하고 있는가
반환값 오류와 예외가 혼재하지 않는가
AfxMessageBox만으로 로그가 남지 않는 상태가 되어 있지 않은가

업무 앱에서는 화면에 오류 메시지를 표시하는 것뿐 아니라, 로그, 조작 이력, 입력값, 외부 연결 상태를 남기는 것이 중요합니다.

오래된 MFC 앱에서는 오류가 AfxMessageBox만으로 끝나는 경우가 있습니다.

AfxMessageBox(_T("저장에 실패했습니다"));

유지보수성을 높이려면, UI 표시와 로그 기록을 분리하는 것이 좋습니다.

LogError(_T("Save failed"), path);
AfxMessageBox(_T("저장에 실패했습니다. 로그를 확인해 주세요."));

30. MFC와 현대 C++를 어떻게 공존시킬 것인가

MFC 앱을 유지보수하고 있다고 해서, 모든 것을 오래된 C++ 작성 방식에 맞출 필요는 없습니다.

UI 계층은 MFC의 작법을 존중하면서, 도메인 로직이나 계산 처리는 현대 C++로 정리할 수 있습니다.

예를 들어, 다음과 같이 나눕니다.

MFC 계층
  CDialog
  CView
  CDocument
  CString
  메시지 맵
  리소스 조작

비 MFC 계층
  std::string / std::wstring
  std::vector
  std::optional
  std::variant
  std::filesystem
  단위 테스트 가능한 클래스
  비즈니스 로직

나쁜 형태는, 모든 처리가 대화상자 클래스에 몰려 있는 상태입니다.

void CMainDialog::OnBnClickedExecute()
{
    // 입력 취득
    // 파일 읽기
    // 통신
    // 계산
    // DB 갱신
    // 화면 갱신
    // 로그 출력
    // 예외 처리
}

이런 코드는 변경하기 어렵고, 테스트하기 어렵고, 버그 조사도 어려워집니다.

개선하려면, MFC 클래스에서 로직을 밖으로 꺼냅니다.

void CMainDialog::OnBnClickedExecute()
{
    if (!UpdateData(TRUE))
    {
        return;
    }

    ExecuteRequest request;
    request.Name = ToStdWString(m_name);
    request.Interval = m_interval;

    ExecuteResult result = m_service.Execute(request);

    m_status = ToCString(result.Message);
    UpdateData(FALSE);
}

이렇게 하면 m_service.Execute는 MFC 없이도 테스트할 수 있습니다.

MFC 기존 자산을 유지보수하는 데 있어 가장 효과적인 개선은, UI 클래스에서 로직을 조금씩 분리해 내는 것입니다.

31. 테스트하기 쉬운 MFC 코드로 만들기

MFC 앱은 그대로 두면 단위 테스트하기 어려운 경우가 많습니다.

이유는 UI, Win32, 파일, 통신, 데이터베이스, 전역 상태가 강하게 결합되기 쉽기 때문입니다.

테스트하기 쉽게 만들기 위한 사고방식은 다음과 같습니다.

CDialog나 CView를 직접 테스트하려 하지 않는다
먼저 비 UI 로직을 분리한다
MFC 타입을 경계에서 변환한다
파일이나 통신을 인터페이스화한다
화면 이벤트 핸들러를 가볍게 만든다

예를 들어, 순수한 C++ 클래스로 처리를 옮깁니다.

class PriceCalculator
{
public:
    int CalculateTotal(const std::vector<int>& prices) const
    {
        int total = 0;
        for (int price : prices)
        {
            total += price;
        }
        return total;
    }
};

MFC 쪽은 입력과 출력만 담당합니다.

void CPriceDialog::OnBnClickedCalculate()
{
    if (!UpdateData(TRUE))
    {
        return;
    }

    std::vector<int> prices = ParsePrices(ToStdWString(m_input));
    int total = m_calculator.CalculateTotal(prices);

    m_result.Format(_T("%d"), total);
    UpdateData(FALSE);
}

이 구조로 만들면, PriceCalculatorParsePrices는 일반적인 C++ 테스트 프레임워크로 테스트할 수 있습니다.

MFC 앱 전체를 한 번에 다시 만들 필요는 없으며, 먼저 이벤트 핸들러 안에서 테스트 가능한 처리를 밖으로 꺼내는 것만으로도 효과가 있습니다.

32. 빌드 환경을 고정하기

MFC 앱 유지보수에서는 빌드 환경을 고정하는 것이 중요합니다.

오래된 코드베이스에서는 다음과 같은 차이로 빌드 결과가 달라질 수 있습니다.

Visual Studio의 버전
MSVC 툴셋의 버전
Windows SDK의 버전
MFC/ATL 구성 요소의 유무
x86 / x64 / ARM64
Debug / Release
Unicode / MBCS
MFC의 정적 링크 / 공유 DLL
런타임 라이브러리 설정
미리 컴파일된 헤더

MFC 앱에서는 stdafx.hpch.h에 많은 의존성이 모여 있는 경우도 있습니다.

#include "framework.h"
#include "MyApp.h"

특정 파일만 컴파일 설정이 다르다거나, 미리 컴파일된 헤더 설정이 어긋나 있다는 이유로 빌드가 깨지는 경우가 있습니다.

유지보수 프로젝트에서는 이 정보를 README에 명시해 두면 이후가 편해집니다.

필요한 Visual Studio 버전
필요한 워크로드와 개별 구성 요소
필요한 Windows SDK
대상 플랫폼
MFC의 링크 방식
빌드 절차
CI 실행 방법
배포물 만드는 방법

“내 PC에서는 빌드된다”를 졸업하는 것이, MFC 유지보수의 첫걸음입니다.

33. CI에서 MFC를 빌드하기

MFC 앱도 CI에서 빌드할 수 있습니다.

다만 CI 환경에 MFC 구성 요소가 설치되어 있어야 합니다.

Visual Studio Build Tools를 사용할 경우, MFC/ATL의 구성 요소 ID를 지정하여 설치해야 합니다.

확인하고 싶은 포인트를 나열합니다.

Build Tools에 MFC가 설치되어 있는가
대상 툴셋이 프로젝트와 일치하는가
Windows SDK가 설치되어 있는가
x86 빌드와 x64 빌드를 모두 확인하고 있는가
리소스 컴파일러가 동작하는가
서명 처리가 있는가
설치 프로그램 생성도 CI 대상인가

MFC 앱에서는 UI 테스트까지 자동화하는 것은 쉽지 않지만, 적어도 다음과 같은 자동화는 유효합니다.

Debug / Release 빌드
x86 / x64 빌드
정적 분석
단위 테스트
설치 프로그램 생성
산출물의 해시 저장
의존 DLL 확인

장기 유지보수에서는 빌드할 수 있는 상태를 유지하는 것만으로도 큰 가치가 있습니다.

34. MFC 앱을 디버깅할 때 볼 곳

MFC 앱에서 문제를 추적할 때는 다음 순서로 보면 효율적입니다.

1. 어느 화면인가
2. 대화상자인가, View인가, Frame인가
3. 조작에 대응하는 리소스 ID는 무엇인가
4. 메시지 맵에서 어느 함수로 가는가
5. UpdateData의 방향은 올바른가
6. Document/View라면 Document의 상태는 무엇인가
7. 명령 라우팅에서 다른 클래스로 흘러가고 있지 않은가
8. 작업자 스레드에서 UI를 다루고 있지 않은가
9. 예외나 오류가 AfxMessageBox만으로 묵살되고 있지 않은가
10. 릴리스 빌드 고유의 초기화 누락이나 수명 문제는 없는가

예를 들어, “버튼을 눌러도 아무 일도 일어나지 않는다”는 문제라면, 다음을 확인합니다.

버튼의 IDC가 올바른가
ON_BN_CLICKED가 존재하는가
핸들러의 시그니처는 올바른가
대화상자 리소스가 다른 것이 아닌가
버튼이 비활성화되어 있지 않은가
처리 중간에 UpdateData가 실패하고 있지 않은가
예외가 묵살되고 있지 않은가

“메뉴를 누를 수 없다”라면 다음을 확인합니다.

ON_UPDATE_COMMAND_UI에서 비활성화되어 있지 않은가
명령 ID가 중복되어 있지 않은가
활성 뷰가 예상대로인가
Frame/View/Document/App 중 어디에 핸들러가 있는가

MFC는 표면상의 이벤트와 실제 처리가 매크로나 라우팅으로 연결되어 있기 때문에, 익숙해지기 전까지는 호출 경로를 그림으로 그려 보면 이해하기 쉽습니다.

35. 자주 있는 함정

MFC 유지보수에서 자주 있는 함정을 정리합니다.

메시지 맵을 보지 않고 함수 호출만 검색한다
CWnd*가 null이 아니면 유효하다고 생각한다
HWND의 수명과 C++ 객체의 수명을 혼동한다
UpdateData(TRUE/FALSE)의 방향을 잘못 사용한다
ON_UPDATE_COMMAND_UI에서 비활성화되고 있음을 알아채지 못한다
resource.h의 ID 충돌을 알아채지 못한다
작업자 스레드에서 UI를 직접 다룬다
CString과 std::string 간 변환에서 문자가 깨진다
MBCS를 전제로 한 코드를 Unicode화로 깨뜨린다
MFC DLL에서 AFX_MANAGE_STATE를 잊는다
x86을 전제로 한 COM/ActiveX를 x64화로 깨뜨린다
GDI 객체의 선택/해제를 잘못한다
고 DPI에서 고정 좌표 레이아웃이 깨진다

MFC의 문제는 C++ 문법만 봐서는 알 수 없는 경우가 있으며, Windows의 메시지, 리소스, 핸들, 모듈, 런타임 설정까지 포함해서 봐야 합니다.

36. 신규 개발에서 MFC를 선택해야 하는가

완전한 신규 개발에서 MFC를 선택할지는 신중하게 판단하는 것이 좋습니다.

MFC를 선택할 이유가 있다면, 다음과 같은 경우입니다.

기존 MFC 코드와 밀접하게 연동해야 한다
기존 MFC 구성 요소나 화면을 재사용하고 싶다
Win32/GDI/COM에 매우 근접한 제어가 필요하다
사내에 MFC 유지보수 역량이 충분히 있다
대상이 Windows 데스크톱으로 한정되어 있다
장기적인 이전 계획상, 먼저 MFC로 증축해야 한다

반대로, 다음의 경우에는 다른 선택지를 검토하는 것이 좋습니다.

모던한 UI를 만들고 싶다
유연한 레이아웃이나 애니메이션이 필요하다
웹 연동이나 클라우드 연동이 중심이다
테스트 용이성을 중시하고 싶다
젊은 개발자가 참여하기 쉬운 기술을 선택하고 싶다
크로스플랫폼 대응이 필요하다
접근성이나 고 DPI를 처음부터 중시하고 싶다

MFC는 “지금부터 배울 가치가 없는” 기술도 “새롭기 때문에 선택하는” 기술도 아니며, 기존 Windows 네이티브 자산과 마주하기 위한 기술입니다.

37. MFC에서 이전할 때의 사고방식

MFC 앱을 다른 기술로 이전하고 싶은 경우, 처음부터 전면 재구축을 목표로 하면 실패하기 쉽습니다.

먼저, 앱을 다음과 같이 분해하여 생각합니다.

UI
비즈니스 로직
파일 형식
통신 처리
데이터베이스 처리
장치 제어
인쇄
COM/OLE 연동
설정 관리
로그

이 중 가장 MFC에 의존하고 있는 것은 UI입니다.

한편, 비즈니스 로직이나 파일 처리는 분리할 수 있을 가능성이 있습니다.

이전의 현실적인 순서는 다음과 같습니다.

1. 빌드 환경을 재현한다
2. 기존 동작을 테스트 데이터로 고정한다
3. UI 이벤트 핸들러에서 로직을 분리한다
4. 비 MFC C++ 라이브러리로 옮긴다
5. 자동 테스트를 추가한다
6. 외부 사양을 문서화한다
7. 필요한 화면부터 단계적으로 교체한다

“MFC를 그만두는 것” 자체를 목적으로 하기보다, “MFC에 갇혀 있는 중요한 로직을 밖으로 꺼내는 것”을 목적으로 하는 편이 성공하기 쉽습니다.

38. MFC 코드를 읽을 때의 입구

기존 MFC 프로젝트를 처음 읽을 때는, 다음과 같은 파일부터 살펴봅니다.

*.vcxproj
  툴셋, MFC 설정, 문자 집합, 런타임 설정을 본다

resource.h
  리소스 ID를 본다

*.rc
  대화상자, 메뉴, 문자열, 아이콘을 본다

*App.cpp / *App.h
  CWinApp 파생 클래스와 InitInstance를 본다

MainFrm.cpp / MainFrm.h
  메인 프레임과 메뉴/도구 모음을 본다

*Doc.cpp / *Doc.h
  Document/View라면 데이터 구조와 저장 처리를 본다

*View.cpp / *View.h
  드로잉과 사용자 조작을 본다

*Dlg.cpp / *Dlg.h
  대화상자, DDX, 버튼 처리를 본다

다음으로, 자주 사용되는 검색 키워드입니다.

BEGIN_MESSAGE_MAP
ON_COMMAND
ON_UPDATE_COMMAND_UI
ON_BN_CLICKED
DoDataExchange
UpdateData
OnInitDialog
OnDraw
Serialize
AfxMessageBox
AfxBeginThread
AFX_MANAGE_STATE

이 근처를 검색하면, 애플리케이션의 동작이 잘 보이게 됩니다.

39. MFC를 유지보수할 때의 설계 방침

MFC 기존 자산을 오래 유지보수한다면, 다음과 같은 방침이 유효합니다.

UI 이벤트 핸들러를 가볍게 만든다
CString이나 CWnd를 비 UI 계층에 지나치게 노출하지 않는다
비즈니스 로직을 일반적인 C++ 클래스로 옮긴다
파일 형식의 호환성 테스트를 만든다
x86/x64의 차이를 명확히 한다
리소스 ID 변경을 리뷰한다
MFC DLL의 모듈 상태를 확인한다
로그를 정비한다
CI에서 빌드를 고정한다
고 DPI나 Windows 11에서의 화면 확인을 정기적으로 실시한다

특히 CDialogCView에 처리를 지나치게 몰아넣지 않는 것이 중요합니다.

MFC의 화면 클래스는 입력, 표시, 이벤트 전달에 집중시킵니다.

화면에서 값을 가져온다
서비스에 전달한다
결과를 화면에 반영한다

이 정도로 유지할 수 있다면, MFC라도 상당히 유지보수하기 쉬워집니다.

40. 실무 체크리스트

MFC 앱을 다룰 때의 체크리스트입니다.

Visual Studio의 버전은 명확한가
MFC/ATL 구성 요소는 설치되어 있는가
x86/x64 대상은 명확한가
Unicode/MBCS 설정을 파악하고 있는가
MFC는 정적 링크인가 공유 DLL인가
필요한 Visual C++ Redistributable은 무엇인가
resource.h와 .rc를 리뷰 대상으로 하고 있는가
메시지 맵으로 이벤트 경로를 추적하고 있는가
UpdateData의 방향은 올바른가
Document/View 구조를 사용하고 있는가
모덜리스 대화상자의 수명 관리는 안전한가
작업자 스레드에서 UI를 직접 다루고 있지 않은가
MFC DLL에서 AFX_MANAGE_STATE가 필요한 곳은 없는가
고 DPI 환경에서 화면을 확인하고 있는가
COM/ActiveX 의존성은 32bit/64bit에 대응하는가
로그는 남는가
비 UI 로직을 테스트할 수 있는가

MFC는 익숙해지기 전까지는 독특하게 보이지만, 봐야 할 곳을 알면 상당히 규칙적으로 읽을 수 있습니다.

41. 정리

MFC는 Windows의 네이티브 데스크톱 애플리케이션을 C++로 구축하기 위한 역사 있는 프레임워크입니다. 신규 개발의 주류는 아니게 되었지만, 기존 자산의 유지보수, 빌드 환경 갱신, 기능 추가, 단계적 이전에서는 지금도 중요한 기술입니다.

MFC를 이해하는 데 특히 중요한 것은 다음과 같은 점입니다.

MFC는 Win32 API를 C++로 다루기 쉽게 만든 것이다
CWinApp이 애플리케이션 전체를 관리한다
CWnd와 HWND의 수명은 같지 않다
메시지 맵으로 이벤트와 함수가 연결된다
Document/View는 데이터와 표시를 분리하는 메커니즘이다
DDX/DDV는 대화상자 값 동기화와 검증에 사용한다
리소스 파일과 resource.h가 매우 중요하다
CString이나 TCHAR는 문자 코드 설정과 함께 이해한다
MFC DLL에서는 모듈 상태에 주의한다
오래된 MFC 앱은 고 DPI, x64, CI, 테스트의 관점에서 다시 살펴볼 가치가 있다

MFC를 다룰 때는, “오래됐으니까 나쁘다”고 단정하기보다 먼저 구조를 이해하는 것이 중요합니다. 기존 MFC 앱에는 오랜 업무 지식, 고객별 사양, 장치 연동, 파일 호환성이 담겨 있는 경우가 있습니다. 그 가치를 지키면서 조금씩 유지보수하기 쉽게 만들려면, MFC의 작법을 이해한 다음 비 UI 로직을 분리하고, 빌드와 테스트를 정비하는 것이 현실적입니다.

굳이 한마디로 정리하면 다음과 같습니다.

Windows 네이티브 앱의 동작 방식을, C++ 클래스와 프레임워크로 다루기 위한 기반.

이 시각을 가지면, MFC는 단순한 오래된 기술이 아니라, 기존 Windows 자산을 안전하게 읽어내기 위한 실마리가 됩니다.

참고

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

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

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

자주 묻는 질문

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

MFC란 무엇인가요?
MFC는 Microsoft Foundation Classes의 약자로, Win32 API를 C++ 클래스로 다루기 쉽게 만들어 주는 Windows 애플리케이션 프레임워크입니다. 창은 CWnd, 대화상자는 CDialog, 애플리케이션 전체는 CWinApp과 같은 클래스로 표현됩니다. 'Windows를 몰라도 쓸 수 있는 마법 같은 라이브러리'가 아니라, Windows의 동작 방식을 C++ 타입과 프레임워크로 정리한 것이므로, Windows 메시지나 핸들 같은 Win32 지식도 필요합니다.
MFC를 지금도 사용할 수 있나요? 지원은 계속되고 있나요?
MFC는 현재도 Visual Studio에서 사용할 수 있으며, 계속 지원되고 있습니다. 다만 Microsoft 문서에는 새로운 기능 추가나 문서 갱신은 이루어지지 않는다는 안내가 있습니다. 위치상으로는 기존 MFC 앱의 유지보수나 기능 추가, 빌드 환경 갱신, 다른 UI로의 단계적 이전에는 현실적으로 자주 쓰이는 반면, 완전히 새로운 일반적인 GUI 앱에서의 채택은 신중하게 판단해야 할 기술입니다. Visual Studio Installer의 개별 구성 요소(C++ MFC for latest build tools) 설치가 필요합니다.
MFC의 메시지 맵이란 무엇인가요?
Windows 메시지나 명령을 핸들러 함수와 연결하는 MFC의 메커니즘입니다. BEGIN_MESSAGE_MAP부터 END_MESSAGE_MAP 사이에, ON_BN_CLICKED나 ON_COMMAND 같은 매크로로 '이 버튼이 클릭되면 이 함수를 호출한다'는 대응 관계를 작성합니다. 함수를 검색해도 직접적인 호출은 보이지 않는데 이벤트 시에 실행되고 있다면 메시지 맵을 확인합니다. MFC 코드 리뷰에서는 핸들러 함수만이 아니라 메시지 맵과 함께 확인하는 것이 중요합니다.
MFC에서 다른 기술로 이전하려면 어떻게 해야 하나요?
처음부터 전면 재구축을 목표로 하면 실패하기 쉽습니다. 현실적인 순서는, 빌드 환경을 재현하고, 기존 동작을 테스트 데이터로 고정하고, UI 이벤트 핸들러에서 로직을 분리해 비-MFC C++ 라이브러리로 옮기고, 자동 테스트를 추가하고, 외부 사양을 문서화한 다음, 필요한 화면부터 단계적으로 교체하는 흐름입니다. 'MFC를 그만두는 것' 자체를 목적으로 삼기보다, 'MFC에 갇혀 있는 중요한 로직을 외부로 꺼내는 것'을 목적으로 하는 편이 성공하기 쉽습니다.

저자 프로필

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

Go Komura

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

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

블로그 목록으로 돌아가기