본문 바로가기
.NET

MFC 개발자 대상 .NET 8 기반 IPC 샘플 코드 기반 교육

by leo21c 2026. 6. 16.

두개의 윈도우 SW가 IPC를 해서 한쪽 SW에서 계산식을 넣고 다른 SW에서 계산을 하고 그 결과를 계산식을 넣은 SW에 전달해서 표시하는 프로그램을 제작해서 샘플 코드 기반.


MFC에서 주로 사용하던 WM_COPYDATA(SendMessage 기반), Named Pipe, Shared Memory 등의 개념은 .NET 8에서도 그대로 사용할 수 있지만, .NET 8 스타일의 가장 현대적이고 강력한 IPC 방식은 gRPC(Google Remote Procedure Call) 또는 Named Pipe(표준 스트림 기반)를 사용하는 것입니다.

여기서는 MFC 개발자분들이 개념적으로 가장 쉽게 매핑할 수 있으면서도, .NET 8의 현대적 특징(async/await, 의존성 주입, 인터럽트 없는 비동기 스트림)을 모두 학습할 수 있는 파이프(Named Pipe) 기반의 IPC 구조로 샘플 코드를 구성하여 교육을 진행하겠습니다.

## 1. 아키텍처 및 개념 매핑 (MFC vs .NET 8)

우리가 만들 시스템은 다음과 같습니다.

  1. Client UI App (WPF/WinForm): 사용자가 계산식(예: 10 + 20)을 입력하고 요청하는 앱. (MFC의 SendMessage를 보내는 측)
  2. Server Logic App (Console/Service): 백그라운드에서 계산을 수행하고 결과를 돌려주는 앱. (MFC의 OnReceive 또는 파이프 Listen 측)
  • MFC 방식: 파이프 핸들(CreateNamedPipe)을 생성하고, 무한 루프 스레드를 돌리며 ConnectNamedPipe로 대기한 뒤, 바이트 포인터를 캐스팅하여 읽고 썼습니다.
  • .NET 8 방식: NamedPipeServerStream을 사용하며, 무한 루프 대신 async/await 비동기 태스크를 사용해 UI나 메인 스레드를 전혀 방해하지 않고 우아하게 데이터를 주고받습니다.

## 2. .NET 8 IPC 샘플 코드 (계산기 시스템)

두 앱이 서로 통신할 때 사용할 메시지 구조(Data Contract)와 서버, 클라이언트 코드를 작성해 보겠습니다. (.NET 8에서는 구조체를 바이트 배열로 직렬화하는 대신, 가볍고 표준적인 System.Text.Json을 주로 사용합니다.)

### 0) 공통 데이터 구조 (Data Contract)

// 두 앱이 공통으로 사용할 클래스 정의
public class CalculatorRequest
{
    public string Expression { get; set; } = string.Empty; // 예: "100 * 5"
}

public class CalculatorResponse
{
    public bool IsSuccess { get; set; }
    public double Result { get; set; }
    public string ErrorMessage { get; set; } = string.Empty;
}

### 1) Server 앱 (계산 수행 및 결과 전달)

HostApplicationBuilder를 활용하여 백그라운드에서 파이프를 열고 대기하는 서버 서바이벌 가동 코드입니다.

using System.IO.Pipes;
using System.Text.Json;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddHostedService<PipeServerService>(); // 백그라운드 서비스 등록

var host = builder.Build();
await host.RunAsync();

// MFC의 백그라운드 워커 스레드 역할을 하는 고성능 HostedService
public class PipeServerService : BackgroundService
{
    private readonly ILogger<PipeServerService> _logger;
    private const string PipeName = "EntecCalculatorPipe";

    public PipeServerService(ILogger<PipeServerService> logger) => _logger = logger;

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("IPC 계산 서버가 시작되었습니다.");

        while (!stoppingToken.IsCancellationRequested)
        {
            // MFC의 CreateNamedPipe + ConnectNamedPipe 과정이 비동기로 단 한 줄 처리됨
            using var server = new NamedPipeServerStream(PipeName, PipeDirection.InOut, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous);
            
            _logger.LogInformation("클라이언트 연결 대기 중...");
            await server.WaitForConnectionAsync(stoppingToken);
            _logger.LogInformation("클라이언트가 연결되었습니다.");

            try
            {
                using var reader = new StreamReader(server);
                using var writer = new StreamWriter(server) { AutoFlush = true };

                // 1. 클라이언트로부터 계산식 수신
                string? jsonRequest = await reader.ReadLineAsync(stoppingToken);
                if (jsonRequest != null)
                {
                    var request = JsonSerializer.Deserialize<CalculatorRequest>(jsonRequest);
                    _logger.LogInformation($"수신된 계산식: {request?.Expression}");

                    // 2. 계산 수행 (실제 프로젝트선 DataTable 등을 활용하거나 파싱 로직 적용)
                    var response = EvaluateExpression(request?.Expression);

                    // 3. 결과를 JSON으로 변환하여 송신
                    string jsonResponse = JsonSerializer.Serialize(response);
                    await writer.WriteLineAsync(jsonResponse);
                }
            }
            catch (Exception ex)
            {
                _logger.LogError($"통신 중 오류 발생: {ex.Message}");
            }
        }
    }

    private CalculatorResponse EvaluateExpression(string? expression)
    {
        try
        {
            // .NET 8의 간단한 계산 식 연산 예시 (간이 구현)
            // 실제 배전 시스템에선 정밀한 데이터 파싱 알고리즘이 들어가는 부분입니다.
            if (string.IsNullOrEmpty(expression)) throw new Exception("식이 비어있습니다.");
            
            // 단순 데모용 (System.Data.DataTable 이용한 Eval)
            var table = new System.Data.DataTable();
            var result = Convert.ToDouble(table.Compute(expression, string.Empty));

            return new CalculatorResponse { IsSuccess = true, Result = result };
        }
        catch (Exception ex)
        {
            return new CalculatorResponse { IsSuccess = false, ErrorMessage = ex.Message };
        }
    }
}

