ASP.NET Core SignalR을 활용한 실시간 통신 구현

ASP.NET Core SignalR은 서버와 클라이언트 간의 양방향 실시간 통신을 가능하게 하는 라이브러리입니다. 이를 통해 웹 애플리케이션에 채팅, 알림, 실시간 대시보드와 같은 기능을 쉽게 통합할 수 있습니다.

주요 NuGet 패키지는 다음과 같습니다:

  • Microsoft.AspNetCore.SignalR.Client: Blazor WebAssembly 등 클라이언트 애플리케이션에서 SignalR 허브에 연결할 때 사용합니다.
  • Microsoft.AspNetCore.SignalR.StackExchangeRedis: Redis를 이용하여 SignalR 서비스를 분산 환경에 배포할 때 사용합니다.

1. SignalR의 기본적인 사용법

SignalR은 Hub 클래스를 사용하여 서버와 클라이언트 간의 연결을 관리하고 메시지 교환을 처리합니다. 클라이언트는 허브에 연결하고 메시지를 수신하거나 서버로 요청을 보낼 수 있습니다.

1.1. 서버 측 구현 (ASP.NET Core)

먼저 ASP.NET Core 애플리케이션에 SignalR 서비스를 구성하고 허브 클래스를 생성해야 합니다. 허브 클래스는 클라이언트 요청을 처리하고 클라이언트로 메시지를 전송하는 로직을 포함합니다.

// Hubs 폴더 내에 실시간 통신을 처리할 허브 클래스를 정의합니다.
// NotificationHub.cs
public class NotificationHub : Hub
{
    // 클라이언트가 호출할 수 있는 메서드입니다.
    public async Task SendGlobalNotification(string senderId, string content)
    {
        string connectionIdentifier = Context.ConnectionId; // 현재 연결된 클라이언트의 ID
        string formattedMessage = $"[{connectionIdentifier}] {senderId}: {content}";
        // 연결된 모든 클라이언트에게 "ReceiveNotification" 이벤트를 통해 메시지를 전송합니다.
        await Clients.All.SendAsync("ReceiveNotification", formattedMessage);
    }

    // 클라이언트 연결/해제 시 추가 로직을 구현할 수 있습니다.
    public override Task OnConnectedAsync()
    {
        Console.WriteLine($"클라이언트 연결됨: {Context.ConnectionId}");
        return base.OnConnectedAsync();
    }

    public override Task OnDisconnectedAsync(Exception? exception)
    {
        Console.WriteLine($"클라이언트 연결 해제됨: {Context.ConnectionId}, Exception: {exception?.Message}");
        return base.OnDisconnectedAsync(exception);
    }
}

다음으로, Program.cs 파일에서 SignalR 서비스를 등록하고 엔드포인트를 매핑합니다.

// Program.cs
var appBuilder = WebApplication.CreateBuilder(args);

// SignalR 서비스 등록
appBuilder.Services.AddSignalR();

// CORS (Cross-Origin Resource Sharing) 설정:
// Blazor WebAssembly와 같은 SPA 클라이언트에서 서버에 접근할 수 있도록 허용합니다.
string[] allowedOrigins = new[] { "https://localhost:7042", "http://localhost:5042" }; // 클라이언트 URL
appBuilder.Services.AddCors(options =>
{
    options.AddDefaultPolicy(policyBuilder =>
    {
        policyBuilder.WithOrigins(allowedOrigins)
                     .AllowAnyMethod()
                     .AllowAnyHeader()
                     .AllowCredentials(); // 자격 증명(쿠키, HTTP 인증) 허용
    });
});

var webApp = appBuilder.Build();

webApp.UseHttpsRedirection();
webApp.UseAuthorization();

// CORS 미들웨어 활성화
webApp.UseCors();

// SignalR 허브 엔드포인트 매핑:
// 클라이언트가 '/hub/notification' 경로로 접속하면 NotificationHub가 처리합니다.
webApp.MapHub<NotificationHub>("/hub/notification");

webApp.MapControllers(); // API 컨트롤러가 있다면 매핑
webApp.Run();

참고 사항:

  • SignalR은 WebSocket을 기반으로 하며, HTTP를 통한 초기 핸드셰이크(Handshake) 과정을 거칩니다.
  • 허브(Hub) 인스턴스는 요청당 생성되는 일시적인(Transient) 생명주기를 가지므로, 허브 내부에 상태를 직접 유지해서는 안 됩니다.
  • 다양한 클라이언트(JavaScript, .NET, Java 등)를 위한 SignalR SDK가 제공됩니다.

1.2. 클라이언트 측 구현 (Blazor WebAssembly)

Blazor WebAssembly 애플리케이션에서 SignalR 서버에 연결하고 메시지를 주고받는 예제입니다.

먼저 Microsoft.AspNetCore.SignalR.Client NuGet 패키지를 설치한 후, 아래와 같이 UI를 구성하고 코드 블록을 작성합니다.

@page "/"

<h3>사용자 이름:</h3>
<input @bind="userNameInput" />
<h3>메시지:</h3>
<input @bind="currentMessage" />
<button @onclick="SendMessage" disabled="@(!IsConnectedToHub)">전송</button>

