feat: Redis 토큰 캐시 관리 구현 (#154) #155
11
SPMS.Application/Interfaces/ITokenCacheService.cs
Normal file
11
SPMS.Application/Interfaces/ITokenCacheService.cs
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
namespace SPMS.Application.Interfaces;
|
||||||
|
|
||||||
|
public interface ITokenCacheService
|
||||||
|
{
|
||||||
|
Task<CachedDeviceInfo?> GetDeviceInfoAsync(long serviceId, long deviceId);
|
||||||
|
Task SetDeviceInfoAsync(long serviceId, long deviceId, CachedDeviceInfo info);
|
||||||
|
Task InvalidateAsync(long serviceId, long deviceId);
|
||||||
|
Task InvalidateByServiceAsync(long serviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public record CachedDeviceInfo(string Token, int Platform, bool IsActive, bool PushAgreed);
|
||||||
|
|
@ -13,11 +13,16 @@ public class DeviceService : IDeviceService
|
||||||
{
|
{
|
||||||
private readonly IDeviceRepository _deviceRepository;
|
private readonly IDeviceRepository _deviceRepository;
|
||||||
private readonly IUnitOfWork _unitOfWork;
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
|
private readonly ITokenCacheService _tokenCache;
|
||||||
|
|
||||||
public DeviceService(IDeviceRepository deviceRepository, IUnitOfWork unitOfWork)
|
public DeviceService(
|
||||||
|
IDeviceRepository deviceRepository,
|
||||||
|
IUnitOfWork unitOfWork,
|
||||||
|
ITokenCacheService tokenCache)
|
||||||
{
|
{
|
||||||
_deviceRepository = deviceRepository;
|
_deviceRepository = deviceRepository;
|
||||||
_unitOfWork = unitOfWork;
|
_unitOfWork = unitOfWork;
|
||||||
|
_tokenCache = tokenCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<DeviceRegisterResponseDto> RegisterAsync(long serviceId, DeviceRegisterRequestDto request)
|
public async Task<DeviceRegisterResponseDto> RegisterAsync(long serviceId, DeviceRegisterRequestDto request)
|
||||||
|
|
@ -35,6 +40,7 @@ public class DeviceService : IDeviceService
|
||||||
existing.UpdatedAt = DateTime.UtcNow;
|
existing.UpdatedAt = DateTime.UtcNow;
|
||||||
_deviceRepository.Update(existing);
|
_deviceRepository.Update(existing);
|
||||||
await _unitOfWork.SaveChangesAsync();
|
await _unitOfWork.SaveChangesAsync();
|
||||||
|
await _tokenCache.InvalidateAsync(serviceId, existing.Id);
|
||||||
|
|
||||||
return new DeviceRegisterResponseDto { DeviceId = existing.Id, IsNew = false };
|
return new DeviceRegisterResponseDto { DeviceId = existing.Id, IsNew = false };
|
||||||
}
|
}
|
||||||
|
|
@ -97,6 +103,7 @@ public class DeviceService : IDeviceService
|
||||||
device.UpdatedAt = DateTime.UtcNow;
|
device.UpdatedAt = DateTime.UtcNow;
|
||||||
_deviceRepository.Update(device);
|
_deviceRepository.Update(device);
|
||||||
await _unitOfWork.SaveChangesAsync();
|
await _unitOfWork.SaveChangesAsync();
|
||||||
|
await _tokenCache.InvalidateAsync(serviceId, device.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task DeleteAsync(long serviceId, DeviceDeleteRequestDto request)
|
public async Task DeleteAsync(long serviceId, DeviceDeleteRequestDto request)
|
||||||
|
|
@ -109,6 +116,7 @@ public class DeviceService : IDeviceService
|
||||||
device.UpdatedAt = DateTime.UtcNow;
|
device.UpdatedAt = DateTime.UtcNow;
|
||||||
_deviceRepository.Update(device);
|
_deviceRepository.Update(device);
|
||||||
await _unitOfWork.SaveChangesAsync();
|
await _unitOfWork.SaveChangesAsync();
|
||||||
|
await _tokenCache.InvalidateAsync(serviceId, device.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<DeviceListResponseDto> GetListAsync(long serviceId, DeviceListRequestDto request)
|
public async Task<DeviceListResponseDto> GetListAsync(long serviceId, DeviceListRequestDto request)
|
||||||
|
|
@ -169,6 +177,7 @@ public class DeviceService : IDeviceService
|
||||||
device.MktAgreeUpdatedAt = DateTime.UtcNow;
|
device.MktAgreeUpdatedAt = DateTime.UtcNow;
|
||||||
_deviceRepository.Update(device);
|
_deviceRepository.Update(device);
|
||||||
await _unitOfWork.SaveChangesAsync();
|
await _unitOfWork.SaveChangesAsync();
|
||||||
|
await _tokenCache.InvalidateAsync(serviceId, device.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Platform ParsePlatform(string platform)
|
private static Platform ParsePlatform(string platform)
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,15 @@ public class RedisConnection : IAsyncDisposable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public IServer? GetServer()
|
||||||
|
{
|
||||||
|
if (_connection is not { IsConnected: true })
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var endpoint = _connection.GetEndPoints().FirstOrDefault();
|
||||||
|
return endpoint != null ? _connection.GetServer(endpoint) : null;
|
||||||
|
}
|
||||||
|
|
||||||
private static string MaskConnectionString(string connectionString)
|
private static string MaskConnectionString(string connectionString)
|
||||||
{
|
{
|
||||||
var parts = connectionString.Split(',');
|
var parts = connectionString.Split(',');
|
||||||
|
|
|
||||||
102
SPMS.Infrastructure/Caching/TokenCacheService.cs
Normal file
102
SPMS.Infrastructure/Caching/TokenCacheService.cs
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
using System.Text.Json;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using SPMS.Application.Interfaces;
|
||||||
|
using SPMS.Application.Settings;
|
||||||
|
using StackExchange.Redis;
|
||||||
|
|
||||||
|
namespace SPMS.Infrastructure.Caching;
|
||||||
|
|
||||||
|
public class TokenCacheService : ITokenCacheService
|
||||||
|
{
|
||||||
|
private readonly RedisConnection _redis;
|
||||||
|
private readonly RedisSettings _settings;
|
||||||
|
private readonly ILogger<TokenCacheService> _logger;
|
||||||
|
|
||||||
|
private static readonly TimeSpan CacheTtl = TimeSpan.FromHours(1);
|
||||||
|
|
||||||
|
public TokenCacheService(
|
||||||
|
RedisConnection redis,
|
||||||
|
IOptions<RedisSettings> settings,
|
||||||
|
ILogger<TokenCacheService> logger)
|
||||||
|
{
|
||||||
|
_redis = redis;
|
||||||
|
_settings = settings.Value;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<CachedDeviceInfo?> GetDeviceInfoAsync(long serviceId, long deviceId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var db = await _redis.GetDatabaseAsync();
|
||||||
|
var value = await db.StringGetAsync(BuildKey(serviceId, deviceId));
|
||||||
|
|
||||||
|
if (value.IsNullOrEmpty)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return JsonSerializer.Deserialize<CachedDeviceInfo>(value!);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "토큰 캐시 조회 실패: serviceId={ServiceId}, deviceId={DeviceId}",
|
||||||
|
serviceId, deviceId);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SetDeviceInfoAsync(long serviceId, long deviceId, CachedDeviceInfo info)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var db = await _redis.GetDatabaseAsync();
|
||||||
|
var json = JsonSerializer.Serialize(info);
|
||||||
|
await db.StringSetAsync(BuildKey(serviceId, deviceId), json, CacheTtl);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "토큰 캐시 저장 실패: serviceId={ServiceId}, deviceId={DeviceId}",
|
||||||
|
serviceId, deviceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task InvalidateAsync(long serviceId, long deviceId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var db = await _redis.GetDatabaseAsync();
|
||||||
|
await db.KeyDeleteAsync(BuildKey(serviceId, deviceId));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "토큰 캐시 무효화 실패: serviceId={ServiceId}, deviceId={DeviceId}",
|
||||||
|
serviceId, deviceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task InvalidateByServiceAsync(long serviceId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var db = await _redis.GetDatabaseAsync();
|
||||||
|
var server = _redis.GetServer();
|
||||||
|
if (server == null) return;
|
||||||
|
|
||||||
|
var pattern = $"{_settings.InstanceName}device:token:{serviceId}:*";
|
||||||
|
var keys = server.Keys(pattern: pattern).ToArray();
|
||||||
|
|
||||||
|
if (keys.Length > 0)
|
||||||
|
await db.KeyDeleteAsync(keys);
|
||||||
|
|
||||||
|
_logger.LogInformation("서비스 토큰 캐시 전체 무효화: serviceId={ServiceId}, 삭제={Count}건",
|
||||||
|
serviceId, keys.Length);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "서비스 토큰 캐시 전체 무효화 실패: serviceId={ServiceId}", serviceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string BuildKey(long serviceId, long deviceId) =>
|
||||||
|
$"{_settings.InstanceName}device:token:{serviceId}:{deviceId}";
|
||||||
|
}
|
||||||
|
|
@ -58,6 +58,7 @@ public static class DependencyInjection
|
||||||
services.AddSingleton<IDuplicateChecker, DuplicateChecker>();
|
services.AddSingleton<IDuplicateChecker, DuplicateChecker>();
|
||||||
services.AddSingleton<IScheduleCancelStore, ScheduleCancelStore>();
|
services.AddSingleton<IScheduleCancelStore, ScheduleCancelStore>();
|
||||||
services.AddSingleton<IBulkJobStore, BulkJobStore>();
|
services.AddSingleton<IBulkJobStore, BulkJobStore>();
|
||||||
|
services.AddSingleton<ITokenCacheService, TokenCacheService>();
|
||||||
|
|
||||||
// RabbitMQ
|
// RabbitMQ
|
||||||
services.Configure<RabbitMQSettings>(configuration.GetSection(RabbitMQSettings.SectionName));
|
services.Configure<RabbitMQSettings>(configuration.GetSection(RabbitMQSettings.SectionName));
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ public class PushWorker : BackgroundService
|
||||||
private readonly IServiceScopeFactory _scopeFactory;
|
private readonly IServiceScopeFactory _scopeFactory;
|
||||||
private readonly IDuplicateChecker _duplicateChecker;
|
private readonly IDuplicateChecker _duplicateChecker;
|
||||||
private readonly IBulkJobStore _bulkJobStore;
|
private readonly IBulkJobStore _bulkJobStore;
|
||||||
|
private readonly ITokenCacheService _tokenCache;
|
||||||
private readonly IFcmSender _fcmSender;
|
private readonly IFcmSender _fcmSender;
|
||||||
private readonly IApnsSender _apnsSender;
|
private readonly IApnsSender _apnsSender;
|
||||||
private readonly ILogger<PushWorker> _logger;
|
private readonly ILogger<PushWorker> _logger;
|
||||||
|
|
@ -35,6 +36,7 @@ public class PushWorker : BackgroundService
|
||||||
IServiceScopeFactory scopeFactory,
|
IServiceScopeFactory scopeFactory,
|
||||||
IDuplicateChecker duplicateChecker,
|
IDuplicateChecker duplicateChecker,
|
||||||
IBulkJobStore bulkJobStore,
|
IBulkJobStore bulkJobStore,
|
||||||
|
ITokenCacheService tokenCache,
|
||||||
IFcmSender fcmSender,
|
IFcmSender fcmSender,
|
||||||
IApnsSender apnsSender,
|
IApnsSender apnsSender,
|
||||||
ILogger<PushWorker> logger)
|
ILogger<PushWorker> logger)
|
||||||
|
|
@ -44,6 +46,7 @@ public class PushWorker : BackgroundService
|
||||||
_scopeFactory = scopeFactory;
|
_scopeFactory = scopeFactory;
|
||||||
_duplicateChecker = duplicateChecker;
|
_duplicateChecker = duplicateChecker;
|
||||||
_bulkJobStore = bulkJobStore;
|
_bulkJobStore = bulkJobStore;
|
||||||
|
_tokenCache = tokenCache;
|
||||||
_fcmSender = fcmSender;
|
_fcmSender = fcmSender;
|
||||||
_apnsSender = apnsSender;
|
_apnsSender = apnsSender;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
|
@ -289,9 +292,36 @@ public class PushWorker : BackgroundService
|
||||||
var devices = new List<Device>();
|
var devices = new List<Device>();
|
||||||
foreach (var id in deviceIds)
|
foreach (var id in deviceIds)
|
||||||
{
|
{
|
||||||
|
// Redis 캐시 우선 조회
|
||||||
|
var cached = await _tokenCache.GetDeviceInfoAsync(message.ServiceId, id);
|
||||||
|
if (cached != null)
|
||||||
|
{
|
||||||
|
if (cached.IsActive && cached.PushAgreed)
|
||||||
|
{
|
||||||
|
devices.Add(new Device
|
||||||
|
{
|
||||||
|
Id = id,
|
||||||
|
ServiceId = message.ServiceId,
|
||||||
|
DeviceToken = cached.Token,
|
||||||
|
Platform = (Platform)cached.Platform,
|
||||||
|
IsActive = true,
|
||||||
|
PushAgreed = true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 캐시 미스 → DB 조회 후 캐시 저장
|
||||||
var device = await deviceRepo.GetByIdAndServiceAsync(id, message.ServiceId);
|
var device = await deviceRepo.GetByIdAndServiceAsync(id, message.ServiceId);
|
||||||
if (device is { IsActive: true, PushAgreed: true })
|
if (device != null)
|
||||||
devices.Add(device);
|
{
|
||||||
|
await _tokenCache.SetDeviceInfoAsync(message.ServiceId, id,
|
||||||
|
new CachedDeviceInfo(device.DeviceToken, (int)device.Platform,
|
||||||
|
device.IsActive, device.PushAgreed));
|
||||||
|
|
||||||
|
if (device is { IsActive: true, PushAgreed: true })
|
||||||
|
devices.Add(device);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return devices;
|
return devices;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user