Cute Light Pink Flying Butterfly HTTP보다 빠르다! Named Pipe를 이용한 프로세스 간 통신(IPC) :: 놀면서 돈벌기
본문 바로가기
IT/Backend | API

HTTP보다 빠르다! Named Pipe를 이용한 프로세스 간 통신(IPC)

by esclife_ 2026. 1. 25.
반응형

IPC(Inter-Process Communication)와 Named Pipe

프로세스는 기본적으로 독립된 메모리 공간을 가집니다. 그래서 서로 데이터를 주고받으려면 특별한 통로가 필요한데, 이를 IPC라고 합니다. Named Pipe는 공유 메모리나 소켓 통신보다 구현이 간결하면서도, 마치 파일을 읽고 쓰는 것처럼 스트림 방식을 사용하여 대용량 데이터를 빠르게 주고받을 수 있는 통신 기법입니다. 이름이 붙여진 파일 경로를 사용하여 서로 관련 없는 프로세스 간에도 데이터를 교환할 수 있는 일방향 또는 양방향 통신(IPC) 방식이라고 볼 수 있어요.

 


Reader-Writer 관점에서 본 Named Pipe

Named Pipe 통신은 본질적으로 하나의 통로(Pipe)를 사이에 둔 Reader(서버)와 Writer(클라이언트)의 동기화된 데이터 흐름입니다.

1. OS가 제공하는 '공유 버퍼' (The Shared Buffer)

Named Pipe는 물리적인 파일이 아니라, 커널 메모리 내에 존재하는 FIFO(First-In, First-Out) 버퍼입니다.

  • Writer가 데이터를 쓰면 커널 버퍼에 쌓이고,
  • Reader가 데이터를 읽으면 그만큼 버퍼에서 제거됩니다.

2. Reader와 Writer의 역할 정의

구분 역할 (Role) 핵심 동작 코드에서의 구현
Writer (클라이언트) 데이터 생산자 요청 메시지(JSON)를 스트림에 'Write' StreamWriter.WriteLineAsync()
Reader (서버) 데이터 소비자 파이프를 열고 메시지가 올 때까지 'Read' 대기 StreamReader.ReadLineAsync()

3. Reader-Writer 관점의 핵심 매커니즘 : 블로킹(Blocking)과 동기화

단순히 쓰고 읽는 것이 아니라, 두 프로세스 간의 속도 차이를 OS가 조율해준다는 점이 핵심입니다.

  • Full Buffer (Writer의 대기): 만약 Reader(서버)가 너무 느려서 OS 버퍼가 꽉 찬다면, Writer(클라이언트)는 버퍼에 빈 공간이 생길 때까지 쓰기 동작을 잠시 멈춥니다(Blocking).
  • Empty Buffer (Reader의 대기): 반대로 Writer가 아무것도 보내지 않으면, Reader는 데이터가 들어올 때까지 읽기 동작에서 멈춰 서서 기다립니다.

ReadLineAsync()로 Reader가 대기하는 동안 서버 메인스레드가 멈추지 않도록 비동기(Non-blocking) Reader를 구현.

4. 일대일(1:1) 단방향/양방향 파이프

  • 단방향: Writer는 쓰기만 하고 Reader는 읽기만 하는 구조 (가장 기본)
  • 양방향 (Full-Duplex): 우리 프로젝트처럼 PipeDirection.InOut을 사용하면, 한 파이프 안에서 서버와 클라이언트가 서로 Reader이자 Writer가 되어 질의응답(Request-Response)을 주고받을 수 있습니다.

왜 TCP/IP 소켓이 아닌 Named Pipe인가?

로컬 소켓 통신 대신 Named Pipe를 선택한 이유는 다음과 같습니다.

  1. 성능: 네트워크 스택을 타지 않고 커널 메모리를 직접 이용하므로 로컬 환경에서 훨씬 빠릅니다.
  2. 보안: 포트(Port)를 열 필요가 없어 외부 해킹 공격으로부터 안전하며, 윈도우의 ACL(액세스 제어 목록)을 통해 파이프 자체에 대한 접근 권한을 설정할 수 있습니다.

코드 구현 분석: NamedPipeServer.cs

using System;
using System.IO;
using System.IO.Pipes;
using System.Threading;
using System.Threading.Tasks;

namespace SecurityDemoProject.AppGateway.Security.NamedPipeServer
{

    public class NamedPipeServer : ICommunicationServer
    {
        private readonly string _pipeName;
        private CancellationTokenSource _cancellationTokenSource;

        public event Func<string, string> RequestReceived;

        public NamedPipeServer(string pipeName)
        {
            _pipeName = pipeName;
        }

        public void StartListening()
        {
            _cancellationTokenSource = new CancellationTokenSource();
            Task.Run(() => ListenLoop(_cancellationTokenSource.Token));
        }

        public void StopListening()
        {
            _cancellationTokenSource?.Cancel();
        }

        private void ListenLoop(CancellationToken token)
        {
            while (!token.IsCancellationRequested)
            {
                try
                {
                    using (var serverStream = new NamedPipeServerStream(_pipeName, PipeDirection.InOut, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous))
                    {
                        serverStream.WaitForConnectionAsync(token).Wait(token);
                        if (token.IsCancellationRequested) break;

                        var reader = new StreamReader(serverStream);
                        var writer = new StreamWriter(serverStream);

                        string line = reader.ReadLine();
                        if (line != null)
                        {
                            // '받은 그대로(line)'를 Invoke의 인자로 전달하고, '반환값'을 받음
                            string response = RequestReceived?.Invoke(line) ?? "Error: No handler registered.";

                            writer.Write(response);
                            writer.Flush();
                        }
                    }
                }
                catch (OperationCanceledException) { break; }
                catch (Exception) { /* 클라이언트 연결 오류 등 무시 */ }
            }
        }

        public string SendRequest(string request)
        {
            using (var clientStream = new NamedPipeClientStream(".", _pipeName, PipeDirection.InOut))
            {
                clientStream.Connect(5000);
                var writer = new StreamWriter(clientStream);
                var reader = new StreamReader(clientStream);
                writer.WriteLine(request);
                writer.Flush();
                return reader.ReadToEnd();
            }
        }

        public void Dispose()
        {
            StopListening();
        }
    }
}

 

=> 아래는 인터페이스 입니다.

using System;

public interface ICommunicationServer : IDisposable
{
    event Func<string, string> RequestReceived;
    void StartListening();
    void StopListening();
    string SendRequest(string request);
}

스트림 기반 통신의 주의점

Named Pipe는 데이터가 물 흐르듯 들어오는 스트림 방식입니다.

  • 메시지 경계(Framing): 데이터가 어디서 시작해서 어디서 끝나는지 알기 위해 우리는 \n(개행 문자)를 기준으로 메시지를 구분하는 방식을 채택했습니다.
  • 비동기 처리: 서버가 요청을 처리하는 동안 다른 클라이언트가 대기하지 않도록 Async 메서드를 활용하여 Non-blocking 통신을 구현했습니다.

OSI 7계층을 다 거치지 않고도 OS 커널을 통해 데이터를 주고받는 이 방식은 로컬 서비스 아키텍처를 설계할 때 매우 강력한 옵션이 될 것 같습니다. 특히 윈도우 시스템 프로그래밍에서 파이프는 가장 기본적이면서도 강력한 도구임을 확인했습니다. 이제 인증, 인가, 호출 제한을 마친 깨끗한 데이터가 이 파이프를 통해 안전하게 전달됩니다.

반응형