improvement: DeadTokenCleanupWorker Redis 캐시 무효화 연동 (#160)
All checks were successful
SPMS_API/pipeline/head This commit looks good

Reviewed-on: https://git.ipstein.myds.me/SPMS/SPMS_API/pulls/161
This commit is contained in:
김선규 2026-02-11 02:16:04 +00:00
commit 890feb9b4c
2 changed files with 29 additions and 1923 deletions

View File

@ -2,6 +2,7 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using SPMS.Application.Interfaces;
using SPMS.Domain.Entities; using SPMS.Domain.Entities;
using SPMS.Infrastructure.Persistence; using SPMS.Infrastructure.Persistence;
@ -10,6 +11,7 @@ namespace SPMS.Infrastructure.Workers;
public class DeadTokenCleanupWorker : BackgroundService public class DeadTokenCleanupWorker : BackgroundService
{ {
private readonly IServiceScopeFactory _scopeFactory; private readonly IServiceScopeFactory _scopeFactory;
private readonly ITokenCacheService _tokenCache;
private readonly ILogger<DeadTokenCleanupWorker> _logger; private readonly ILogger<DeadTokenCleanupWorker> _logger;
private static readonly TimeZoneInfo KstZone = TimeZoneInfo.FindSystemTimeZoneById("Asia/Seoul"); private static readonly TimeZoneInfo KstZone = TimeZoneInfo.FindSystemTimeZoneById("Asia/Seoul");
@ -18,9 +20,11 @@ public class DeadTokenCleanupWorker : BackgroundService
public DeadTokenCleanupWorker( public DeadTokenCleanupWorker(
IServiceScopeFactory scopeFactory, IServiceScopeFactory scopeFactory,
ITokenCacheService tokenCache,
ILogger<DeadTokenCleanupWorker> logger) ILogger<DeadTokenCleanupWorker> logger)
{ {
_scopeFactory = scopeFactory; _scopeFactory = scopeFactory;
_tokenCache = tokenCache;
_logger = logger; _logger = logger;
} }
@ -99,23 +103,38 @@ public class DeadTokenCleanupWorker : BackgroundService
return; return;
} }
// 배치 삭제 (1000건씩, 트랜잭션 없음) // 배치 삭제 (1000건씩, 트랜잭션 없음) + Redis 캐시 무효화
var totalDeleted = 0; var totalDeleted = 0;
int deletedInBatch;
do while (!stoppingToken.IsCancellationRequested)
{ {
deletedInBatch = await context.Database.ExecuteSqlRawAsync( var batch = await context.Set<Device>()
"DELETE FROM Device WHERE is_active = false AND updated_at < {0} LIMIT 1000", .Where(d => !d.IsActive && d.UpdatedAt < cutoffUtc)
new object[] { cutoffUtc }, .Select(d => new { d.Id, d.ServiceId })
.Take(BatchSize)
.ToListAsync(stoppingToken);
if (batch.Count == 0)
break;
var ids = batch.Select(b => b.Id).ToList();
var idList = string.Join(",", ids);
#pragma warning disable EF1002 // ID는 내부 DB 쿼리에서 추출한 long 값 — SQL injection 위험 없음
var deletedInBatch = await context.Database.ExecuteSqlRawAsync(
$"DELETE FROM Device WHERE id IN ({idList})",
stoppingToken); stoppingToken);
#pragma warning restore EF1002
totalDeleted += deletedInBatch; totalDeleted += deletedInBatch;
if (deletedInBatch > 0) // 삭제된 디바이스의 Redis 캐시 무효화
_logger.LogInformation("DeadTokenCleanupWorker 배치 삭제: {BatchCount}건 (누적: {TotalDeleted}건)", foreach (var item in batch)
deletedInBatch, totalDeleted); await _tokenCache.InvalidateAsync(item.ServiceId, item.Id);
} while (deletedInBatch > 0 && !stoppingToken.IsCancellationRequested);
_logger.LogInformation("DeadTokenCleanupWorker 배치 삭제: {BatchCount}건 (누적: {TotalDeleted}건), 캐시 무효화 완료",
deletedInBatch, totalDeleted);
}
await WriteSystemLogAsync(context, totalDeleted, stoppingToken); await WriteSystemLogAsync(context, totalDeleted, stoppingToken);

1913
TASKS.md

File diff suppressed because it is too large Load Diff