<ul>
    @foreach (var msg in receivedMessages)
    {
        <li>@msg</li>
    }
</ul>

@code {
    private HubConnection? hubClientConnection; // SignalR 허브 연결 객체
    private string? userNameInput; // 사용자 입력 이름
    private string? currentMessage; // 사용자 입력 메시지
    private List<string> receivedMessages = new(); // 수신된 메시지 목록

    protected override async Task OnInitializedAsync()
    {
        // 허브 연결을 설정합니다.
        hubClientConnection = new HubConnectionBuilder()
            .WithUrl("https://localhost:7110/hub/notification") // 서버 허브 엔드포인트 URL
            .WithAutomaticReconnect() // 자동 재연결 설정
            .Build();

        // 서버로부터 "ReceiveNotification" 메시지를 수신할 때 실행될 핸들러를 등록합니다.
        hubClientConnection.On<string>("ReceiveNotification", (messageContent) =>
        {
            receivedMessages.Add(messageContent);
            InvokeAsync(StateHasChanged); // UI 업데이트
        });

        // 허브 연결을 시작합니다.
        await hubClientConnection.StartAsync();
    }

    // 메시지 전송 버튼 클릭 시 호출됩니다.
    private async Task SendMessage()
    {
        if (hubClientConnection is not null && userNameInput is not null && currentMessage is not null)
        {
            // 서버 허브의 "SendGlobalNotification" 메서드를 호출합니다.
            await hubClientConnection.SendAsync("SendGlobalNotification", userNameInput, currentMessage);
            currentMessage = string.Empty; // 메시지 입력 필드 초기화
        }
    }

    // 허브 연결 상태를 확인하는 속성
    public bool IsConnectedToHub =>
        hubClientConnection?.State == HubConnectionState.Connected;

    // 컴포넌트 소멸 시 허브 연결을 해제하여 리소스를 정리합니다.
    public async ValueTask DisposeAsync()
    {
        if (hubClientConnection is not null)
        {
            await hubClientConnection.DisposeAsync();
        }
    }
}

2. 컨트롤러 또는 백그라운드 서비스에서 메시지 푸시

허브 외에도 ASP.NET Core 컨트롤러나 백그라운드 서비스에서 IHubContext<THub>를 주입받아 클라이언트로 메시지를 전송할 수 있습니다. 이는 서버의 특정 이벤트(예: 데이터베이스 변경)가 발생했을 때 클라이언트에 알림을 보내는 데 유용합니다.

// 서버 측: 컨트롤러에서 IHubContext를 사용하여 메시지 전송
[Route("api/[controller]")]
[ApiController]
public class MessageApiController : ControllerBase
{
    private readonly IHubContext<NotificationHub> _notificationHubContext;

    public MessageApiController(IHubContext<NotificationHub> notificationHubContext)
    {
        _notificationHubContext = notificationHubContext;
    }

    [HttpGet("send-server-alert")]
    public async Task<ActionResult> SendServerAlert(string alertMessage)
    {
        // 모든 연결된 클라이언트에게 "ServerAlert" 이벤트를 통해 메시지를 전송합니다.
        await _notificationHubContext.Clients.All.SendAsync("ServerAlert", $"서버 알림: {alertMessage}");
        return Ok($"알림 '{alertMessage}'이(가) 전송되었습니다.");
    }
}

클라이언트 측에서는 새로운 메시지 이벤트(예: "ServerAlert")에 대한 리스너를 추가해야 합니다.

// 클라이언트 측: 새로운 메시지 이벤트 리스너 추가 (Index.razor @code 블록 내부)
hubClientConnection.On<string>("ServerAlert", (alert) =>
{
    receivedMessages.Add(alert);
    InvokeAsync(StateHasChanged);
});

3. SignalR 연결 협상 과정 건너뛰기 (Skip Negotiation)

SignalR은 WebSocket, Server-Sent Events(SSE), Long Polling 등 여러 전송 방식을 추상화하여 사용합니다. 클라이언트는 처음에 negotiate 요청을 보내 서버와 어떤 전송 방식을 사용할지 협상(handshake)합니다. 이 과정에서 서버는 연결 정보를 생성합니다. 다중 서버 환경에서는 협상 요청을 받은 서버와 실제 연결이 수립되는 서버가 다를 수 있어 문제가 발생할 수 있습니다.

대부분의 최신 브라우저가 WebSocket을 지원하므로, 협상 과정을 건너뛰고 바로 WebSocket을 사용하도록 설정할 수 있습니다. 이는 특히 분산 환경에서 유용합니다.

// 클라이언트 측: 협상 건너뛰기 및 WebSocket 강제 사용 설정
hubClientConnection = new HubConnectionBuilder()
    .WithUrl("https://localhost:7110/hub/notification", options =>
    {
        options.SkipNegotiation = true; // 협상 과정 건너뛰기
        options.Transports = HttpTransportType.WebSockets; // WebSocket 전송 방식 강제
    })
    .WithAutomaticReconnect()
    .Build();

4. SignalR 분산 배포 (Redis 사용)

