항공사 정보 캐싱 샘플

구조 설명

  • AirlineCode (모델) — Code, NameKr, NameEn, Country 4개의 속성만 가진 단순 데이터 클래스
  • AirlineCodeCacheService (핵심 서비스) — IMemoryCache와 MySQL 커넥션 문자열을 필드로 가지며, GetAirlineCodesAsync, GetAirlineByCodeAsync, RefreshCacheAsync 메서드로 AirlineCode 리스트를 생성·반환
  • AirlineController (API 계층) — 서비스를 주입받아( 마름모 = 집합 관계) HTTP 엔드포인트로 노출
  • 점선 화살표는 서비스가 AirlineCode 객체들을 생성/반환하는 흐름을, 우측의 IMemoryCache·MySqlConnection은 외부 의존성을 나타냅니다

1. 필요한 패키지 설치

bash

dotnet add package MySqlConnector
dotnet add package Microsoft.Extensions.Caching.Memory

MySqlConnector는 비동기 처리가 빠르고 안정적이라 MySql.Data보다 추천됩니다.

2. 모델 클래스

csharp

public class AirlineCode
{
    public string Code { get; set; }       // 항공사 코드 (예: KE, OZ)
    public string NameKr { get; set; }      // 항공사명 (한글)
    public string NameEn { get; set; }      // 항공사명 (영문)
    public string Country { get; set; }     // 국가
}

3. 캐시 서비스 클래스 (MySQL + 비동기)

csharp

using Microsoft.Extensions.Caching.Memory;
using MySqlConnector;

public class AirlineCodeCacheService
{
    private readonly IMemoryCache _cache;
    private readonly string _connectionString;
    private const string CacheKey = "AirlineCodeList";

    // 동시에 여러 요청이 DB를 동시에 때리는 것(캐시 스탬피드) 방지용
    private static readonly SemaphoreSlim _lock = new(1, 1);

    public AirlineCodeCacheService(IMemoryCache cache, string connectionString)
    {
        _cache = cache;
        _connectionString = connectionString;
    }

    /// <summary>
    /// 항공사 코드 전체 목록 조회 (캐시 우선)
    /// </summary>
    public async Task<List<AirlineCode>> GetAirlineCodesAsync()
    {
        if (_cache.TryGetValue(CacheKey, out List<AirlineCode> cachedList))
        {
            return cachedList;
        }

        await _lock.WaitAsync();
        try
        {
            // 락 획득 후 다시 한번 체크 (다른 스레드가 이미 채워놨을 수도 있음)
            if (_cache.TryGetValue(CacheKey, out cachedList))
            {
                return cachedList;
            }

            var list = await LoadFromDatabaseAsync();

            var cacheOptions = new MemoryCacheEntryOptions()
                .SetAbsoluteExpiration(TimeSpan.FromHours(6))
                .SetSlidingExpiration(TimeSpan.FromMinutes(30))
                .SetPriority(CacheItemPriority.Normal);

            _cache.Set(CacheKey, list, cacheOptions);

            return list;
        }
        finally
        {
            _lock.Release();
        }
    }

    /// <summary>
    /// 특정 코드로 항공사 정보 조회
    /// </summary>
    public async Task<AirlineCode> GetAirlineByCodeAsync(string code)
    {
        var list = await GetAirlineCodesAsync();
        return list.FirstOrDefault(x => x.Code.Equals(code, StringComparison.OrdinalIgnoreCase));
    }

    /// <summary>
    /// 캐시 강제 갱신 (데이터 변경 시 호출)
    /// </summary>
    public async Task RefreshCacheAsync()
    {
        _cache.Remove(CacheKey);
        await GetAirlineCodesAsync();
    }

    /// <summary>
    /// MySQL에서 실제 데이터 로드
    /// </summary>
    private async Task<List<AirlineCode>> LoadFromDatabaseAsync()
    {
        var result = new List<AirlineCode>();

        using var conn = new MySqlConnection(_connectionString);
        await conn.OpenAsync();

        using var cmd = new MySqlCommand(
            "SELECT CODE, NAME_KR, NAME_EN, COUNTRY FROM TB_AIRLINE_CODE", conn);

        using var reader = await cmd.ExecuteReaderAsync();
        while (await reader.ReadAsync())
        {
            result.Add(new AirlineCode
            {
                Code = reader["CODE"].ToString(),
                NameKr = reader["NAME_KR"].ToString(),
                NameEn = reader["NAME_EN"].ToString(),
                Country = reader["COUNTRY"].ToString()
            });
        }

        return result;
    }
}

4. Program.cs 등록

csharp

var builder = WebApplication.CreateBuilder(args);

// MemoryCache 등록
builder.Services.AddMemoryCache();

// appsettings.json의 ConnectionStrings:DefaultConnection 사용
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");

builder.Services.AddSingleton(sp =>
{
    var cache = sp.GetRequiredService<IMemoryCache>();
    return new AirlineCodeCacheService(cache, connectionString);
});

var app = builder.Build();

5. appsettings.json

json

{
  "ConnectionStrings": {
    "DefaultConnection": "Server=localhost;Port=3306;Database=mydb;User=root;Password=yourpassword;"
  }
}

6. 컨트롤러 사용 예시

csharp

[ApiController]
[Route("api/[controller]")]
public class AirlineController : ControllerBase
{
    private readonly AirlineCodeCacheService _airlineCacheService;

    public AirlineController(AirlineCodeCacheService airlineCacheService)
    {
        _airlineCacheService = airlineCacheService;
    }

    [HttpGet]
    public async Task<IActionResult> GetAll()
    {
        var list = await _airlineCacheService.GetAirlineCodesAsync();
        return Ok(list);
    }

    [HttpGet("{code}")]
    public async Task<IActionResult> GetByCode(string code)
    {
        var airline = await _airlineCacheService.GetAirlineByCodeAsync(code);
        if (airline == null) return NotFound();
        return Ok(airline);
    }

    [HttpPost("refresh")]
    public async Task<IActionResult> Refresh()
    {
        await _airlineCacheService.RefreshCacheAsync();
        return Ok("캐시가 갱신되었습니다.");
    }
}

변경/추가된 핵심 사항

항목설명
MySqlConnectorMySQL 전용 비동기 드라이버 사용
SemaphoreSlim캐시 만료 순간 동시 다발 DB 조회(스탬피드) 방지
전부 async/awaitI/O 바운드 작업이므로 비동기로 처리해 스레드 블로킹 방지