### 2) Client 앱 (UI 측 - 계산 요청 및 결과 표시)

WPF나 WinForm의 버튼 클릭 이벤트 핸들러 내부 등에 들어갈 통신 코드입니다.

using System.IO.Pipes;
using System.Text.Json;

public async Task<CalculatorResponse> RequestCalculationAsync(string expression)
{
    const string PipeName = "EntecCalculatorPipe";
    
    // MFC의 CreateFile(Pipe) 코드가 현대적인 스트림으로 추상화됨
    using var client = new NamedPipeClientStream(".", PipeName, PipeDirection.InOut, PipeOptions.Asynchronous);
    
    // 서버 연결 (제한시간 5초)
    await client.ConnectAsync(5000);

    using var writer = new StreamWriter(client) { AutoFlush = true };
    using var reader = new StreamReader(client);

    // 1. 요청 데이터 생성 및 직렬화
    var request = new CalculatorRequest { Expression = expression };
    string jsonRequest = JsonSerializer.Serialize(request);

    // 2. 서버로 전송 (SendMessage의 비동기화)
    await writer.WriteLineAsync(jsonRequest);

    // 3. 서버로부터 결과 수신 (OnReceive 비동기 대기)
    string? jsonResponse = await reader.ReadLineAsync();
    if (jsonResponse == null) 
        throw new Exception("서버로부터 응답을 받지 못했습니다.");

    var response = JsonSerializer.Deserialize<CalculatorResponse>(jsonResponse);
    return response ?? new CalculatorResponse { IsSuccess = false, ErrorMessage = "데이터 역직렬화 실패" };
}

// UI에서 호출하는 예시 (WPF Button_Click)
private async void BtnCalculate_Click(object sender, RoutedEventArgs e)
{
    TxtResult.Text = "계산 중...";
    try
    {
        // UI 스레드를 대기(Block)시키지 않고, 비동기로 결과를 받아옴 (가장 중요한 패러다임)
        var response = await RequestCalculationAsync(TxtExpression.Text);
        
        if (response.IsSuccess)
            TxtResult.Text = $"결과: {response.Result}";
        else
            TxtResult.Text = $"에러: {response.ErrorMessage}";
    }
    catch (Exception ex)
    {
        TxtResult.Text = $"연결 실패: {ex.Message}";
    }
}

## 3. MFC 개발자 관점에서의 핵심 교육 포인트 (시사점)

팀원분들에게 이 샘플을 기반으로 교육할 때, 전통적인 Win32/MFC 방식과의 차이점을 다음과 같이 짚어주면 흡수율이 매우 높습니다.

  1. 메시지 펌핑과 데드락(Deadlock) 해방
    • MFC에서 SendMessage를 쓰면 상대방 윈도우가 응답할 때까지 내 UI 스레드가 멈추거나, 양쪽이 동시에 보낼 때 데드락 위험이 있었습니다.
    • .NET 8의 await client.ConnectAsync()와 await reader.ReadLineAsync()는 작업을 OS(IOCP)에 맡겨두고 스레드를 반환하므로, 통신 중에도 클라이언트 UI가 절대 버벅이거나 멈추지 않습니다.
  2. 캐스팅(Casting) 없는 데이터 모델링
    • MFC에서는 WM_COPYDATA로 보낼 때 pThread->pParam을 (BYTE*)로 캐스팅하고 구조체 크기만큼 복사(memcpy)해야 했습니다. 버퍼 오버플로우나 32/64비트 정렬(Padding) 문제가 빈번했죠.
    • .NET 8은 JsonSerializer를 통해 객체를 텍스트/바이트 스트림으로 안전하게 직렬화하므로 메모리 오염 걱정이 전혀 없습니다.
  3. BackgroundService를 통한 안정적인 데몬(Daemon) 구현
    • MFC에서 파이프 서버를 만들려면 AfxBeginThread로 루프를 돌리고 프로그램 종료 시 스레드 종료 이벤트(SetEvent)를 날려 수동으로 정리해야 했습니다.
    • .NET 8 서버의 ExecuteAsync(CancellationToken stoppingToken)는 시스템이 종료될 때 stoppingToken에 취소 신호가 자동으로 들어와 안전한 자원 해제(Graceful Shutdown)가 기본적으로 보장됩니다.
LIST