여러 서버에 SignalR 서비스를 배포할 경우, 각 서버는 독립적인 허브 인스턴스를 가지게 됩니다. 이로 인해 특정 서버에 연결된 클라이언트만이 해당 서버를 통해 전송된 메시지를 수신하는 문제가 발생합니다. 이 문제를 해결하기 위해 Redis와 같은 백플레인(Backplane)을 사용하여 모든 SignalR 서버 인스턴스 간에 메시지를 동기화할 수 있습니다.

먼저 Microsoft.AspNetCore.SignalR.StackExchangeRedis NuGet 패키지를 설치합니다. 그리고 Program.cs에서 SignalR 서비스 등록 시 Redis 백플레인을 추가합니다.

// Program.cs (서버 측)
// Redis 백플레인을 추가하여 분산 환경에서 메시지 동기화를 지원합니다.
appBuilder.Services.AddSignalR().AddStackExchangeRedis("localhost:6379", redisOptions =>
{
    redisOptions.Configuration.ChannelPrefix = "AppNotifications_"; // Redis 채널 접두사 설정
});

AddStackExchangeRedis 메서드의 첫 번째 매개변수는 Redis 서버의 주소입니다. ChannelPrefix는 이 애플리케이션의 메시지에 사용할 Redis 채널의 접두사를 지정합니다.

5. 통신 프로토콜 비교 (TCP, HTTP, WebSocket 등)

실시간 통신과 웹 서비스에서 자주 사용되는 몇 가지 통신 프로토콜의 주요 특징과 차이점을 이해하는 것은 중요합니다.

5.1. TCP, HTTP, WebSocket

  • TCP (Transmission Control Protocol): OSI 모델의 전송 계층 프로토콜로, 신뢰할 수 있는 연결 지향 데이터 스트림을 제공합니다. HTTP와 WebSocket 모두 TCP 위에 구축됩니다.
  • HTTP (Hypertext Transfer Protocol): OSI 모델의 응용 계층 프로토콜로, 클라이언트-서버 모델에서 요청-응답 방식으로 동작합니다. 일반적으로 무상태(stateless)이며, 각 요청 후 연결이 끊어집니다. 텍스트 기반 데이터 전송에 사용됩니다.
  • WebSocket: HTTP와 마찬가지로 응용 계층 프로토콜이지만, TCP 위에 구축되어 있습니다. HTTP를 통한 초기 핸드셰이크 후, 클라이언트와 서버 간에 영구적인 양방향(full-duplex) 통신 채널을 생성합니다. 이를 통해 서버가 클라이언트에게 비동기적으로 메시지를 푸시할 수 있습니다.

5.2. HTTP와 WebSocket의 특징 및 차이점

특징 HTTP WebSocket
통신 방식 요청-응답 (단방향) 양방향 (Full-duplex)
연결 유지 무상태, 연결 단절 상태 유지, 장기 연결 (Long-lived)
데이터 형식 주로 텍스트 텍스트 및 바이너리
오버헤드 각 요청에 헤더 포함, 오버헤드 큼 초기 핸드셰이크 후 프레임 기반, 오버헤드 작음
방화벽 통과 일반적으로 용이 (80/443 포트) HTTP 핸드셰이크를 사용하므로 방화벽 통과 용이
동일 출처 정책 적용 (CORS 필요) 적용되지 않음 (Cross-origin 가능)

5.3. HTTP/1.x, HTTP/2, HTTP/3

  • HTTP/1.x:
    • 문제점: 한 번에 하나의 요청만 처리 가능 (HOL 블로킹), 헤더 중복 전송, 텍스트 기반으로 인한 비효율성.
  • HTTP/2:
    • 개선점: 다중화(Multiplexing)를 통해 하나의 TCP 연결로 여러 요청 동시 처리, 헤더 압축, 서버 푸시(Server Push) 기능 도입, 바이너리 프레이밍으로 효율성 증대.
  • HTTP/3:
    • 개선점: TCP 대신 UDP 기반의 QUIC 프로토콜 사용. TCP의 HOL(Head-of-Line) 블로킹 문제 해결, 연결 설정 시간 단축, 향상된 혼잡 제어 및 패킷 손실 복구 능력.

5.4. RPC (Remote Procedure Call)

  • RPC는 프로토콜이라기보다는 원격지에 있는 프로시저(함수/메서드)를 로컬에 있는 것처럼 호출할 수 있게 해주는 개념 또는 통신 방식입니다.
  • TCP 또는 HTTP 기반으로 구현될 수 있으며, 바이너리 직렬화를 통해 데이터 전송 효율이 높습니다.
  • 로드 밸런싱 기능을 내장할 수 있으며, 주로 서비스 간 고성능 통신에 사용됩니다.
  • gRPC: Google이 개발한 오픈소스 RPC 프레임워크로, HTTP/2를 기반으로 하며 Protocol Buffers를 사용하여 데이터 직렬화를 수행합니다. 언어 중립적이며 고성능을 제공합니다.

태그: ASP.NET Core SignalR Real-time Communication websocket Blazor WebAssembly

7월 16일 21:32에 게시됨