fix: RabbitMQ 연결 실패 시 앱 크래시 방지 (#124) #125

Merged
seonkyu.kim merged 2 commits from fix/#124-rabbitmq-crash into develop 2026-02-10 10:16:33 +00:00
4 changed files with 101 additions and 66 deletions

View File

@ -4,6 +4,7 @@ using Microsoft.EntityFrameworkCore;
using Swashbuckle.AspNetCore.Annotations; using Swashbuckle.AspNetCore.Annotations;
using SPMS.Domain.Common; using SPMS.Domain.Common;
using SPMS.Infrastructure; using SPMS.Infrastructure;
using SPMS.Infrastructure.Messaging;
namespace SPMS.API.Controllers; namespace SPMS.API.Controllers;
@ -14,10 +15,17 @@ namespace SPMS.API.Controllers;
public class PublicController : ControllerBase public class PublicController : ControllerBase
{ {
private readonly AppDbContext _dbContext; private readonly AppDbContext _dbContext;
private readonly RabbitMQConnection _rabbitConnection;
private readonly RabbitMQInitializer _rabbitInitializer;
public PublicController(AppDbContext dbContext) public PublicController(
AppDbContext dbContext,
RabbitMQConnection rabbitConnection,
RabbitMQInitializer rabbitInitializer)
{ {
_dbContext = dbContext; _dbContext = dbContext;
_rabbitConnection = rabbitConnection;
_rabbitInitializer = rabbitInitializer;
} }
[HttpPost("health")] [HttpPost("health")]
@ -39,11 +47,26 @@ public class PublicController : ControllerBase
allHealthy = false; allHealthy = false;
} }
// 2. Redis 연결 확인 (Phase 2에서 구현 예정) // 2. Redis 연결 확인 (Phase 3-2에서 구현 예정)
checks["redis"] = new { status = "not_configured" }; checks["redis"] = new { status = "not_configured" };
// 3. RabbitMQ 연결 확인 (Phase 2에서 구현 예정) // 3. RabbitMQ 연결 확인
checks["rabbitmq"] = new { status = "not_configured" }; var rabbitConnected = _rabbitConnection.IsConnected;
var rabbitInitialized = _rabbitInitializer.IsInitialized;
if (rabbitConnected && rabbitInitialized)
{
checks["rabbitmq"] = new { status = "healthy" };
}
else
{
checks["rabbitmq"] = new
{
status = "unhealthy",
connected = rabbitConnected,
initialized = rabbitInitialized
};
allHealthy = false;
}
if (allHealthy) if (allHealthy)
{ {

View File

@ -58,7 +58,8 @@ public static class DependencyInjection
// RabbitMQ // RabbitMQ
services.Configure<RabbitMQSettings>(configuration.GetSection(RabbitMQSettings.SectionName)); services.Configure<RabbitMQSettings>(configuration.GetSection(RabbitMQSettings.SectionName));
services.AddSingleton<RabbitMQConnection>(); services.AddSingleton<RabbitMQConnection>();
services.AddHostedService<RabbitMQInitializer>(); services.AddSingleton<RabbitMQInitializer>();
services.AddHostedService(sp => sp.GetRequiredService<RabbitMQInitializer>());
services.AddScoped<IPushQueueService, PushQueueService>(); services.AddScoped<IPushQueueService, PushQueueService>();
// Push Senders // Push Senders

View File

@ -12,6 +12,8 @@ public class RabbitMQConnection : IAsyncDisposable
private readonly SemaphoreSlim _semaphore = new(1, 1); private readonly SemaphoreSlim _semaphore = new(1, 1);
private IConnection? _connection; private IConnection? _connection;
public bool IsConnected => _connection is { IsOpen: true };
public RabbitMQConnection( public RabbitMQConnection(
IOptions<RabbitMQSettings> settings, IOptions<RabbitMQSettings> settings,
ILogger<RabbitMQConnection> logger) ILogger<RabbitMQConnection> logger)

View File

@ -6,11 +6,14 @@ using SPMS.Application.Settings;
namespace SPMS.Infrastructure.Messaging; namespace SPMS.Infrastructure.Messaging;
public class RabbitMQInitializer : IHostedService public class RabbitMQInitializer : BackgroundService
{ {
private readonly RabbitMQConnection _connection; private readonly RabbitMQConnection _connection;
private readonly RabbitMQSettings _settings; private readonly RabbitMQSettings _settings;
private readonly ILogger<RabbitMQInitializer> _logger; private readonly ILogger<RabbitMQInitializer> _logger;
private static readonly TimeSpan RetryInterval = TimeSpan.FromSeconds(30);
public bool IsInitialized { get; private set; }
public RabbitMQInitializer( public RabbitMQInitializer(
RabbitMQConnection connection, RabbitMQConnection connection,
@ -22,70 +25,76 @@ public class RabbitMQInitializer : IHostedService
_logger = logger; _logger = logger;
} }
public async Task StartAsync(CancellationToken cancellationToken) protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{ {
try while (!stoppingToken.IsCancellationRequested)
{ {
await using var channel = await _connection.CreateChannelAsync(cancellationToken); try
// Exchange 선언: Direct, Durable
await channel.ExchangeDeclareAsync(
exchange: _settings.Exchange,
type: ExchangeType.Direct,
durable: true,
autoDelete: false,
arguments: null,
cancellationToken: cancellationToken);
_logger.LogInformation("Exchange 선언 완료: {Exchange}", _settings.Exchange);
var queueArgs = new Dictionary<string, object?>
{ {
{ "x-message-ttl", _settings.MessageTtl } await InitializeAsync(stoppingToken);
}; IsInitialized = true;
_logger.LogInformation("RabbitMQ 초기화 완료");
// Push Queue 선언 return;
await channel.QueueDeclareAsync( }
queue: _settings.PushQueue, catch (Exception ex)
durable: true, {
exclusive: false, _logger.LogWarning(ex, "RabbitMQ 초기화 실패 — {RetrySeconds}초 후 재시도합니다.", RetryInterval.TotalSeconds);
autoDelete: false, await Task.Delay(RetryInterval, stoppingToken);
arguments: queueArgs, }
cancellationToken: cancellationToken);
await channel.QueueBindAsync(
queue: _settings.PushQueue,
exchange: _settings.Exchange,
routingKey: "push",
cancellationToken: cancellationToken);
_logger.LogInformation("Queue 선언 및 바인딩 완료: {Queue} → {Exchange} (routing_key: push)",
_settings.PushQueue, _settings.Exchange);
// Schedule Queue 선언
await channel.QueueDeclareAsync(
queue: _settings.ScheduleQueue,
durable: true,
exclusive: false,
autoDelete: false,
arguments: queueArgs,
cancellationToken: cancellationToken);
await channel.QueueBindAsync(
queue: _settings.ScheduleQueue,
exchange: _settings.Exchange,
routingKey: "schedule",
cancellationToken: cancellationToken);
_logger.LogInformation("Queue 선언 및 바인딩 완료: {Queue} → {Exchange} (routing_key: schedule)",
_settings.ScheduleQueue, _settings.Exchange);
}
catch (Exception ex)
{
_logger.LogError(ex, "RabbitMQ Exchange/Queue 초기화 실패");
throw;
} }
} }
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; private async Task InitializeAsync(CancellationToken cancellationToken)
{
await using var channel = await _connection.CreateChannelAsync(cancellationToken);
await channel.ExchangeDeclareAsync(
exchange: _settings.Exchange,
type: ExchangeType.Direct,
durable: true,
autoDelete: false,
arguments: null,
cancellationToken: cancellationToken);
_logger.LogInformation("Exchange 선언 완료: {Exchange}", _settings.Exchange);
var queueArgs = new Dictionary<string, object?>
{
{ "x-message-ttl", _settings.MessageTtl }
};
await channel.QueueDeclareAsync(
queue: _settings.PushQueue,
durable: true,
exclusive: false,
autoDelete: false,
arguments: queueArgs,
cancellationToken: cancellationToken);
await channel.QueueBindAsync(
queue: _settings.PushQueue,
exchange: _settings.Exchange,
routingKey: "push",
cancellationToken: cancellationToken);
_logger.LogInformation("Queue 선언 및 바인딩 완료: {Queue} → {Exchange} (routing_key: push)",
_settings.PushQueue, _settings.Exchange);
await channel.QueueDeclareAsync(
queue: _settings.ScheduleQueue,
durable: true,
exclusive: false,
autoDelete: false,
arguments: queueArgs,
cancellationToken: cancellationToken);
await channel.QueueBindAsync(
queue: _settings.ScheduleQueue,
exchange: _settings.Exchange,
routingKey: "schedule",
cancellationToken: cancellationToken);
_logger.LogInformation("Queue 선언 및 바인딩 완료: {Queue} → {Exchange} (routing_key: schedule)",
_settings.ScheduleQueue, _settings.Exchange);
}
} }