์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐Ÿš€ C#์œผ๋กœ ๊ตฌํ˜„ํ•˜๋Š” ํ™•์žฅ ๊ฐ€๋Šฅํ•œ ์›น ์„œ๋น„์Šค ์•„ํ‚คํ…์ฒ˜: ์„ฑ์žฅํ•˜๋Š” ์„œ๋น„์Šค๋ฅผ ์œ„ํ•œ ์‹ค์ „ ์„ค๊ณ„ ๊ฐ€์ด๋“œ

๐Ÿš€ C#์œผ๋กœ ๊ตฌํ˜„ํ•˜๋Š” ํ™•์žฅ ๊ฐ€๋Šฅํ•œ ์›น ์„œ๋น„์Šค ์•„ํ‚คํ…์ฒ˜: ์„ฑ์žฅํ•˜๋Š” ์„œ๋น„์Šค๋ฅผ ์œ„ํ•œ ์‹ค์ „ ์„ค๊ณ„ ๊ฐ€์ด๋“œ

์นœ๊ตฌ์•ผ, ์˜ค๋Š˜์€ ์ง„์งœ ์‹ค๋ฌด์—์„œ ์จ๋จน์„ ์ˆ˜ ์žˆ๋Š” C# ์›น ์„œ๋น„์Šค ์•„ํ‚คํ…์ฒ˜ ์ด์•ผ๊ธฐ๋ฅผ ํ•ด๋ณผ๊ฒŒ! ๐ŸŽฏ

์•ผ, ๋„ˆ๋„ ๊ฐœ๋ฐœํ•˜๋‹ค ๋ณด๋ฉด ์ด๋Ÿฐ ๊ฒฝํ—˜ ์žˆ์ง€ ์•Š์•„? ์ฒ˜์Œ์—” ๊ฐ„๋‹จํ•˜๊ฒŒ ๋งŒ๋“ค์—ˆ๋˜ ์„œ๋น„์Šค๊ฐ€ ์‚ฌ์šฉ์ž๊ฐ€ ๋Š˜์–ด๋‚˜๋ฉด์„œ ์ ์  ๋А๋ ค์ง€๊ณ , ์ฝ”๋“œ๋Š” ์ŠคํŒŒ๊ฒŒํ‹ฐ์ฒ˜๋Ÿผ ์—‰์ผœ๊ฐ€๊ณ ... ๐Ÿ˜… ๊ทธ๋Ÿด ๋•Œ๋งˆ๋‹ค "์•„, ์ฒ˜์Œ๋ถ€ํ„ฐ ์ œ๋Œ€๋กœ ์„ค๊ณ„ํ•  ๊ฑธ" ํ•˜๊ณ  ํ›„ํšŒํ•˜๊ฒŒ ๋˜์ž–์•„?

์˜ค๋Š˜์€ ๊ทธ๋Ÿฐ ๊ณ ๋ฏผ์„ ํ•ด๊ฒฐํ•ด์ค„ ํ™•์žฅ ๊ฐ€๋Šฅํ•œ ์›น ์„œ๋น„์Šค ์•„ํ‚คํ…์ฒ˜์— ๋Œ€ํ•ด ๊นŠ์ด ์žˆ๊ฒŒ ํŒŒํ—ค์ณ๋ณผ ๊ฑฐ์•ผ. C#๊ณผ .NET ์ƒํƒœ๊ณ„๋ฅผ ํ™œ์šฉํ•ด์„œ ๋ง์ด์ง€! ์‹ค์ œ๋กœ ๋‚ด๊ฐ€ ํ”„๋กœ์ ํŠธ์—์„œ ๊ฒช์—ˆ๋˜ ์‹œํ–‰์ฐฉ์˜ค๋“ค๊ณผ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•๋“ค์„ ๋ชจ๋‘ ๋‹ด์•˜์œผ๋‹ˆ๊นŒ, ๋๊นŒ์ง€ ์ง‘์ค‘ํ•ด์ค˜! ๐Ÿ’ช

ํ™•์žฅ ๊ฐ€๋Šฅํ•œ ์•„ํ‚คํ…์ฒ˜ ๊ตฌ์กฐ Client Layer Web / Mobile API Consumer API Gateway Load Balancer Authentication Service A Business Logic Microservice Service B Business Logic Microservice Service C Business Logic Microservice Data Layer Database / Cache / Message Queue Monitoring Logging Cache Redis

๐ŸŽฏ ํ™•์žฅ ๊ฐ€๋Šฅํ•œ ์•„ํ‚คํ…์ฒ˜๊ฐ€ ๋ญ๊ธธ๋ž˜?

๋จผ์ € ๊ธฐ๋ณธ๋ถ€ํ„ฐ ์งš๊ณ  ๋„˜์–ด๊ฐ€์ž. ํ™•์žฅ ๊ฐ€๋Šฅํ•œ ์•„ํ‚คํ…์ฒ˜(Scalable Architecture)๋ž€ ๋ญ˜๊นŒ? ๊ฐ„๋‹จํžˆ ๋งํ•˜๋ฉด, ์‚ฌ์šฉ์ž๊ฐ€ 10๋ช…์ด๋“  10๋งŒ ๋ช…์ด๋“  ์„œ๋น„์Šค๊ฐ€ ์•ˆ์ •์ ์œผ๋กœ ๋Œ์•„๊ฐˆ ์ˆ˜ ์žˆ๋„๋ก ์„ค๊ณ„๋œ ๊ตฌ์กฐ๋ฅผ ๋งํ•ด. ๐Ÿ—๏ธ

ํ™•์žฅ์„ฑ์˜ ๋‘ ๊ฐ€์ง€ ๋ฐฉํ–ฅ:

์ˆ˜์ง ํ™•์žฅ(Scale Up): ์„œ๋ฒ„ ํ•œ ๋Œ€์˜ ์„ฑ๋Šฅ์„ ์—…๊ทธ๋ ˆ์ด๋“œํ•˜๋Š” ๊ฑฐ์•ผ. CPU ๋” ์ข‹์€ ๊ฑธ๋กœ, ๋ฉ”๋ชจ๋ฆฌ ๋” ๋งŽ์ด! ๊ทผ๋ฐ ์ด๊ฑด ํ•œ๊ณ„๊ฐ€ ์žˆ์ง€.

์ˆ˜ํ‰ ํ™•์žฅ(Scale Out): ์„œ๋ฒ„๋ฅผ ์—ฌ๋Ÿฌ ๋Œ€๋กœ ๋Š˜๋ฆฌ๋Š” ๊ฑฐ์•ผ. ์ด๊ฒŒ ์ง„์งœ ํ™•์žฅ ๊ฐ€๋Šฅํ•œ ์•„ํ‚คํ…์ฒ˜์˜ ํ•ต์‹ฌ์ด์•ผ! ๐Ÿ’ก

C#์œผ๋กœ ์›น ์„œ๋น„์Šค๋ฅผ ๋งŒ๋“ค ๋•Œ, ์šฐ๋ฆฌ๋Š” ์ฃผ๋กœ ASP.NET Core๋ฅผ ์‚ฌ์šฉํ•˜๊ฒŒ ๋ผ. ์ด ์นœ๊ตฌ๋Š” ์ •๋ง ๊ฐ•๋ ฅํ•œ ํ”„๋ ˆ์ž„์›Œํฌ์ธ๋ฐ, ์ œ๋Œ€๋กœ ํ™œ์šฉํ•˜๋ ค๋ฉด ์•„ํ‚คํ…์ฒ˜ ์„ค๊ณ„๊ฐ€ ์ค‘์š”ํ•ด. ๊ทธ๋ƒฅ ์ฝ”๋“œ๋งŒ ๋ง‰ ์งœ๋ฉด ๋‚˜์ค‘์— ์ •๋ง ํ›„ํšŒํ•˜๊ฒŒ ๋  ๊ฑฐ์•ผ. ๐Ÿ˜ญ

๐Ÿ›๏ธ ๋ ˆ์ด์–ด๋“œ ์•„ํ‚คํ…์ฒ˜: ๊ธฐ๋ณธ ์ค‘์˜ ๊ธฐ๋ณธ

ํ™•์žฅ ๊ฐ€๋Šฅํ•œ ์„œ๋น„์Šค๋ฅผ ๋งŒ๋“ค๋ ค๋ฉด ๋จผ์ € ๋ ˆ์ด์–ด๋“œ ์•„ํ‚คํ…์ฒ˜(Layered Architecture)๋ฅผ ์ดํ•ดํ•ด์•ผ ํ•ด. ์ด๊ฑด ๋งˆ์น˜ ์ผ€์ดํฌ์ฒ˜๋Ÿผ ์—ฌ๋Ÿฌ ์ธต์œผ๋กœ ๋‚˜๋ˆ ์„œ ์„ค๊ณ„ํ•˜๋Š” ๋ฐฉ์‹์ด์•ผ. ๐Ÿฐ

์ „ํ˜•์ ์ธ 4๊ณ„์ธต ๊ตฌ์กฐ:

1. Presentation Layer (ํ”„๋ ˆ์  ํ…Œ์ด์…˜ ๊ณ„์ธต)
์‚ฌ์šฉ์ž์™€ ์ง์ ‘ ์†Œํ†ตํ•˜๋Š” ๋ถ€๋ถ„์ด์•ผ. API ์ปจํŠธ๋กค๋Ÿฌ, ๋ทฐ, ํ”„๋ก ํŠธ์—”๋“œ ๋“ฑ์ด ์—ฌ๊ธฐ ์†ํ•ด.

2. Business Logic Layer (๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ๊ณ„์ธต)
์‹ค์ œ ๋น„์ฆˆ๋‹ˆ์Šค ๊ทœ์น™๊ณผ ๋กœ์ง์ด ๋“ค์–ด๊ฐ€๋Š” ๊ณณ. ์„œ๋น„์Šค ํด๋ž˜์Šค๋“ค์ด ์—ฌ๊ธฐ ์žˆ์ง€.

3. Data Access Layer (๋ฐ์ดํ„ฐ ์ ‘๊ทผ ๊ณ„์ธต)
๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์™€ ํ†ต์‹ ํ•˜๋Š” ๋ถ€๋ถ„. Repository ํŒจํ„ด์„ ๋งŽ์ด ์จ.

4. Cross-Cutting Concerns (๊ณตํ†ต ๊ด€์‹ฌ์‚ฌ)
๋กœ๊น…, ์ธ์ฆ, ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ๊ฐ™์€ ๋ชจ๋“  ๊ณ„์ธต์—์„œ ํ•„์š”ํ•œ ๊ธฐ๋Šฅ๋“ค.

์ž, ์ด์ œ ์‹ค์ œ C# ์ฝ”๋“œ๋กœ ์–ด๋–ป๊ฒŒ ๊ตฌํ˜„ํ•˜๋Š”์ง€ ๋ณผ๊นŒ? ๐Ÿ”

// Presentation Layer - API Controller
[ApiController]
[Route("api/[controller]")]
public class ProductController : ControllerBase
{
    private readonly IProductService _productService;
    private readonly ILogger<ProductController> _logger;

    public ProductController(
        IProductService productService, 
        ILogger<ProductController> logger)
    {
        _productService = productService;
        _logger = logger;
    }

    [HttpGet("{id}")]
    public async Task<IActionResult> GetProduct(int id)
    {
        try
        {
            var product = await _productService.GetProductByIdAsync(id);
            if (product == null)
                return NotFound();
            
            return Ok(product);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "์ƒํ’ˆ ์กฐํšŒ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {ProductId}", id);
            return StatusCode(500, "์„œ๋ฒ„ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.");
        }
    }
}
// Business Logic Layer - Service
public class ProductService : IProductService
{
    private readonly IProductRepository _repository;
    private readonly IMemoryCache _cache;
    private readonly ILogger<ProductService> _logger;

    public ProductService(
        IProductRepository repository,
        IMemoryCache cache,
        ILogger<ProductService> logger)
    {
        _repository = repository;
        _cache = cache;
        _logger = logger;
    }

    public async Task<ProductDto> GetProductByIdAsync(int id)
    {
        // ์บ์‹œ ํ™•์ธ
        var cacheKey = $"product_{id}";
        if (_cache.TryGetValue(cacheKey, out ProductDto cachedProduct))
        {
            _logger.LogInformation("์บ์‹œ์—์„œ ์ƒํ’ˆ ์กฐํšŒ: {ProductId}", id);
            return cachedProduct;
        }

        // DB์—์„œ ์กฐํšŒ
        var product = await _repository.GetByIdAsync(id);
        if (product == null)
            return null;

        var productDto = MapToDto(product);

        // ์บ์‹œ์— ์ €์žฅ (10๋ถ„)
        _cache.Set(cacheKey, productDto, TimeSpan.FromMinutes(10));

        return productDto;
    }

    private ProductDto MapToDto(Product product)
    {
        return new ProductDto
        {
            Id = product.Id,
            Name = product.Name,
            Price = product.Price,
            Stock = product.Stock
        };
    }
}
// Data Access Layer - Repository
public class ProductRepository : IProductRepository
{
    private readonly ApplicationDbContext _context;

    public ProductRepository(ApplicationDbContext context)
    {
        _context = context;
    }

    public async Task<Product> GetByIdAsync(int id)
    {
        return await _context.Products
            .AsNoTracking()
            .FirstOrDefaultAsync(p => p.Id == id);
    }

    public async Task<IEnumerable<Product>> GetAllAsync()
    {
        return await _context.Products
            .AsNoTracking()
            .ToListAsync();
    }

    public async Task<Product> AddAsync(Product product)
    {
        _context.Products.Add(product);
        await _context.SaveChangesAsync();
        return product;
    }
}

๋ณด๋‹ค์‹œํ”ผ ๊ฐ ๊ณ„์ธต์ด ๋ช…ํ™•ํ•˜๊ฒŒ ๋ถ„๋ฆฌ๋˜์–ด ์žˆ์ง€? ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ๋‚˜์ค‘์— ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ๋ฐ”๊พธ๊ฑฐ๋‚˜, ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ์ˆ˜์ •ํ•˜๊ฑฐ๋‚˜, API ํ˜•์‹์„ ๋ณ€๊ฒฝํ•  ๋•Œ ๋‹ค๋ฅธ ๋ถ€๋ถ„์— ์˜ํ–ฅ์„ ์ตœ์†Œํ™”ํ•  ์ˆ˜ ์žˆ์–ด. ๐Ÿ‘

๐Ÿ”Œ ์˜์กด์„ฑ ์ฃผ์ž…(DI): ํ™•์žฅ์„ฑ์˜ ํ•ต์‹ฌ

C#์˜ ASP.NET Core๋Š” ์˜์กด์„ฑ ์ฃผ์ž…(Dependency Injection)์„ ๊ธฐ๋ณธ์œผ๋กœ ์ง€์›ํ•ด. ์ด๊ฒŒ ์ง„์งœ ๊ฒŒ์ž„ ์ฒด์ธ์ €์•ผ! ๐ŸŽฎ

์˜์กด์„ฑ ์ฃผ์ž…์ด ๋ญ๋ƒ๊ณ ? ๊ฐ„๋‹จํžˆ ๋งํ•˜๋ฉด, ํด๋ž˜์Šค๊ฐ€ ํ•„์š”ํ•œ ๊ฐ์ฒด๋ฅผ ์ง์ ‘ ์ƒ์„ฑํ•˜์ง€ ์•Š๊ณ  ์™ธ๋ถ€์—์„œ ์ฃผ์ž…๋ฐ›๋Š” ๊ฑฐ์•ผ. ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ์ฝ”๋“œ์˜ ๊ฒฐํ•ฉ๋„๊ฐ€ ๋‚ฎ์•„์ง€๊ณ , ํ…Œ์ŠคํŠธํ•˜๊ธฐ๋„ ์‰ฌ์›Œ์ง€๊ณ , ํ™•์žฅํ•˜๊ธฐ๋„ ํŽธํ•ด์ ธ.

// Program.cs ๋˜๋Š” Startup.cs์—์„œ ์„œ๋น„์Šค ๋“ฑ๋ก
public class Program
{
    public static void Main(string[] args)
    {
        var builder = WebApplication.CreateBuilder(args);

        // ์„œ๋น„์Šค ๋“ฑ๋ก
        builder.Services.AddControllers();
        
        // ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ปจํ…์ŠคํŠธ ๋“ฑ๋ก
        builder.Services.AddDbContext<ApplicationDbContext>(options =>
            options.UseSqlServer(
                builder.Configuration.GetConnectionString("DefaultConnection")));

        // ๋ ˆํฌ์ง€ํ† ๋ฆฌ ๋“ฑ๋ก
        builder.Services.AddScoped<IProductRepository, ProductRepository>();
        builder.Services.AddScoped<IOrderRepository, OrderRepository>();

        // ์„œ๋น„์Šค ๋“ฑ๋ก
        builder.Services.AddScoped<IProductService, ProductService>();
        builder.Services.AddScoped<IOrderService, OrderService>();

        // ์บ์‹ฑ
        builder.Services.AddMemoryCache();
        builder.Services.AddDistributedMemoryCache();

        // Redis ์บ์‹œ (ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ)
        // builder.Services.AddStackExchangeRedisCache(options =>
        // {
        //     options.Configuration = builder.Configuration
        //         .GetConnectionString("Redis");
        // });

        // ๋กœ๊น…
        builder.Services.AddLogging(logging =>
        {
            logging.AddConsole();
            logging.AddDebug();
        });

        var app = builder.Build();

        app.UseHttpsRedirection();
        app.UseAuthorization();
        app.MapControllers();

        app.Run();
    }
}
๐Ÿ’ก DI ๋ผ์ดํ”„ํƒ€์ž„ ์ดํ•ดํ•˜๊ธฐ: Transient: ์š”์ฒญํ•  ๋•Œ๋งˆ๋‹ค ์ƒˆ๋กœ์šด ์ธ์Šคํ„ด์Šค ์ƒ์„ฑ. ๊ฐ€๋ฒผ์šด stateless ์„œ๋น„์Šค์— ์ ํ•ฉ.
Scoped: HTTP ์š”์ฒญ๋‹น ํ•˜๋‚˜์˜ ์ธ์Šคํ„ด์Šค. ๋Œ€๋ถ€๋ถ„์˜ ์„œ๋น„์Šค์™€ ๋ ˆํฌ์ง€ํ† ๋ฆฌ์— ์‚ฌ์šฉ.
Singleton: ์•ฑ ์ „์ฒด์—์„œ ํ•˜๋‚˜์˜ ์ธ์Šคํ„ด์Šค๋งŒ ์‚ฌ์šฉ. ์„ค์ •, ์บ์‹œ ๋“ฑ์— ์ ํ•ฉ.

์ด๋ ‡๊ฒŒ DI๋ฅผ ํ™œ์šฉํ•˜๋ฉด ๋‚˜์ค‘์— ๊ตฌํ˜„์ฒด๋ฅผ ๋ฐ”๊พธ๊ธฐ๋„ ์‰ฌ์›Œ. ์˜ˆ๋ฅผ ๋“ค์–ด, ๋ฉ”๋ชจ๋ฆฌ ์บ์‹œ์—์„œ Redis๋กœ ๋ฐ”๊พธ๊ณ  ์‹ถ๋‹ค๋ฉด? ๊ทธ๋ƒฅ ๋“ฑ๋ก๋งŒ ๋ฐ”๊พธ๋ฉด ๋ผ! ์ฝ”๋“œ๋Š” ํ•˜๋‚˜๋„ ์•ˆ ๊ฑด๋“œ๋ ค๋„ ๋˜์ง€. ๐Ÿ˜Ž

๐ŸŽจ CQRS ํŒจํ„ด: ์ฝ๊ธฐ์™€ ์“ฐ๊ธฐ์˜ ๋ถ„๋ฆฌ

์„œ๋น„์Šค๊ฐ€ ์ปค์ง€๋ฉด์„œ ๊ฐ€์žฅ ๋จผ์ € ๋А๋ผ๋Š” ๋ณ‘๋ชฉํ˜„์ƒ์ด ๋ญ˜๊นŒ? ๋ฐ”๋กœ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์•ผ! ํŠนํžˆ ์ฝ๊ธฐ ์ž‘์—…์ด ์“ฐ๊ธฐ ์ž‘์—…๋ณด๋‹ค ํ›จ์”ฌ ๋งŽ์€ ๊ฒฝ์šฐ๊ฐ€ ๋Œ€๋ถ€๋ถ„์ด์ง€. ์ด๋Ÿด ๋•Œ CQRS(Command Query Responsibility Segregation) ํŒจํ„ด์ด ๋น›์„ ๋ฐœํ•ด. โœจ

CQRS์˜ ํ•ต์‹ฌ ์•„์ด๋””์–ด:

Command: ๋ฐ์ดํ„ฐ๋ฅผ ๋ณ€๊ฒฝํ•˜๋Š” ์ž‘์—… (Create, Update, Delete)
Query: ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒํ•˜๋Š” ์ž‘์—… (Read)

์ด ๋‘˜์„ ์™„์ „ํžˆ ๋ถ„๋ฆฌํ•ด์„œ ๊ฐ๊ฐ ์ตœ์ ํ™”ํ•˜๋Š” ๊ฑฐ์•ผ!

// Command ๋ชจ๋ธ
public class CreateProductCommand
{
    public string Name { get; set; }
    public decimal Price { get; set; }
    public int Stock { get; set; }
}

public class CreateProductCommandHandler
{
    private readonly ApplicationDbContext _context;
    private readonly IEventPublisher _eventPublisher;

    public CreateProductCommandHandler(
        ApplicationDbContext context,
        IEventPublisher eventPublisher)
    {
        _context = context;
        _eventPublisher = eventPublisher;
    }

    public async Task<int> HandleAsync(CreateProductCommand command)
    {
        var product = new Product
        {
            Name = command.Name,
            Price = command.Price,
            Stock = command.Stock,
            CreatedAt = DateTime.UtcNow
        };

        _context.Products.Add(product);
        await _context.SaveChangesAsync();

        // ์ด๋ฒคํŠธ ๋ฐœํ–‰ (๋‹ค๋ฅธ ์„œ๋น„์Šค์— ์•Œ๋ฆผ)
        await _eventPublisher.PublishAsync(new ProductCreatedEvent
        {
            ProductId = product.Id,
            Name = product.Name
        });

        return product.Id;
    }
}
// Query ๋ชจ๋ธ
public class GetProductQuery
{
    public int Id { get; set; }
}

public class GetProductQueryHandler
{
    private readonly IReadOnlyDbContext _readContext;
    private readonly IMemoryCache _cache;

    public GetProductQueryHandler(
        IReadOnlyDbContext readContext,
        IMemoryCache cache)
    {
        _readContext = readContext;
        _cache = cache;
    }

    public async Task<ProductReadModel> HandleAsync(GetProductQuery query)
    {
        var cacheKey = $"product_read_{query.Id}";
        
        if (_cache.TryGetValue(cacheKey, out ProductReadModel cached))
            return cached;

        // ์ฝ๊ธฐ ์ „์šฉ DB์—์„œ ์กฐํšŒ (๋ณต์ œ๋ณธ ๋˜๋Š” ์ฝ๊ธฐ ์ตœ์ ํ™”๋œ DB)
        var product = await _readContext.Products
            .AsNoTracking()
            .Where(p => p.Id == query.Id)
            .Select(p => new ProductReadModel
            {
                Id = p.Id,
                Name = p.Name,
                Price = p.Price,
                Stock = p.Stock,
                CategoryName = p.Category.Name,
                ReviewCount = p.Reviews.Count,
                AverageRating = p.Reviews.Average(r => r.Rating)
            })
            .FirstOrDefaultAsync();

        if (product != null)
        {
            _cache.Set(cacheKey, product, TimeSpan.FromMinutes(15));
        }

        return product;
    }
}

// ์ฝ๊ธฐ ์ตœ์ ํ™”๋œ ๋ชจ๋ธ
public class ProductReadModel
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
    public int Stock { get; set; }
    public string CategoryName { get; set; }
    public int ReviewCount { get; set; }
    public double AverageRating { get; set; }
}

CQRS๋ฅผ ์“ฐ๋ฉด ์ฝ๊ธฐ ์ž‘์—…์„ ๋ณ„๋„์˜ ์ฝ๊ธฐ ์ „์šฉ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋‚˜ ์บ์‹œ์—์„œ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์–ด. ์“ฐ๊ธฐ ์ž‘์—…์€ ๋ฉ”์ธ DB์—์„œ ์ฒ˜๋ฆฌํ•˜๊ณ , ์ฝ๊ธฐ ์ž‘์—…์€ ๋ณต์ œ๋œ DB๋‚˜ ์บ์‹œ์—์„œ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฑฐ์ง€. ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ์„ฑ๋Šฅ์ด ์—„์ฒญ๋‚˜๊ฒŒ ํ–ฅ์ƒ๋ผ! ๐Ÿš€

๐Ÿ”„ ์ด๋ฒคํŠธ ๊ธฐ๋ฐ˜ ์•„ํ‚คํ…์ฒ˜: ๋А์Šจํ•œ ๊ฒฐํ•ฉ์˜ ๋ฏธํ•™

๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ์•„ํ‚คํ…์ฒ˜๋กœ ๊ฐ€๋ฉด ์„œ๋น„์Šค ๊ฐ„ ํ†ต์‹ ์ด ์ค‘์š”ํ•ด์ ธ. ์ด๋•Œ ์ด๋ฒคํŠธ ๊ธฐ๋ฐ˜ ์•„ํ‚คํ…์ฒ˜(Event-Driven Architecture)๊ฐ€ ์ •๋ง ์œ ์šฉํ•ด. ๐ŸŽช

์˜ˆ๋ฅผ ๋“ค์–ด๋ณผ๊ฒŒ. ์‚ฌ์šฉ์ž๊ฐ€ ์ฃผ๋ฌธ์„ ํ•˜๋ฉด ์–ด๋–ค ์ผ๋“ค์ด ์ผ์–ด๋‚˜์•ผ ํ• ๊นŒ?

1๏ธโƒฃ ์ฃผ๋ฌธ ์ •๋ณด ์ €์žฅ
2๏ธโƒฃ ์žฌ๊ณ  ๊ฐ์†Œ
3๏ธโƒฃ ๊ฒฐ์ œ ์ฒ˜๋ฆฌ
4๏ธโƒฃ ์ด๋ฉ”์ผ ๋ฐœ์†ก
5๏ธโƒฃ ํฌ์ธํŠธ ์ ๋ฆฝ
6๏ธโƒฃ ๋ฐฐ์†ก ์ค€๋น„

์ด๊ฑธ ๋™๊ธฐ์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•˜๋ฉด? ํ•˜๋‚˜๋ผ๋„ ๋А๋ฆฌ๊ฑฐ๋‚˜ ์‹คํŒจํ•˜๋ฉด ์ „์ฒด๊ฐ€ ๋А๋ ค์ง€๊ฑฐ๋‚˜ ์‹คํŒจํ•ด. ํ•˜์ง€๋งŒ ์ด๋ฒคํŠธ ๊ธฐ๋ฐ˜์œผ๋กœ ์ฒ˜๋ฆฌํ•˜๋ฉด ๊ฐ ์ž‘์—…์ด ๋…๋ฆฝ์ ์œผ๋กœ ์‹คํ–‰๋ผ! ๐Ÿ’ช

// ์ด๋ฒคํŠธ ์ •์˜
public class OrderCreatedEvent
{
    public int OrderId { get; set; }
    public int UserId { get; set; }
    public List<OrderItem> Items { get; set; }
    public decimal TotalAmount { get; set; }
    public DateTime CreatedAt { get; set; }
}

// ์ฃผ๋ฌธ ์„œ๋น„์Šค
public class OrderService
{
    private readonly IOrderRepository _orderRepository;
    private readonly IEventBus _eventBus;

    public async Task<int> CreateOrderAsync(CreateOrderDto dto)
    {
        // ์ฃผ๋ฌธ ์ƒ์„ฑ
        var order = new Order
        {
            UserId = dto.UserId,
            Items = dto.Items,
            TotalAmount = dto.Items.Sum(i => i.Price * i.Quantity),
            Status = OrderStatus.Pending,
            CreatedAt = DateTime.UtcNow
        };

        await _orderRepository.AddAsync(order);

        // ์ด๋ฒคํŠธ ๋ฐœํ–‰ (๋น„๋™๊ธฐ)
        await _eventBus.PublishAsync(new OrderCreatedEvent
        {
            OrderId = order.Id,
            UserId = order.UserId,
            Items = order.Items,
            TotalAmount = order.TotalAmount,
            CreatedAt = order.CreatedAt
        });

        return order.Id;
    }
}
// ์žฌ๊ณ  ์„œ๋น„์Šค - ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ
public class InventoryEventHandler : IEventHandler<OrderCreatedEvent>
{
    private readonly IInventoryService _inventoryService;
    private readonly ILogger<InventoryEventHandler> _logger;

    public async Task HandleAsync(OrderCreatedEvent @event)
    {
        try
        {
            foreach (var item in @event.Items)
            {
                await _inventoryService.DecreaseStockAsync(
                    item.ProductId, 
                    item.Quantity);
            }
            
            _logger.LogInformation(
                "์žฌ๊ณ  ๊ฐ์†Œ ์™„๋ฃŒ: OrderId={OrderId}", 
                @event.OrderId);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, 
                "์žฌ๊ณ  ๊ฐ์†Œ ์‹คํŒจ: OrderId={OrderId}", 
                @event.OrderId);
            
            // ๋ณด์ƒ ํŠธ๋žœ์žญ์…˜ ๋˜๋Š” ์žฌ์‹œ๋„ ๋กœ์ง
            throw;
        }
    }
}

// ์ด๋ฉ”์ผ ์„œ๋น„์Šค - ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ
public class EmailEventHandler : IEventHandler<OrderCreatedEvent>
{
    private readonly IEmailService _emailService;
    private readonly IUserRepository _userRepository;

    public async Task HandleAsync(OrderCreatedEvent @event)
    {
        var user = await _userRepository.GetByIdAsync(@event.UserId);
        
        await _emailService.SendOrderConfirmationAsync(
            user.Email,
            @event.OrderId,
            @event.Items,
            @event.TotalAmount);
    }
}

์ด๋ฒคํŠธ ๋ฒ„์Šค๋กœ๋Š” RabbitMQ, Azure Service Bus, Apache Kafka ๊ฐ™์€ ๊ฑธ ์“ธ ์ˆ˜ ์žˆ์–ด. C#์—์„œ๋Š” MassTransit์ด๋‚˜ NServiceBus ๊ฐ™์€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ์ด๋Ÿฐ ๊ฑธ ์‰ฝ๊ฒŒ ๊ตฌํ˜„ํ•˜๊ฒŒ ํ•ด์ค˜. ๐Ÿ˜Š

// MassTransit์„ ์‚ฌ์šฉํ•œ ์ด๋ฒคํŠธ ๋ฒ„์Šค ์„ค์ •
public class Program
{
    public static void Main(string[] args)
    {
        var builder = WebApplication.CreateBuilder(args);

        builder.Services.AddMassTransit(x =>
        {
            // ์ปจ์Šˆ๋จธ ๋“ฑ๋ก
            x.AddConsumer<InventoryEventHandler>();
            x.AddConsumer<EmailEventHandler>();

            // RabbitMQ ์„ค์ •
            x.UsingRabbitMq((context, cfg) =>
            {
                cfg.Host("rabbitmq://localhost", h =>
                {
                    h.Username("guest");
                    h.Password("guest");
                });

                cfg.ConfigureEndpoints(context);
            });
        });

        var app = builder.Build();
        app.Run();
    }
}

๐Ÿ’พ ์บ์‹ฑ ์ „๋žต: ์„ฑ๋Šฅ์˜ ๋น„๋ฐ€ ๋ฌด๊ธฐ

ํ™•์žฅ ๊ฐ€๋Šฅํ•œ ์•„ํ‚คํ…์ฒ˜์—์„œ ์บ์‹ฑ์€ ์ •๋ง ์ค‘์š”ํ•ด! ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋ถ€ํ•˜๋ฅผ ์ค„์ด๊ณ  ์‘๋‹ต ์†๋„๋ฅผ ๊ทน์ ์œผ๋กœ ๊ฐœ์„ ํ•  ์ˆ˜ ์žˆ๊ฑฐ๋“ . ๐Ÿƒโ€โ™‚๏ธ๐Ÿ’จ

์บ์‹ฑ ๋ ˆ๋ฒจ:

1. ๋ฉ”๋ชจ๋ฆฌ ์บ์‹œ: ๋‹จ์ผ ์„œ๋ฒ„ ๋‚ด์—์„œ ๋น ๋ฅธ ์ ‘๊ทผ. IMemoryCache ์‚ฌ์šฉ.
2. ๋ถ„์‚ฐ ์บ์‹œ: ์—ฌ๋Ÿฌ ์„œ๋ฒ„๊ฐ€ ๊ณต์œ . Redis, Memcached ์‚ฌ์šฉ.
3. CDN ์บ์‹œ: ์ •์  ๋ฆฌ์†Œ์Šค๋ฅผ ์ „ ์„ธ๊ณ„์— ๋ถ„์‚ฐ. CloudFlare, Azure CDN ๋“ฑ.

// Redis ๋ถ„์‚ฐ ์บ์‹œ ๊ตฌํ˜„
public class RedisCacheService : ICacheService
{
    private readonly IDistributedCache _cache;
    private readonly ILogger<RedisCacheService> _logger;

    public RedisCacheService(
        IDistributedCache cache,
        ILogger<RedisCacheService> logger)
    {
        _cache = cache;
        _logger = logger;
    }

    public async Task<T> GetOrCreateAsync<T>(
        string key,
        Func<Task<T>> factory,
        TimeSpan? expiration = null)
    {
        // ์บ์‹œ์—์„œ ์กฐํšŒ
        var cachedData = await _cache.GetStringAsync(key);
        
        if (!string.IsNullOrEmpty(cachedData))
        {
            _logger.LogDebug("์บ์‹œ ํžˆํŠธ: {Key}", key);
            return JsonSerializer.Deserialize<T>(cachedData);
        }

        // ์บ์‹œ ๋ฏธ์Šค - ๋ฐ์ดํ„ฐ ์ƒ์„ฑ
        _logger.LogDebug("์บ์‹œ ๋ฏธ์Šค: {Key}", key);
        var data = await factory();

        if (data != null)
        {
            var serialized = JsonSerializer.Serialize(data);
            var options = new DistributedCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow = 
                    expiration ?? TimeSpan.FromMinutes(10)
            };

            await _cache.SetStringAsync(key, serialized, options);
        }

        return data;
    }

    public async Task RemoveAsync(string key)
    {
        await _cache.RemoveAsync(key);
        _logger.LogDebug("์บ์‹œ ์‚ญ์ œ: {Key}", key);
    }

    public async Task RemoveByPrefixAsync(string prefix)
    {
        // Redis์˜ SCAN ๋ช…๋ น์„ ์‚ฌ์šฉํ•˜์—ฌ ํŒจํ„ด ๋งค์นญ
        // ์‹ค์ œ ๊ตฌํ˜„์€ StackExchange.Redis๋ฅผ ์ง์ ‘ ์‚ฌ์šฉ
        _logger.LogDebug("์บ์‹œ ํŒจํ„ด ์‚ญ์ œ: {Prefix}*", prefix);
    }
}
// ์บ์‹œ ํ™œ์šฉ ์˜ˆ์ œ
public class ProductService
{
    private readonly IProductRepository _repository;
    private readonly ICacheService _cache;

    public async Task<ProductDto> GetProductAsync(int id)
    {
        return await _cache.GetOrCreateAsync(
            $"product:{id}",
            async () =>
            {
                var product = await _repository.GetByIdAsync(id);
                return product != null ? MapToDto(product) : null;
            },
            TimeSpan.FromMinutes(30)
        );
    }

    public async Task<List<ProductDto>> GetPopularProductsAsync()
    {
        return await _cache.GetOrCreateAsync(
            "products:popular",
            async () =>
            {
                var products = await _repository
                    .GetPopularProductsAsync(limit: 20);
                return products.Select(MapToDto).ToList();
            },
            TimeSpan.FromMinutes(5) // ์ธ๊ธฐ ์ƒํ’ˆ์€ ์ž์ฃผ ๊ฐฑ์‹ 
        );
    }

    public async Task UpdateProductAsync(int id, UpdateProductDto dto)
    {
        await _repository.UpdateAsync(id, dto);
        
        // ์บ์‹œ ๋ฌดํšจํ™”
        await _cache.RemoveAsync($"product:{id}");
        await _cache.RemoveAsync("products:popular");
    }
}
๐ŸŽฏ ์บ์‹ฑ ์ „๋žต ํŒ: Cache-Aside: ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ์บ์‹œ๋ฅผ ์ง์ ‘ ๊ด€๋ฆฌ. ๊ฐ€์žฅ ์ผ๋ฐ˜์ .
Write-Through: ๋ฐ์ดํ„ฐ ์“ธ ๋•Œ ์บ์‹œ๋„ ํ•จ๊ป˜ ์—…๋ฐ์ดํŠธ.
Write-Behind: ์บ์‹œ์— ๋จผ์ € ์“ฐ๊ณ  ๋‚˜์ค‘์— DB์— ๋น„๋™๊ธฐ๋กœ ์ €์žฅ.
Refresh-Ahead: ๋งŒ๋ฃŒ ์ „์— ๋ฏธ๋ฆฌ ๊ฐฑ์‹ .

๐Ÿ” API Gateway์™€ ์ธ์ฆ/์ธ๊ฐ€

๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ์•„ํ‚คํ…์ฒ˜์—์„œ๋Š” API Gateway๊ฐ€ ํ•„์ˆ˜์•ผ. ๋ชจ๋“  ํด๋ผ์ด์–ธํŠธ ์š”์ฒญ์˜ ์ง„์ž…์  ์—ญํ• ์„ ํ•˜๋ฉด์„œ ์ธ์ฆ, ๋ผ์šฐํŒ…, ๋กœ๋“œ ๋ฐธ๋Ÿฐ์‹ฑ, ์†๋„ ์ œํ•œ ๋“ฑ์„ ์ฒ˜๋ฆฌํ•ด. ๐Ÿšช

C#์—์„œ๋Š” Ocelot์ด๋‚˜ YARP(Yet Another Reverse Proxy) ๊ฐ™์€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์“ธ ์ˆ˜ ์žˆ์–ด. ์ตœ๊ทผ์—๋Š” YARP๊ฐ€ ๋งˆ์ดํฌ๋กœ์†Œํ”„ํŠธ์—์„œ ๊ณต์‹ ์ง€์›ํ•˜๋ฉด์„œ ์ธ๊ธฐ๊ฐ€ ๋งŽ์•„์กŒ์ง€.

// YARP๋ฅผ ์‚ฌ์šฉํ•œ API Gateway ์„ค์ •
// appsettings.json
{
  "ReverseProxy": {
    "Routes": {
      "product-route": {
        "ClusterId": "product-cluster",
        "Match": {
          "Path": "/api/products/{**catch-all}"
        },
        "Transforms": [
          { "PathPattern": "/api/products/{**catch-all}" }
        ]
      },
      "order-route": {
        "ClusterId": "order-cluster",
        "Match": {
          "Path": "/api/orders/{**catch-all}"
        }
      },
      "user-route": {
        "ClusterId": "user-cluster",
        "Match": {
          "Path": "/api/users/{**catch-all}"
        }
      }
    },
    "Clusters": {
      "product-cluster": {
        "Destinations": {
          "destination1": {
            "Address": "https://product-service:5001"
          },
          "destination2": {
            "Address": "https://product-service:5002"
          }
        },
        "LoadBalancingPolicy": "RoundRobin"
      },
      "order-cluster": {
        "Destinations": {
          "destination1": {
            "Address": "https://order-service:5003"
          }
        }
      },
      "user-cluster": {
        "Destinations": {
          "destination1": {
            "Address": "https://user-service:5004"
          }
        }
      }
    }
  }
}
// JWT ๊ธฐ๋ฐ˜ ์ธ์ฆ ๊ตฌํ˜„
public class Program
{
    public static void Main(string[] args)
    {
        var builder = WebApplication.CreateBuilder(args);

        // JWT ์ธ์ฆ ์„ค์ •
        builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
            .AddJwtBearer(options =>
            {
                options.TokenValidationParameters = new TokenValidationParameters
                {
                    ValidateIssuer = true,
                    ValidateAudience = true,
                    ValidateLifetime = true,
                    ValidateIssuerSigningKey = true,
                    ValidIssuer = builder.Configuration["Jwt:Issuer"],
                    ValidAudience = builder.Configuration["Jwt:Audience"],
                    IssuerSigningKey = new SymmetricSecurityKey(
                        Encoding.UTF8.GetBytes(
                            builder.Configuration["Jwt:Key"]))
                };
            });

        // ๊ถŒํ•œ ์ •์ฑ… ์„ค์ •
        builder.Services.AddAuthorization(options =>
        {
            options.AddPolicy("AdminOnly", policy =>
                policy.RequireRole("Admin"));
            
            options.AddPolicy("UserOrAdmin", policy =>
                policy.RequireRole("User", "Admin"));
        });

        // YARP ์ถ”๊ฐ€
        builder.Services.AddReverseProxy()
            .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));

        var app = builder.Build();

        app.UseAuthentication();
        app.UseAuthorization();
        
        app.MapReverseProxy();

        app.Run();
    }
}
// JWT ํ† ํฐ ์ƒ์„ฑ ์„œ๋น„์Šค
public class TokenService : ITokenService
{
    private readonly IConfiguration _configuration;

    public TokenService(IConfiguration configuration)
    {
        _configuration = configuration;
    }

    public string GenerateToken(User user)
    {
        var securityKey = new SymmetricSecurityKey(
            Encoding.UTF8.GetBytes(_configuration["Jwt:Key"]));
        
        var credentials = new SigningCredentials(
            securityKey, 
            SecurityAlgorithms.HmacSha256);

        var claims = new[]
        {
            new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
            new Claim(ClaimTypes.Name, user.Username),
            new Claim(ClaimTypes.Email, user.Email),
            new Claim(ClaimTypes.Role, user.Role),
            new Claim("premium", user.IsPremium.ToString())
        };

        var token = new JwtSecurityToken(
            issuer: _configuration["Jwt:Issuer"],
            audience: _configuration["Jwt:Audience"],
            claims: claims,
            expires: DateTime.Now.AddHours(24),
            signingCredentials: credentials);

        return new JwtSecurityTokenHandler().WriteToken(token);
    }

    public ClaimsPrincipal ValidateToken(string token)
    {
        var tokenHandler = new JwtSecurityTokenHandler();
        var key = Encoding.UTF8.GetBytes(_configuration["Jwt:Key"]);

        try
        {
            var principal = tokenHandler.ValidateToken(token, 
                new TokenValidationParameters
                {
                    ValidateIssuerSigningKey = true,
                    IssuerSigningKey = new SymmetricSecurityKey(key),
                    ValidateIssuer = true,
                    ValidIssuer = _configuration["Jwt:Issuer"],
                    ValidateAudience = true,
                    ValidAudience = _configuration["Jwt:Audience"],
                    ValidateLifetime = true,
                    ClockSkew = TimeSpan.Zero
                }, 
                out SecurityToken validatedToken);

            return principal;
        }
        catch
        {
            return null;
        }
    }
}

๐Ÿ“Š ๋ชจ๋‹ˆํ„ฐ๋ง๊ณผ ๋กœ๊น…: ๋ณด์ด์ง€ ์•Š์œผ๋ฉด ๊ด€๋ฆฌํ•  ์ˆ˜ ์—†๋‹ค

ํ™•์žฅ ๊ฐ€๋Šฅํ•œ ์‹œ์Šคํ…œ์„ ์šด์˜ํ•˜๋ ค๋ฉด ๋ชจ๋‹ˆํ„ฐ๋ง๊ณผ ๋กœ๊น…์ด ํ•„์ˆ˜์•ผ. ๋ฌธ์ œ๊ฐ€ ์ƒ๊ฒผ์„ ๋•Œ ๋น ๋ฅด๊ฒŒ ํŒŒ์•…ํ•˜๊ณ  ๋Œ€์‘ํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•˜๊ฑฐ๋“ ! ๐Ÿ”

๋ชจ๋‹ˆํ„ฐ๋งํ•ด์•ผ ํ•  ์ฃผ์š” ์ง€ํ‘œ:

์‘๋‹ต ์‹œ๊ฐ„(Response Time): API ์‘๋‹ต์ด ์–ผ๋งˆ๋‚˜ ๋น ๋ฅธ๊ฐ€?
์ฒ˜๋ฆฌ๋Ÿ‰(Throughput): ์ดˆ๋‹น ๋ช‡ ๊ฐœ์˜ ์š”์ฒญ์„ ์ฒ˜๋ฆฌํ•˜๋Š”๊ฐ€?
์—๋Ÿฌ์œจ(Error Rate): ์‹คํŒจํ•˜๋Š” ์š”์ฒญ์ด ์–ผ๋งˆ๋‚˜ ๋˜๋Š”๊ฐ€?
๋ฆฌ์†Œ์Šค ์‚ฌ์šฉ๋ฅ : CPU, ๋ฉ”๋ชจ๋ฆฌ, ๋””์Šคํฌ, ๋„คํŠธ์›Œํฌ ์‚ฌ์šฉ๋Ÿ‰
๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ฑ๋Šฅ: ์ฟผ๋ฆฌ ์‹คํ–‰ ์‹œ๊ฐ„, ์ปค๋„ฅ์…˜ ํ’€ ์ƒํƒœ

// Serilog๋ฅผ ์‚ฌ์šฉํ•œ ๊ตฌ์กฐํ™”๋œ ๋กœ๊น…
public class Program
{
    public static void Main(string[] args)
    {
        // Serilog ์„ค์ •
        Log.Logger = new LoggerConfiguration()
            .MinimumLevel.Information()
            .MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
            .Enrich.FromLogContext()
            .Enrich.WithMachineName()
            .Enrich.WithEnvironmentName()
            .WriteTo.Console(
                outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}")
            .WriteTo.File(
                path: "logs/app-.txt",
                rollingInterval: RollingInterval.Day,
                retainedFileCountLimit: 30)
            .WriteTo.Seq("http://localhost:5341") // Seq ๋กœ๊ทธ ์„œ๋ฒ„
            .CreateLogger();

        try
        {
            Log.Information("์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์‹œ์ž‘");
            CreateHostBuilder(args).Build().Run();
        }
        catch (Exception ex)
        {
            Log.Fatal(ex, "์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์‹œ์ž‘ ์‹คํŒจ");
        }
        finally
        {
            Log.CloseAndFlush();
        }
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .UseSerilog()
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            });
}
// ์„ฑ๋Šฅ ์ธก์ • ๋ฏธ๋“ค์›จ์–ด
public class PerformanceMonitoringMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<PerformanceMonitoringMiddleware> _logger;
    private readonly IMetricsService _metrics;

    public PerformanceMonitoringMiddleware(
        RequestDelegate next,
        ILogger<PerformanceMonitoringMiddleware> logger,
        IMetricsService metrics)
    {
        _next = next;
        _logger = logger;
        _metrics = metrics;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        var sw = Stopwatch.StartNew();
        var path = context.Request.Path;
        var method = context.Request.Method;

        try
        {
            await _next(context);
            sw.Stop();

            var statusCode = context.Response.StatusCode;
            var duration = sw.ElapsedMilliseconds;

            // ๋ฉ”ํŠธ๋ฆญ ๊ธฐ๋ก
            _metrics.RecordRequestDuration(path, method, statusCode, duration);

            // ๋А๋ฆฐ ์š”์ฒญ ๋กœ๊น…
            if (duration > 1000)
            {
                _logger.LogWarning(
                    "๋А๋ฆฐ ์š”์ฒญ ๊ฐ์ง€: {Method} {Path} - {Duration}ms, Status: {StatusCode}",
                    method, path, duration, statusCode);
            }

            // ๊ตฌ์กฐํ™”๋œ ๋กœ๊ทธ
            _logger.LogInformation(
                "HTTP {Method} {Path} responded {StatusCode} in {Duration}ms",
                method, path, statusCode, duration);
        }
        catch (Exception ex)
        {
            sw.Stop();
            
            _logger.LogError(ex,
                "์š”์ฒญ ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜: {Method} {Path} - {Duration}ms",
                method, path, sw.ElapsedMilliseconds);

            _metrics.RecordError(path, method, ex.GetType().Name);
            
            throw;
        }
    }
}
// Application Insights ํ†ตํ•ฉ
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // Application Insights ์ถ”๊ฐ€
        services.AddApplicationInsightsTelemetry(options =>
        {
            options.ConnectionString = Configuration["ApplicationInsights:ConnectionString"];
            options.EnableAdaptiveSampling = true;
            options.EnableQuickPulseMetricStream = true;
        });

        // ์ปค์Šคํ…€ ํ…”๋ ˆ๋ฉ”ํŠธ๋ฆฌ ์ดˆ๊ธฐํ™”
        services.AddSingleton<ITelemetryInitializer, CustomTelemetryInitializer>();

        // ํ—ฌ์Šค ์ฒดํฌ
        services.AddHealthChecks()
            .AddDbContextCheck<ApplicationDbContext>()
            .AddRedis(Configuration["Redis:ConnectionString"])
            .AddUrlGroup(new Uri("https://external-api.com/health"), "External API");
    }

    public void Configure(IApplicationBuilder app)
    {
        app.UseMiddleware<PerformanceMonitoringMiddleware>();
        
        // ํ—ฌ์Šค ์ฒดํฌ ์—”๋“œํฌ์ธํŠธ
        app.UseHealthChecks("/health", new HealthCheckOptions
        {
            ResponseWriter = async (context, report) =>
            {
                context.Response.ContentType = "application/json";
                var result = JsonSerializer.Serialize(new
                {
                    status = report.Status.ToString(),
                    checks = report.Entries.Select(e => new
                    {
                        name = e.Key,
                        status = e.Value.Status.ToString(),
                        duration = e.Value.Duration.TotalMilliseconds,
                        description = e.Value.Description
                    }),
                    totalDuration = report.TotalDuration.TotalMilliseconds
                });
                await context.Response.WriteAsync(result);
            }
        });
    }
}

์‹ค์ œ ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ๋Š” Prometheus + Grafana, ELK Stack(Elasticsearch, Logstash, Kibana), ๋˜๋Š” Azure Application Insights ๊ฐ™์€ ๋„๊ตฌ๋“ค์„ ์กฐํ•ฉํ•ด์„œ ์‚ฌ์šฉํ•ด. ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ์‹ค์‹œ๊ฐ„์œผ๋กœ ์‹œ์Šคํ…œ ์ƒํƒœ๋ฅผ ๋ชจ๋‹ˆํ„ฐ๋งํ•˜๊ณ , ๋ฌธ์ œ๊ฐ€ ์ƒ๊ธฐ๋ฉด ์ฆ‰์‹œ ์•Œ๋ฆผ์„ ๋ฐ›์„ ์ˆ˜ ์žˆ์–ด! ๐Ÿ“ฑ

๐Ÿณ ์ปจํ…Œ์ด๋„ˆํ™”์™€ ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ด์…˜

ํ˜„๋Œ€์ ์ธ ํ™•์žฅ ๊ฐ€๋Šฅํ•œ ์•„ํ‚คํ…์ฒ˜์—์„œ Docker์™€ Kubernetes๋Š” ๊ฑฐ์˜ ํ•„์ˆ˜๊ฐ€ ๋์–ด. ์ปจํ…Œ์ด๋„ˆ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๊ฐœ๋ฐœ ํ™˜๊ฒฝ๊ณผ ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์„ ๋™์ผํ•˜๊ฒŒ ์œ ์ง€ํ•  ์ˆ˜ ์žˆ๊ณ , ๋ฐฐํฌ์™€ ํ™•์žฅ์ด ์—„์ฒญ ์‰ฌ์›Œ์ ธ! ๐Ÿšข

# Dockerfile - ASP.NET Core ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443

FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY ["ProductService/ProductService.csproj", "ProductService/"]
COPY ["Common/Common.csproj", "Common/"]
RUN dotnet restore "ProductService/ProductService.csproj"
COPY . .
WORKDIR "/src/ProductService"
RUN dotnet build "ProductService.csproj" -c Release -o /app/build

FROM build AS publish
RUN dotnet publish "ProductService.csproj" -c Release -o /app/publish

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "ProductService.dll"]
# docker-compose.yml - ๋กœ์ปฌ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ
version: '3.8'

services:
  product-service:
    build:
      context: .
      dockerfile: ProductService/Dockerfile
    ports:
      - "5001:80"
    environment:
      - ASPNETCORE_ENVIRONMENT=Development
      - ConnectionStrings__DefaultConnection=Server=sqlserver;Database=ProductDb;User=sa;Password=YourPassword123;
      - Redis__ConnectionString=redis:6379
    depends_on:
      - sqlserver
      - redis
    networks:
      - app-network

  order-service:
    build:
      context: .
      dockerfile: OrderService/Dockerfile
    ports:
      - "5002:80"
    environment:
      - ASPNETCORE_ENVIRONMENT=Development
      - ConnectionStrings__DefaultConnection=Server=sqlserver;Database=OrderDb;User=sa;Password=YourPassword123;
      - RabbitMQ__Host=rabbitmq
    depends_on:
      - sqlserver
      - rabbitmq
    networks:
      - app-network

  api-gateway:
    build:
      context: .
      dockerfile: ApiGateway/Dockerfile
    ports:
      - "5000:80"
    environment:
      - ASPNETCORE_ENVIRONMENT=Development
    depends_on:
      - product-service
      - order-service
    networks:
      - app-network

  sqlserver:
    image: mcr.microsoft.com/mssql/server:2022-latest
    environment:
      - ACCEPT_EULA=Y
      - SA_PASSWORD=YourPassword123
    ports:
      - "1433:1433"
    volumes:
      - sqlserver-data:/var/opt/mssql
    networks:
      - app-network

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    volumes:
      - redis-data:/data
    networks:
      - app-network

  rabbitmq:
    image: rabbitmq:3-management-alpine
    ports:
      - "5672:5672"
      - "15672:15672"
    environment:
      - RABBITMQ_DEFAULT_USER=guest
      - RABBITMQ_DEFAULT_PASS=guest
    volumes:
      - rabbitmq-data:/var/lib/rabbitmq
    networks:
      - app-network

volumes:
  sqlserver-data:
  redis-data:
  rabbitmq-data:

networks:
  app-network:
    driver: bridge
# Kubernetes Deployment - ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ
apiVersion: apps/v1
kind: Deployment
metadata:
  name: product-service
  labels:
    app: product-service
spec:
  replicas: 3
  selector:
    matchLabels:
      app: product-service
  template:
    metadata:
      labels:
        app: product-service
    spec:
      containers:
      - name: product-service
        image: myregistry.azurecr.io/product-service:latest
        ports:
        - containerPort: 80
        env:
        - name: ASPNETCORE_ENVIRONMENT
          value: "Production"
        - name: ConnectionStrings__DefaultConnection
          valueFrom:
            secretKeyRef:
              name: db-secret
              key: connection-string
        - name: Redis__ConnectionString
          valueFrom:
            configMapKeyRef:
              name: app-config
              key: redis-connection
        resources:
          requests:
            memory: "256Mi"
            cpu: "250m"
          limits:
            memory: "512Mi"
            cpu: "500m"
        livenessProbe:
          httpGet:
            path: /health
            port: 80
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /health/ready
            port: 80
          initialDelaySeconds: 5
          periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
  name: product-service
spec:
  selector:
    app: product-service
  ports:
  - protocol: TCP
    port: 80
    targetPort: 80
  type: ClusterIP
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: product-service-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: product-service
  minReplicas: 3
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70
  - type: Resource
    resource:
      name: memory
      target:
        type: Utilization
        averageUtilization: 80

Kubernetes๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์ž๋™ ์Šค์ผ€์ผ๋ง, ๋กค๋ง ์—…๋ฐ์ดํŠธ, ์ž๊ฐ€ ์น˜์œ  ๋“ฑ์˜ ๊ธฐ๋Šฅ์„ ์‰ฝ๊ฒŒ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์–ด. ํŠธ๋ž˜ํ”ฝ์ด ๋Š˜์–ด๋‚˜๋ฉด ์ž๋™์œผ๋กœ Pod๋ฅผ ๋Š˜๋ฆฌ๊ณ , ๋ฌธ์ œ๊ฐ€ ์ƒ๊ธด Pod๋Š” ์ž๋™์œผ๋กœ ์žฌ์‹œ์ž‘ํ•ด์ค˜. ์ •๋ง ํŽธ๋ฆฌํ•˜์ง€! ๐ŸŽ‰

๐Ÿ”„ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ™•์žฅ ์ „๋žต

๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋Š” ํ™•์žฅ ๊ฐ€๋Šฅํ•œ ์•„ํ‚คํ…์ฒ˜์—์„œ ๊ฐ€์žฅ ๊นŒ๋‹ค๋กœ์šด ๋ถ€๋ถ„์ด์•ผ. ์ž˜๋ชป ์„ค๊ณ„ํ•˜๋ฉด ๋ณ‘๋ชฉํ˜„์ƒ์˜ ์ฃผ๋ฒ”์ด ๋˜๊ฑฐ๋“ . ์—ฌ๋Ÿฌ ๊ฐ€์ง€ ์ „๋žต์„ ์‚ดํŽด๋ณด์ž! ๐Ÿ’พ

๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ™•์žฅ ๋ฐฉ๋ฒ•:

1. ์ฝ๊ธฐ ๋ณต์ œ๋ณธ(Read Replica)
๋งˆ์Šคํ„ฐ DB์—์„œ ์ฝ๊ธฐ ์ „์šฉ ๋ณต์ œ๋ณธ์„ ๋งŒ๋“ค์–ด์„œ ์ฝ๊ธฐ ๋ถ€ํ•˜๋ฅผ ๋ถ„์‚ฐ์‹œ์ผœ.

2. ์ƒค๋”ฉ(Sharding)
๋ฐ์ดํ„ฐ๋ฅผ ์—ฌ๋Ÿฌ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ๋ถ„์‚ฐ ์ €์žฅํ•ด. ์˜ˆ๋ฅผ ๋“ค์–ด ์‚ฌ์šฉ์ž ID ๋ฒ”์œ„๋ณ„๋กœ ๋‚˜๋ˆ„๋Š” ๊ฑฐ์ง€.

3. CQRS + Event Sourcing
์ฝ๊ธฐ์™€ ์“ฐ๊ธฐ๋ฅผ ์™„์ „ํžˆ ๋ถ„๋ฆฌํ•˜๊ณ , ์ด๋ฒคํŠธ ๊ธฐ๋ฐ˜์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ๋™๊ธฐํ™”ํ•ด.

4. NoSQL ํ™œ์šฉ
๊ด€๊ณ„ํ˜• DB๊ฐ€ ํ•„์š” ์—†๋Š” ๋ฐ์ดํ„ฐ๋Š” MongoDB, Cassandra ๊ฐ™์€ NoSQL์„ ์จ.

// ์ฝ๊ธฐ/์“ฐ๊ธฐ ๋ถ„๋ฆฌ ๊ตฌํ˜„
public class DbContextFactory
{
    private readonly string _writeConnectionString;
    private readonly string _readConnectionString;

    public DbContextFactory(IConfiguration configuration)
    {
        _writeConnectionString = configuration
            .GetConnectionString("WriteConnection");
        _readConnectionString = configuration
            .GetConnectionString("ReadConnection");
    }

    public ApplicationDbContext CreateWriteContext()
    {
        var options = new DbContextOptionsBuilder<ApplicationDbContext>()
            .UseSqlServer(_writeConnectionString)
            .Options;

        return new ApplicationDbContext(options);
    }

    public ApplicationDbContext CreateReadContext()
    {
        var options = new DbContextOptionsBuilder<ApplicationDbContext>()
            .UseSqlServer(_readConnectionString)
            .UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking)
            .Options;

        return new ApplicationDbContext(options);
    }
}

// Repository์—์„œ ํ™œ์šฉ
public class ProductRepository : IProductRepository
{
    private readonly DbContextFactory _contextFactory;

    public ProductRepository(DbContextFactory contextFactory)
    {
        _contextFactory = contextFactory;
    }

    // ์ฝ๊ธฐ ์ž‘์—… - ์ฝ๊ธฐ ์ „์šฉ DB ์‚ฌ์šฉ
    public async Task<Product> GetByIdAsync(int id)
    {
        using var context = _contextFactory.CreateReadContext();
        return await context.Products
            .AsNoTracking()
            .FirstOrDefaultAsync(p => p.Id == id);
    }

    public async Task<List<Product>> GetAllAsync()
    {
        using var context = _contextFactory.CreateReadContext();
        return await context.Products
            .AsNoTracking()
            .ToListAsync();
    }

    // ์“ฐ๊ธฐ ์ž‘์—… - ๋งˆ์Šคํ„ฐ DB ์‚ฌ์šฉ
    public async Task<Product> AddAsync(Product product)
    {
        using var context = _contextFactory.CreateWriteContext();
        context.Products.Add(product);
        await context.SaveChangesAsync();
        return product;
    }

    public async Task UpdateAsync(Product product)
    {
        using var context = _contextFactory.CreateWriteContext();
        context.Products.Update(product);
        await context.SaveChangesAsync();
    }
}
// ์ƒค๋”ฉ ๊ตฌํ˜„ ์˜ˆ์ œ
public class ShardedProductRepository : IProductRepository
{
    private readonly List<DbContextFactory> _shards;
    private readonly ILogger<ShardedProductRepository> _logger;

    public ShardedProductRepository(
        IConfiguration configuration,
        ILogger<ShardedProductRepository> logger)
    {
        _logger = logger;
        _shards = new List<DbContextFactory>();

        // ์—ฌ๋Ÿฌ ์ƒค๋“œ ์ดˆ๊ธฐํ™”
        var shardCount = configuration.GetValue<int>("Database:ShardCount");
        for (int i = 0; i < shardCount; i++)
        {
            var connectionString = configuration
                .GetConnectionString($"Shard{i}");
            _shards.Add(new DbContextFactory(connectionString));
        }
    }

    private int GetShardIndex(int productId)
    {
        // ๊ฐ„๋‹จํ•œ ํ•ด์‹œ ๊ธฐ๋ฐ˜ ์ƒค๋”ฉ
        return productId % _shards.Count;
    }

    public async Task<Product> GetByIdAsync(int id)
    {
        var shardIndex = GetShardIndex(id);
        using var context = _shards[shardIndex].CreateReadContext();
        
        _logger.LogDebug("์ƒค๋“œ {ShardIndex}์—์„œ ์ƒํ’ˆ ์กฐํšŒ: {ProductId}", 
            shardIndex, id);

        return await context.Products
            .AsNoTracking()
            .FirstOrDefaultAsync(p => p.Id == id);
    }

    public async Task<List<Product>> GetAllAsync()
    {
        // ๋ชจ๋“  ์ƒค๋“œ์—์„œ ๋ฐ์ดํ„ฐ ์กฐํšŒ
        var tasks = _shards.Select(async (shard, index) =>
        {
            using var context = shard.CreateReadContext();
            _logger.LogDebug("์ƒค๋“œ {ShardIndex}์—์„œ ์ „์ฒด ์ƒํ’ˆ ์กฐํšŒ", index);
            return await context.Products.AsNoTracking().ToListAsync();
        });

        var results = await Task.WhenAll(tasks);
        return results.SelectMany(r => r).ToList();
    }

    public async Task<Product> AddAsync(Product product)
    {
        var shardIndex = GetShardIndex(product.Id);
        using var context = _shards[shardIndex].CreateWriteContext();
        
        _logger.LogInformation("์ƒค๋“œ {ShardIndex}์— ์ƒํ’ˆ ์ถ”๊ฐ€: {ProductId}", 
            shardIndex, product.Id);

        context.Products.Add(product);
        await context.SaveChangesAsync();
        return product;
    }
}
โš ๏ธ ์ƒค๋”ฉ ์ฃผ์˜์‚ฌํ•ญ: ์ƒค๋”ฉ์€ ๊ฐ•๋ ฅํ•˜์ง€๋งŒ ๋ณต์žก๋„๊ฐ€ ๋†’์•„์ ธ. ์กฐ์ธ์ด ์–ด๋ ค์›Œ์ง€๊ณ , ํŠธ๋žœ์žญ์…˜ ๊ด€๋ฆฌ๊ฐ€ ๊นŒ๋‹ค๋กœ์›Œ. ์ •๋ง ํ•„์š”ํ•œ ๊ฒฝ์šฐ์—๋งŒ ์‚ฌ์šฉํ•˜๊ณ , ๊ฐ€๋Šฅํ•˜๋ฉด ์ฝ๊ธฐ ๋ณต์ œ๋ณธ์ด๋‚˜ ์บ์‹ฑ์œผ๋กœ ๋จผ์ € ํ•ด๊ฒฐํ•ด๋ด!

๐Ÿšฆ Rate Limiting๊ณผ Circuit Breaker

ํ™•์žฅ ๊ฐ€๋Šฅํ•œ ์‹œ์Šคํ…œ์„ ์•ˆ์ •์ ์œผ๋กœ ์šด์˜ํ•˜๋ ค๋ฉด Rate Limiting๊ณผ Circuit Breaker ํŒจํ„ด์ด ํ•„์ˆ˜์•ผ. ์ด ๋‘ ๊ฐ€์ง€๋Š” ์‹œ์Šคํ…œ์„ ๊ณผ๋ถ€ํ•˜์™€ ์žฅ์• ๋กœ๋ถ€ํ„ฐ ๋ณดํ˜ธํ•ด์ค˜! ๐Ÿ›ก๏ธ

// Rate Limiting ๊ตฌํ˜„ (AspNetCoreRateLimit ์‚ฌ์šฉ)
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // Rate Limiting ์„ค์ •
        services.AddMemoryCache();
        services.Configure<IpRateLimitOptions>(Configuration.GetSection("IpRateLimiting"));
        services.Configure<IpRateLimitPolicies>(Configuration.GetSection("IpRateLimitPolicies"));
        services.AddInMemoryRateLimiting();
        services.AddSingleton<IRateLimitConfiguration, RateLimitConfiguration>();
    }

    public void Configure(IApplicationBuilder app)
    {
        app.UseIpRateLimiting();
    }
}

// appsettings.json
{
  "IpRateLimiting": {
    "EnableEndpointRateLimiting": true,
    "StackBlockedRequests": false,
    "RealIpHeader": "X-Real-IP",
    "ClientIdHeader": "X-ClientId",
    "HttpStatusCode": 429,
    "GeneralRules": [
      {
        "Endpoint": "*",
        "Period": "1s",
        "Limit": 10
      },
      {
        "Endpoint": "*",
        "Period": "1m",
        "Limit": 100
      },
      {
        "Endpoint": "*/api/products",
        "Period": "1s",
        "Limit": 20
      }
    ]
  }
}
// Circuit Breaker ๊ตฌํ˜„ (Polly ์‚ฌ์šฉ)
public class ExternalApiService
{
    private readonly HttpClient _httpClient;
    private readonly IAsyncPolicy<HttpResponseMessage> _circuitBreakerPolicy;
    private readonly ILogger<ExternalApiService> _logger;

    public ExternalApiService(
        HttpClient httpClient,
        ILogger<ExternalApiService> logger)
    {
        _httpClient = httpClient;
        _logger = logger;

        // Circuit Breaker ์ •์ฑ… ์„ค์ •
        _circuitBreakerPolicy = Policy
            .HandleResult<HttpResponseMessage>(r => !r.IsSuccessStatusCode)
            .Or<HttpRequestException>()
            .CircuitBreakerAsync(
                handledEventsAllowedBeforeBreaking: 3,
                durationOfBreak: TimeSpan.FromSeconds(30),
                onBreak: (result, duration) =>
                {
                    _logger.LogWarning(
                        "Circuit Breaker ์—ด๋ฆผ: {Duration}์ดˆ ๋™์•ˆ ์ฐจ๋‹จ๋จ", 
                        duration.TotalSeconds);
                },
                onReset: () =>
                {
                    _logger.LogInformation("Circuit Breaker ๋ฆฌ์…‹๋จ");
                },
                onHalfOpen: () =>
                {
                    _logger.LogInformation("Circuit Breaker Half-Open ์ƒํƒœ");
                });
    }

    public async Task<ExternalData> GetDataAsync(string id)
    {
        try
        {
            var response = await _circuitBreakerPolicy.ExecuteAsync(async () =>
            {
                _logger.LogDebug("์™ธ๋ถ€ API ํ˜ธ์ถœ: {Id}", id);
                return await _httpClient.GetAsync($"/api/data/{id}");
            });

            if (response.IsSuccessStatusCode)
            {
                var content = await response.Content.ReadAsStringAsync();
                return JsonSerializer.Deserialize<ExternalData>(content);
            }

            _logger.LogWarning(
                "์™ธ๋ถ€ API ํ˜ธ์ถœ ์‹คํŒจ: {StatusCode}", 
                response.StatusCode);
            return null;
        }
        catch (BrokenCircuitException ex)
        {
            _logger.LogError(ex, "Circuit Breaker๊ฐ€ ์—ด๋ ค์žˆ์–ด ์š”์ฒญ ์ฐจ๋‹จ๋จ");
            // Fallback ๋ฐ์ดํ„ฐ ๋ฐ˜ํ™˜ ๋˜๋Š” ์บ์‹œ๋œ ๋ฐ์ดํ„ฐ ์‚ฌ์šฉ
            return GetFallbackData(id);
        }
    }

    private ExternalData GetFallbackData(string id)
    {
        // ์บ์‹œ๋œ ๋ฐ์ดํ„ฐ๋‚˜ ๊ธฐ๋ณธ๊ฐ’ ๋ฐ˜ํ™˜
        return new ExternalData { Id = id, Status = "Unavailable" };
    }
}
// ์ข…ํ•ฉ์ ์ธ Resilience ์ •์ฑ…
public static class ResiliencePolicies
{
    public static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy(
        ILogger logger)
    {
        return Policy
            .HandleResult<HttpResponseMessage>(r => 
                r.StatusCode == System.Net.HttpStatusCode.RequestTimeout ||
                r.StatusCode == System.Net.HttpStatusCode.ServiceUnavailable)
            .Or<HttpRequestException>()
            .WaitAndRetryAsync(
                retryCount: 3,
                sleepDurationProvider: retryAttempt => 
                    TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
                onRetry: (outcome, timespan, retryCount, context) =>
                {
                    logger.LogWarning(
                        "์žฌ์‹œ๋„ {RetryCount}: {Timespan}์ดˆ ํ›„ ์žฌ์‹œ๋„",
                        retryCount, timespan.TotalSeconds);
                });
    }

    public static IAsyncPolicy<HttpResponseMessage> GetCircuitBreakerPolicy(
        ILogger logger)
    {
        return Policy
            .HandleResult<HttpResponseMessage>(r => !r.IsSuccessStatusCode)
            .Or<HttpRequestException>()
            .AdvancedCircuitBreakerAsync(
                failureThreshold: 0.5,
                samplingDuration: TimeSpan.FromSeconds(10),
                minimumThroughput: 8,
                durationOfBreak: TimeSpan.FromSeconds(30),
                onBreak: (result, duration) =>
                {
                    logger.LogWarning("Circuit Breaker ์—ด๋ฆผ");
                },
                onReset: () =>
                {
                    logger.LogInformation("Circuit Breaker ๋ฆฌ์…‹");
                });
    }

    public static IAsyncPolicy<HttpResponseMessage> GetTimeoutPolicy()
    {
        return Policy.TimeoutAsync<HttpResponseMessage>(
            TimeSpan.FromSeconds(10));
    }

    public static IAsyncPolicy<HttpResponseMessage> GetCombinedPolicy(
        ILogger logger)
    {
        var retry = GetRetryPolicy(logger);
        var circuitBreaker = GetCircuitBreakerPolicy(logger);
        var timeout = GetTimeoutPolicy();

        // ์ •์ฑ… ์กฐํ•ฉ: Timeout -> Retry -> Circuit Breaker
        return Policy.WrapAsync(timeout, retry, circuitBreaker);
    }
}

์ด๋Ÿฐ ํŒจํ„ด๋“ค์„ ์ ์šฉํ•˜๋ฉด ์™ธ๋ถ€ ์„œ๋น„์Šค๊ฐ€ ๋А๋ ค์ง€๊ฑฐ๋‚˜ ์žฅ์• ๊ฐ€ ๋‚˜๋„ ์šฐ๋ฆฌ ์‹œ์Šคํ…œ์€ ์•ˆ์ •์ ์œผ๋กœ ๋™์ž‘ํ•  ์ˆ˜ ์žˆ์–ด. ํŠนํžˆ ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ์•„ํ‚คํ…์ฒ˜์—์„œ๋Š” ํ•„์ˆ˜์ ์ด์•ผ! ๐Ÿ’ช

๐Ÿ“ฆ ๋ฉ”์‹œ์ง€ ํ๋ฅผ ํ™œ์šฉํ•œ ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ

ํ™•์žฅ ๊ฐ€๋Šฅํ•œ ์‹œ์Šคํ…œ์—์„œ ๋ฉ”์‹œ์ง€ ํ๋Š” ์ •๋ง ์ค‘์š”ํ•œ ์—ญํ• ์„ ํ•ด. ์‹œ๊ฐ„์ด ์˜ค๋ž˜ ๊ฑธ๋ฆฌ๋Š” ์ž‘์—…์„ ๋น„๋™๊ธฐ๋กœ ์ฒ˜๋ฆฌํ•˜๊ณ , ์„œ๋น„์Šค ๊ฐ„ ๊ฒฐํ•ฉ๋„๋ฅผ ๋‚ฎ์ถœ ์ˆ˜ ์žˆ๊ฑฐ๋“ ! ๐Ÿ“ฌ

// RabbitMQ๋ฅผ ์‚ฌ์šฉํ•œ ๋ฉ”์‹œ์ง€ ๋ฐœํ–‰
public class OrderService
{
    private readonly IOrderRepository _repository;
    private readonly IMessagePublisher _messagePublisher;
    private readonly ILogger<OrderService> _logger;

    public async Task<Order> CreateOrderAsync(CreateOrderDto dto)
    {
        // ์ฃผ๋ฌธ ์ƒ์„ฑ
        var order = new Order
        {
            UserId = dto.UserId,
            Items = dto.Items,
            TotalAmount = CalculateTotal(dto.Items),
            Status = OrderStatus.Pending,
            CreatedAt = DateTime.UtcNow
        };

        await _repository.AddAsync(order);
        _logger.LogInformation("์ฃผ๋ฌธ ์ƒ์„ฑ๋จ: {OrderId}", order.Id);

        // ๋ฉ”์‹œ์ง€ ๋ฐœํ–‰ (๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ)
        await _messagePublisher.PublishAsync(new OrderCreatedMessage
        {
            OrderId = order.Id,
            UserId = order.UserId,
            Items = order.Items.Select(i => new OrderItemMessage
            {
                ProductId = i.ProductId,
                Quantity = i.Quantity,
                Price = i.Price
            }).ToList(),
            TotalAmount = order.TotalAmount,
            CreatedAt = order.CreatedAt
        });

        return order;
    }
}

// ๋ฉ”์‹œ์ง€ ๋ฐœํ–‰์ž
public class RabbitMqPublisher : IMessagePublisher
{
    private readonly IConnection _connection;
    private readonly IModel _channel;
    private readonly ILogger<RabbitMqPublisher> _logger;

    public RabbitMqPublisher(
        IConfiguration configuration,
        ILogger<RabbitMqPublisher> logger)
    {
        _logger = logger;
        
        var factory = new ConnectionFactory
        {
            HostName = configuration["RabbitMQ:Host"],
            UserName = configuration["RabbitMQ:Username"],
            Password = configuration["RabbitMQ:Password"],
            VirtualHost = configuration["RabbitMQ:VirtualHost"]
        };

        _connection = factory.CreateConnection();
        _channel = _connection.CreateModel();

        // Exchange ์„ ์–ธ
        _channel.ExchangeDeclare(
            exchange: "orders",
            type: ExchangeType.Topic,
            durable: true);
    }

    public async Task PublishAsync<T>(T message, string routingKey = "")
    {
        var json = JsonSerializer.Serialize(message);
        var body = Encoding.UTF8.GetBytes(json);

        var properties = _channel.CreateBasicProperties();
        properties.Persistent = true;
        properties.ContentType = "application/json";
        properties.MessageId = Guid.NewGuid().ToString();
        properties.Timestamp = new AmqpTimestamp(
            DateTimeOffset.UtcNow.ToUnixTimeSeconds());

        _channel.BasicPublish(
            exchange: "orders",
            routingKey: routingKey,
            basicProperties: properties,
            body: body);

        _logger.LogDebug(
            "๋ฉ”์‹œ์ง€ ๋ฐœํ–‰๋จ: {MessageType}, RoutingKey: {RoutingKey}",
            typeof(T).Name, routingKey);

        await Task.CompletedTask;
    }
}
// ๋ฉ”์‹œ์ง€ ์†Œ๋น„์ž (Worker Service)
public class OrderMessageConsumer : BackgroundService
{
    private readonly IConnection _connection;
    private readonly IModel _channel;
    private readonly IServiceProvider _serviceProvider;
    private readonly ILogger<OrderMessageConsumer> _logger;

    public OrderMessageConsumer(
        IConfiguration configuration,
        IServiceProvider serviceProvider,
        ILogger<OrderMessageConsumer> logger)
    {
        _serviceProvider = serviceProvider;
        _logger = logger;

        var factory = new ConnectionFactory
        {
            HostName = configuration["RabbitMQ:Host"],
            UserName = configuration["RabbitMQ:Username"],
            Password = configuration["RabbitMQ:Password"]
        };

        _connection = factory.CreateConnection();
        _channel = _connection.CreateModel();

        // Queue ์„ ์–ธ
        _channel.QueueDeclare(
            queue: "order-processing",
            durable: true,
            exclusive: false,
            autoDelete: false);

        // Queue๋ฅผ Exchange์— ๋ฐ”์ธ๋”ฉ
        _channel.QueueBind(
            queue: "order-processing",
            exchange: "orders",
            routingKey: "order.created");

        // Prefetch ์„ค์ • (๋™์‹œ ์ฒ˜๋ฆฌ ๋ฉ”์‹œ์ง€ ์ˆ˜ ์ œํ•œ)
        _channel.BasicQos(0, 10, false);
    }

    protected override Task ExecuteAsync(CancellationToken stoppingToken)
    {
        var consumer = new EventingBasicConsumer(_channel);
        
        consumer.Received += async (model, ea) =>
        {
            var body = ea.Body.ToArray();
            var message = Encoding.UTF8.GetString(body);

            try
            {
                _logger.LogInformation("๋ฉ”์‹œ์ง€ ์ˆ˜์‹ : {MessageId}", 
                    ea.BasicProperties.MessageId);

                var orderMessage = JsonSerializer
                    .Deserialize<OrderCreatedMessage>(message);

                // ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ
                using var scope = _serviceProvider.CreateScope();
                var processor = scope.ServiceProvider
                    .GetRequiredService<IOrderProcessor>();
                
                await processor.ProcessOrderAsync(orderMessage);

                // ์ฒ˜๋ฆฌ ์™„๋ฃŒ ํ™•์ธ
                _channel.BasicAck(ea.DeliveryTag, false);
                
                _logger.LogInformation("๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ ์™„๋ฃŒ: {OrderId}", 
                    orderMessage.OrderId);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ ์‹คํŒจ");

                // ์žฌ์‹œ๋„ ๋˜๋Š” Dead Letter Queue๋กœ ์ด๋™
                _channel.BasicNack(ea.DeliveryTag, false, false);
            }
        };

        _channel.BasicConsume(
            queue: "order-processing",
            autoAck: false,
            consumer: consumer);

        return Task.CompletedTask;
    }

    public override void Dispose()
    {
        _channel?.Close();
        _connection?.Close();
        base.Dispose();
    }
}

๋ฉ”์‹œ์ง€ ํ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์ฃผ๋ฌธ ์ฒ˜๋ฆฌ, ์ด๋ฉ”์ผ ๋ฐœ์†ก, ์ด๋ฏธ์ง€ ๋ฆฌ์‚ฌ์ด์ง• ๊ฐ™์€ ๋ฌด๊ฑฐ์šด ์ž‘์—…๋“ค์„ ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์–ด. ์‚ฌ์šฉ์ž๋Š” ๋น ๋ฅธ ์‘๋‹ต์„ ๋ฐ›๊ณ , ์‹ค์ œ ์ฒ˜๋ฆฌ๋Š” ๋‚˜์ค‘์— ๋น„๋™๊ธฐ๋กœ ์ง„ํ–‰๋˜๋Š” ๊ฑฐ์ง€! โšก

๐Ÿ” ์„ฑ๋Šฅ ์ตœ์ ํ™” ํŒ

ํ™•์žฅ ๊ฐ€๋Šฅํ•œ ์•„ํ‚คํ…์ฒ˜๋ฅผ ๋งŒ๋“ค์—ˆ๋‹ค๊ณ  ๋์ด ์•„๋‹ˆ์•ผ. ์ง€์†์ ์ธ ์„ฑ๋Šฅ ์ตœ์ ํ™”๊ฐ€ ํ•„์š”ํ•ด! ์‹ค์ „์—์„œ ์จ๋จน์„ ์ˆ˜ ์žˆ๋Š” ํŒ๋“ค์„ ๊ณต์œ ํ• ๊ฒŒ. ๐ŸŽฏ

๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ตœ์ ํ™”:

์ธ๋ฑ์Šค ํ™œ์šฉ: ์ž์ฃผ ์กฐํšŒํ•˜๋Š” ์ปฌ๋Ÿผ์—๋Š” ๋ฐ˜๋“œ์‹œ ์ธ๋ฑ์Šค๋ฅผ ๊ฑธ์–ด์ค˜.
N+1 ๋ฌธ์ œ ํ•ด๊ฒฐ: Include()๋‚˜ ThenInclude()๋กœ Eager Loading ์‚ฌ์šฉ.
์ฟผ๋ฆฌ ์ตœ์ ํ™”: Select๋กœ ํ•„์š”ํ•œ ์ปฌ๋Ÿผ๋งŒ ๊ฐ€์ ธ์˜ค๊ธฐ.
Connection Pooling: ์ปค๋„ฅ์…˜ ์žฌ์‚ฌ์šฉ์œผ๋กœ ์˜ค๋ฒ„ํ—ค๋“œ ๊ฐ์†Œ.

// N+1 ๋ฌธ์ œ ํ•ด๊ฒฐ
// โŒ ๋‚˜์œ ์˜ˆ - N+1 ๋ฌธ์ œ ๋ฐœ์ƒ
public async Task<List<OrderDto>> GetOrdersBad()
{
    var orders = await _context.Orders.ToListAsync();
    
    // ๊ฐ ์ฃผ๋ฌธ๋งˆ๋‹ค ๋ณ„๋„ ์ฟผ๋ฆฌ ์‹คํ–‰ (N๋ฒˆ)
    foreach (var order in orders)
    {
        order.User = await _context.Users
            .FirstOrDefaultAsync(u => u.Id == order.UserId);
        order.Items = await _context.OrderItems
            .Where(i => i.OrderId == order.Id)
            .ToListAsync();
    }
    
    return orders.Select(MapToDto).ToList();
}

// โœ… ์ข‹์€ ์˜ˆ - Eager Loading
public async Task<List<OrderDto>> GetOrdersGood()
{
    var orders = await _context.Orders
        .Include(o => o.User)
        .Include(o => o.Items)
            .ThenInclude(i => i.Product)
        .AsNoTracking()
        .ToListAsync();
    
    return orders.Select(MapToDto).ToList();
}

// โœ… ๋” ์ข‹์€ ์˜ˆ - Projection์œผ๋กœ ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ๋งŒ
public async Task<List<OrderDto>> GetOrdersBest()
{
    return await _context.Orders
        .Select(o => new OrderDto
        {
            Id = o.Id,
            OrderDate = o.CreatedAt,
            UserName = o.User.Name,
            UserEmail = o.User.Email,
            TotalAmount = o.TotalAmount,
            Items = o.Items.Select(i => new OrderItemDto
            {
                ProductName = i.Product.Name,
                Quantity = i.Quantity,
                Price = i.Price
            }).ToList()
        })
        .AsNoTracking()
        .ToListAsync();
}
// ๋น„๋™๊ธฐ ํ”„๋กœ๊ทธ๋ž˜๋ฐ ์ตœ์ ํ™”
// โŒ ๋‚˜์œ ์˜ˆ - ์ˆœ์ฐจ ์‹คํ–‰
public async Task<DashboardData> GetDashboardDataBad()
{
    var orders = await _orderService.GetRecentOrdersAsync();
    var products = await _productService.GetPopularProductsAsync();
    var users = await _userService.GetActiveUsersAsync();
    var stats = await _statsService.GetStatisticsAsync();

    return new DashboardData
    {
        Orders = orders,
        Products = products,
        Users = users,
        Statistics = stats
    };
}

// โœ… ์ข‹์€ ์˜ˆ - ๋ณ‘๋ ฌ ์‹คํ–‰
public async Task<DashboardData> GetDashboardDataGood()
{
    var ordersTask = _orderService.GetRecentOrdersAsync();
    var productsTask = _productService.GetPopularProductsAsync();
    var usersTask = _userService.GetActiveUsersAsync();
    var statsTask = _statsService.GetStatisticsAsync();

    await Task.WhenAll(ordersTask, productsTask, usersTask, statsTask);

    return new DashboardData
    {
        Orders = await ordersTask,
        Products = await productsTask,
        Users = await usersTask,
        Statistics = await statsTask
    };
}
// Response Compression
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // Response Compression ์ถ”๊ฐ€
        services.AddResponseCompression(options =>
        {
            options.EnableForHttps = true;
            options.Providers.Add<BrotliCompressionProvider>();
            options.Providers.Add<GzipCompressionProvider>();
            options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(
                new[] { "application/json", "text/json" });
        });

        services.Configure<BrotliCompressionProviderOptions>(options =>
        {
            options.Level = CompressionLevel.Fastest;
        });

        services.Configure<GzipCompressionProviderOptions>(options =>
        {
            options.Level = CompressionLevel.Optimal;
        });
    }

    public void Configure(IApplicationBuilder app)
    {
        app.UseResponseCompression();
    }
}
๐Ÿ’ก ์„ฑ๋Šฅ ์ธก์ • ๋„๊ตฌ: BenchmarkDotNet: ์ฝ”๋“œ ์„ฑ๋Šฅ ๋ฒค์น˜๋งˆํ‚น
MiniProfiler: ์‹ค์‹œ๊ฐ„ ์„ฑ๋Šฅ ํ”„๋กœํŒŒ์ผ๋ง
Application Insights: ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ ๋ชจ๋‹ˆํ„ฐ๋ง
dotTrace: ์ƒ์„ธํ•œ ์„ฑ๋Šฅ ๋ถ„์„

๐ŸŒ ์‹ค์ „ ์•„ํ‚คํ…์ฒ˜ ์˜ˆ์ œ

์ž, ์ด์ œ ์ง€๊ธˆ๊นŒ์ง€ ๋ฐฐ์šด ๋‚ด์šฉ์„ ์ข…ํ•ฉํ•ด์„œ ์‹ค์ œ ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž! ์ „์ž์ƒ๊ฑฐ๋ž˜ ํ”Œ๋žซํผ์„ ์˜ˆ๋กœ ๋“ค์–ด๋ณผ๊ฒŒ. ๐Ÿ›’

// ์†”๋ฃจ์…˜ ๊ตฌ์กฐ
ECommercePlatform/
โ”œโ”€โ”€ src/
โ”‚   โ”œโ”€โ”€ ApiGateway/
โ”‚   โ”‚   โ”œโ”€โ”€ ApiGateway.csproj
โ”‚   โ”‚   โ”œโ”€โ”€ Program.cs
โ”‚   โ”‚   โ”œโ”€โ”€ appsettings.json
โ”‚   โ”‚   โ””โ”€โ”€ Middleware/
โ”‚   โ”‚
โ”‚   โ”œโ”€โ”€ Services/
โ”‚   โ”‚   โ”œโ”€โ”€ ProductService/
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ ProductService.Api/
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ ProductService.Application/
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ ProductService.Domain/
โ”‚   โ”‚   โ”‚   โ””โ”€โ”€ ProductService.Infrastructure/
โ”‚   โ”‚   โ”‚
โ”‚   โ”‚   โ”œโ”€โ”€ OrderService/
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ OrderService.Api/
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ OrderService.Application/
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ OrderService.Domain/
โ”‚   โ”‚   โ”‚   โ””โ”€โ”€ OrderService.Infrastructure/
โ”‚   โ”‚   โ”‚
โ”‚   โ”‚   โ”œโ”€โ”€ UserService/
โ”‚   โ”‚   โ””โ”€โ”€ PaymentService/
โ”‚   โ”‚
โ”‚   โ”œโ”€โ”€ BuildingBlocks/
โ”‚   โ”‚   โ”œโ”€โ”€ Common/
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ Common.csproj
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ Models/
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ Exceptions/
โ”‚   โ”‚   โ”‚   โ””โ”€โ”€ Extensions/
โ”‚   โ”‚   โ”‚
โ”‚   โ”‚   โ”œโ”€โ”€ EventBus/
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ EventBus.csproj
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ Abstractions/
โ”‚   โ”‚   โ”‚   โ””โ”€โ”€ RabbitMQ/
โ”‚   โ”‚   โ”‚
โ”‚   โ”‚   โ””โ”€โ”€ Logging/
โ”‚   โ”‚
โ”‚   โ””โ”€โ”€ WebApps/
โ”‚       โ”œโ”€โ”€ WebMVC/
โ”‚       โ””โ”€โ”€ WebSPA/
โ”‚
โ”œโ”€โ”€ tests/
โ”‚   โ”œโ”€โ”€ ProductService.Tests/
โ”‚   โ”œโ”€โ”€ OrderService.Tests/
โ”‚   โ””โ”€โ”€ IntegrationTests/
โ”‚
โ””โ”€โ”€ docker-compose.yml
// Clean Architecture ๊ธฐ๋ฐ˜ Product Service ๊ตฌ์กฐ

// Domain Layer - ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์˜ ํ•ต์‹ฌ
namespace ProductService.Domain.Entities
{
    public class Product : BaseEntity
    {
        public string Name { get; private set; }
        public string Description { get; private set; }
        public decimal Price { get; private set; }
        public int Stock { get; private set; }
        public Category Category { get; private set; }
        public List<ProductImage> Images { get; private set; }

        private Product() { } // EF Core์šฉ

        public Product(string name, decimal price, int stock)
        {
            if (string.IsNullOrWhiteSpace(name))
                throw new ArgumentException("์ƒํ’ˆ๋ช…์€ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.");
            
            if (price <= 0)
                throw new ArgumentException("๊ฐ€๊ฒฉ์€ 0๋ณด๋‹ค ์ปค์•ผ ํ•ฉ๋‹ˆ๋‹ค.");

            Name = name;
            Price = price;
            Stock = stock;
            Images = new List<ProductImage>();
        }

        public void UpdatePrice(decimal newPrice)
        {
            if (newPrice <= 0)
                throw new ArgumentException("๊ฐ€๊ฒฉ์€ 0๋ณด๋‹ค ์ปค์•ผ ํ•ฉ๋‹ˆ๋‹ค.");

            Price = newPrice;
            UpdatedAt = DateTime.UtcNow;
        }

        public void DecreaseStock(int quantity)
        {
            if (quantity > Stock)
                throw new InvalidOperationException("์žฌ๊ณ ๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค.");

            Stock -= quantity;
            UpdatedAt = DateTime.UtcNow;
        }

        public void IncreaseStock(int quantity)
        {
            Stock += quantity;
            UpdatedAt = DateTime.UtcNow;
        }
    }
}

// Application Layer - ์œ ์Šค์ผ€์ด์Šค
namespace ProductService.Application.Commands
{
    public class CreateProductCommand : IRequest<int>
    {
        public string Name { get; set; }
        public string Description { get; set; }
        public decimal Price { get; set; }
        public int Stock { get; set; }
        public int CategoryId { get; set; }
    }

    public class CreateProductCommandHandler 
        : IRequestHandler<CreateProductCommand, int>
    {
        private readonly IProductRepository _repository;
        private readonly IEventBus _eventBus;
        private readonly ILogger<CreateProductCommandHandler> _logger;

        public CreateProductCommandHandler(
            IProductRepository repository,
            IEventBus eventBus,
            ILogger<CreateProductCommandHandler> logger)
        {
            _repository = repository;
            _eventBus = eventBus;
            _logger = logger;
        }

        public async Task<int> Handle(
            CreateProductCommand request, 
            CancellationToken cancellationToken)
        {
            // ๋„๋ฉ”์ธ ์—”ํ‹ฐํ‹ฐ ์ƒ์„ฑ
            var product = new Product(
                request.Name, 
                request.Price, 
                request.Stock);
            
            product.Description = request.Description;

            // ์ €์žฅ
            await _repository.AddAsync(product);
            await _repository.UnitOfWork.SaveChangesAsync(cancellationToken);

            _logger.LogInformation(
                "์ƒํ’ˆ ์ƒ์„ฑ๋จ: {ProductId} - {ProductName}", 
                product.Id, product.Name);

            // ์ด๋ฒคํŠธ ๋ฐœํ–‰
            await _eventBus.PublishAsync(new ProductCreatedEvent
            {
                ProductId = product.Id,
                Name = product.Name,
                Price = product.Price
            });

            return product.Id;
        }
    }
}
// Infrastructure Layer - ๋ฐ์ดํ„ฐ ์ ‘๊ทผ
namespace ProductService.Infrastructure.Repositories
{
    public class ProductRepository : IProductRepository
    {
        private readonly ProductDbContext _context;

        public IUnitOfWork UnitOfWork => _context;

        public ProductRepository(ProductDbContext context)
        {
            _context = context ?? throw new ArgumentNullException(nameof(context));
        }

        public async Task<Product> GetByIdAsync(int id)
        {
            return await _context.Products
                .Include(p => p.Category)
                .Include(p => p.Images)
                .FirstOrDefaultAsync(p => p.Id == id);
        }

        public async Task<IEnumerable<Product>> GetByCategoryAsync(int categoryId)
        {
            return await _context.Products
                .Where(p => p.Category.Id == categoryId)
                .Include(p => p.Images)
                .AsNoTracking()
                .ToListAsync();
        }

        public async Task<Product> AddAsync(Product product)
        {
            return (await _context.Products.AddAsync(product)).Entity;
        }

        public void Update(Product product)
        {
            _context.Entry(product).State = EntityState.Modified;
        }
    }
}

// API Layer - ์ปจํŠธ๋กค๋Ÿฌ
namespace ProductService.Api.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class ProductsController : ControllerBase
    {
        private readonly IMediator _mediator;
        private readonly ILogger<ProductsController> _logger;

        public ProductsController(
            IMediator mediator,
            ILogger<ProductsController> logger)
        {
            _mediator = mediator;
            _logger = logger;
        }

        [HttpPost]
        [ProducesResponseType(StatusCodes.Status201Created)]
        [ProducesResponseType(StatusCodes.Status400BadRequest)]
        public async Task<ActionResult<int>> CreateProduct(
            [FromBody] CreateProductCommand command)
        {
            var productId = await _mediator.Send(command);
            return CreatedAtAction(
                nameof(GetProduct), 
                new { id = productId }, 
                productId);
        }

        [HttpGet("{id}")]
        [ProducesResponseType(typeof(ProductDto), StatusCodes.Status200OK)]
        [ProducesResponseType(StatusCodes.Status404NotFound)]
        public async Task<ActionResult<ProductDto>> GetProduct(int id)
        {
            var query = new GetProductQuery { Id = id };
            var product = await _mediator.Send(query);

            if (product == null)
                return NotFound();

            return Ok(product);
        }

        [HttpPut("{id}/stock")]
        public async Task<IActionResult> UpdateStock(
            int id, 
            [FromBody] UpdateStockCommand command)
        {
            command.ProductId = id;
            await _mediator.Send(command);
            return NoContent();
        }
    }
}

์ด๋ ‡๊ฒŒ Clean Architecture๋ฅผ ์ ์šฉํ•˜๋ฉด ๊ฐ ๋ ˆ์ด์–ด๊ฐ€ ๋ช…ํ™•ํ•˜๊ฒŒ ๋ถ„๋ฆฌ๋˜๊ณ , ํ…Œ์ŠคํŠธํ•˜๊ธฐ๋„ ์‰ฌ์›Œ์ ธ. ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์€ Domain Layer์—, ์œ ์Šค์ผ€์ด์Šค๋Š” Application Layer์—, ๊ธฐ์ˆ ์ ์ธ ๊ตฌํ˜„์€ Infrastructure Layer์— ๋‘๋Š” ๊ฑฐ์ง€! ๐ŸŽจ

๐Ÿš€ ๋ฐฐํฌ ์ „๋žต๊ณผ CI/CD

ํ™•์žฅ ๊ฐ€๋Šฅํ•œ ์•„ํ‚คํ…์ฒ˜๋ฅผ ๋งŒ๋“ค์—ˆ์œผ๋ฉด ์ด์ œ ์•ˆ์ „ํ•˜๊ฒŒ ๋ฐฐํฌํ•˜๋Š” ๋ฐฉ๋ฒ•๋„ ์•Œ์•„์•ผ ํ•ด! ํ˜„๋Œ€์ ์ธ CI/CD ํŒŒ์ดํ”„๋ผ์ธ์„ ๊ตฌ์ถ•ํ•ด๋ณด์ž. ๐Ÿ”„

# GitHub Actions๋ฅผ ์‚ฌ์šฉํ•œ CI/CD ํŒŒ์ดํ”„๋ผ์ธ
# .github/workflows/deploy.yml

name: Build and Deploy

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

env:
  DOTNET_VERSION: '8.0.x'
  AZURE_WEBAPP_NAME: 'my-ecommerce-app'

jobs:
  build-and-test:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Setup .NET
      uses: actions/setup-dotnet@v3
      with:
        dotnet-version: ${{ env.DOTNET_VERSION }}
    
    - name: Restore dependencies
      run: dotnet restore
    
    - name: Build
      run: dotnet build --configuration Release --no-restore
    
    - name: Run unit tests
      run: dotnet test --no-build --verbosity normal --collect:"XPlat Code Coverage"
    
    - name: Upload coverage reports
      uses: codecov/codecov-action@v3
      with:
        files: ./coverage.cobertura.xml
    
    - name: Publish
      run: dotnet publish -c Release -o ./publish
    
    - name: Upload artifact
      uses: actions/upload-artifact@v3
      with:
        name: webapp
        path: ./publish

  docker-build:
    runs-on: ubuntu-latest
    needs: build-and-test
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Set up Docker Buildx
      uses: docker/setup-buildx-action@v2
    
    - name: Login to Container Registry
      uses: docker/login-action@v2
      with:
        registry: ${{ secrets.ACR_LOGIN_SERVER }}
        username: ${{ secrets.ACR_USERNAME }}
        password: ${{ secrets.ACR_PASSWORD }}
    
    - name: Build and push Docker image
      uses: docker/build-push-action@v4
      with:
        context: .
        push: true
        tags: |
          ${{ secrets.ACR_LOGIN_SERVER }}/product-service:${{ github.sha }}
          ${{ secrets.ACR_LOGIN_SERVER }}/product-service:latest
        cache-from: type=registry,ref=${{ secrets.ACR_LOGIN_SERVER }}/product-service:buildcache
        cache-to: type=registry,ref=${{ secrets.ACR_LOGIN_SERVER }}/product-service:buildcache,mode=max

  deploy-to-staging:
    runs-on: ubuntu-latest
    needs: docker-build
    if: github.ref == 'refs/heads/develop'
    environment: staging
    
    steps:
    - name: Deploy to Azure Kubernetes Service (Staging)
      uses: azure/k8s-deploy@v4
      with:
        manifests: |
          k8s/staging/deployment.yml
          k8s/staging/service.yml
        images: |
          ${{ secrets.ACR_LOGIN_SERVER }}/product-service:${{ github.sha }}
        namespace: staging

  deploy-to-production:
    runs-on: ubuntu-latest
    needs: docker-build
    if: github.ref == 'refs/heads/main'
    environment: production
    
    steps:
    - name: Deploy to Azure Kubernetes Service (Production)
      uses: azure/k8s-deploy@v4
      with:
        manifests: |
          k8s/production/deployment.yml
          k8s/production/service.yml
        images: |
          ${{ secrets.ACR_LOGIN_SERVER }}/product-service:${{ github.sha }}
        namespace: production
        strategy: canary
        percentage: 20
    
    - name: Wait for canary validation
      run: sleep 300
    
    - name: Promote canary deployment
      uses: azure/k8s-deploy@v4
      with:
        manifests: |
          k8s/production/deployment.yml
        images: |
          ${{ secrets.ACR_LOGIN_SERVER }}/product-service:${{ github.sha }}
        namespace: production
        strategy: canary
        percentage: 100

๋ฐฐํฌ ์ „๋žต ์ข…๋ฅ˜:

Rolling Update: ์ ์ง„์ ์œผ๋กœ ์ƒˆ ๋ฒ„์ „์œผ๋กœ ๊ต์ฒด. ๊ฐ€์žฅ ์ผ๋ฐ˜์ .
Blue-Green: ์ƒˆ ํ™˜๊ฒฝ์„ ์™„์ „ํžˆ ์ค€๋น„ํ•œ ํ›„ ํŠธ๋ž˜ํ”ฝ ์ „ํ™˜.
Canary: ์ผ๋ถ€ ์‚ฌ์šฉ์ž์—๊ฒŒ๋งŒ ๋จผ์ € ๋ฐฐํฌํ•˜๊ณ  ์ ์ง„์  ํ™•๋Œ€.
A/B Testing: ๋‘ ๋ฒ„์ „์„ ๋™์‹œ์— ์šด์˜ํ•˜๋ฉฐ ๋น„๊ต.

๐Ÿ’ผ ์žฌ๋Šฅ๋„ท์—์„œ C# ์ „๋ฌธ๊ฐ€ ์ฐพ๊ธฐ

์—ฌ๊ธฐ๊นŒ์ง€ ์ฝ์—ˆ๋‹ค๋ฉด ์ •๋ง ๋Œ€๋‹จํ•ด! ๐ŸŽ‰ ํ™•์žฅ ๊ฐ€๋Šฅํ•œ ์›น ์„œ๋น„์Šค ์•„ํ‚คํ…์ฒ˜๋Š” ์ •๋ง ๋ฐฉ๋Œ€ํ•œ ์ฃผ์ œ์•ผ. ํ˜ผ์ž์„œ ๋ชจ๋“  ๊ฑธ ๋‹ค ๊ตฌํ˜„ํ•˜๊ธฐ๋Š” ์‰ฝ์ง€ ์•Š์ง€. ํŠนํžˆ ์ฒ˜์Œ ์‹œ์ž‘ํ•˜๋Š” ๊ฒฝ์šฐ์—๋Š” ๋”๋”์šฑ ๊ทธ๋ ‡๊ณ .

๊ทธ๋Ÿด ๋•Œ๋Š” ์žฌ๋Šฅ๋„ท์—์„œ C# ์ „๋ฌธ๊ฐ€์˜ ๋„์›€์„ ๋ฐ›๋Š” ๊ฒƒ๋„ ์ข‹์€ ๋ฐฉ๋ฒ•์ด์•ผ! ์‹ค๋ฌด ๊ฒฝํ—˜์ด ํ’๋ถ€ํ•œ ๊ฐœ๋ฐœ์ž๋“ค์ด ์•„ํ‚คํ…์ฒ˜ ์„ค๊ณ„๋ถ€ํ„ฐ ์ฝ”๋“œ ๋ฆฌ๋ทฐ, ์„ฑ๋Šฅ ์ตœ์ ํ™”๊นŒ์ง€ ๋‹ค์–‘ํ•œ ์„œ๋น„์Šค๋ฅผ ์ œ๊ณตํ•˜๊ณ  ์žˆ๊ฑฐ๋“ . ํŠนํžˆ ํ”„๋กœ์ ํŠธ ์ดˆ๊ธฐ ๋‹จ๊ณ„์—์„œ ์˜ฌ๋ฐ”๋ฅธ ๋ฐฉํ–ฅ์„ ์žก๋Š” ๊ฒŒ ์ •๋ง ์ค‘์š”ํ•œ๋ฐ, ๊ฒฝํ—˜ ๋งŽ์€ ๋ฉ˜ํ† ์˜ ์กฐ์–ธ์ด ํฐ ๋„์›€์ด ๋  ๊ฑฐ์•ผ! ๐Ÿ˜Š

์žฌ๋Šฅ๋„ท์—๋Š” ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ์•„ํ‚คํ…์ฒ˜ ๊ตฌ์ถ•, ํด๋ผ์šฐ๋“œ ๋ฐฐํฌ, ์„ฑ๋Šฅ ํŠœ๋‹ ๋“ฑ ๋‹ค์–‘ํ•œ ๋ถ„์•ผ์˜ ์ „๋ฌธ๊ฐ€๋“ค์ด ์žˆ์–ด. 1:1 ์ปจ์„คํŒ…๋ถ€ํ„ฐ ํ”„๋กœ์ ํŠธ ์ „์ฒด ๊ฐœ๋ฐœ๊นŒ์ง€, ํ•„์š”ํ•œ ๋งŒํผ๋งŒ ๋„์›€์„ ๋ฐ›์„ ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฒŒ ์žฅ์ ์ด์ง€!

๐ŸŽ“ ๋งˆ๋ฌด๋ฆฌํ•˜๋ฉฐ

์ž, ์˜ค๋Š˜ ์šฐ๋ฆฌ๊ฐ€ ๋‹ค๋ฃฌ ๋‚ด์šฉ์„ ์ •๋ฆฌํ•ด๋ณผ๊นŒ? ์ •๋ง ๋งŽ์€ ๊ฑธ ๋ฐฐ์› ์–ด! ๐Ÿš€

ํ•ต์‹ฌ ์š”์•ฝ:

โœ… ๋ ˆ์ด์–ด๋“œ ์•„ํ‚คํ…์ฒ˜๋กœ ๊ด€์‹ฌ์‚ฌ ๋ถ„๋ฆฌ
โœ… ์˜์กด์„ฑ ์ฃผ์ž…์œผ๋กœ ๋А์Šจํ•œ ๊ฒฐํ•ฉ
โœ… CQRS ํŒจํ„ด์œผ๋กœ ์ฝ๊ธฐ/์“ฐ๊ธฐ ์ตœ์ ํ™”
โœ… ์ด๋ฒคํŠธ ๊ธฐ๋ฐ˜ ์•„ํ‚คํ…์ฒ˜๋กœ ์„œ๋น„์Šค ๊ฐ„ ํ†ต์‹ 
โœ… ์บ์‹ฑ ์ „๋žต์œผ๋กœ ์„ฑ๋Šฅ ํ–ฅ์ƒ
โœ… API Gateway๋กœ ์ง„์ž…์  ํ†ตํ•ฉ
โœ… ๋ชจ๋‹ˆํ„ฐ๋ง๊ณผ ๋กœ๊น…์œผ๋กœ ๊ฐ€์‹œ์„ฑ ํ™•๋ณด
โœ… ์ปจํ…Œ์ด๋„ˆํ™”๋กœ ๋ฐฐํฌ ๊ฐ„์†Œํ™”
โœ… ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ™•์žฅ ์ „๋žต
โœ… Circuit Breaker๋กœ ์žฅ์•  ๊ฒฉ๋ฆฌ
โœ… ๋ฉ”์‹œ์ง€ ํ๋กœ ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ
โœ… CI/CD๋กœ ์ž๋™ํ™”๋œ ๋ฐฐํฌ

ํ™•์žฅ ๊ฐ€๋Šฅํ•œ ์•„ํ‚คํ…์ฒ˜๋Š” ํ•˜๋ฃจ์•„์นจ์— ๋งŒ๋“ค์–ด์ง€์ง€ ์•Š์•„. ์ง€์†์ ์ธ ๊ฐœ์„ ๊ณผ ๋ฆฌํŒฉํ† ๋ง์ด ํ•„์š”ํ•˜์ง€. ์ฒ˜์Œ๋ถ€ํ„ฐ ์™„๋ฒฝํ•  ํ•„์š”๋Š” ์—†์–ด. ์ž‘๊ฒŒ ์‹œ์ž‘ํ•ด์„œ ํ•„์š”์— ๋”ฐ๋ผ ์ ์ง„์ ์œผ๋กœ ๊ฐœ์„ ํ•ด๋‚˜๊ฐ€๋ฉด ๋ผ! ๐Ÿ’ช

๊ฐ€์žฅ ์ค‘์š”ํ•œ ๊ฑด ์‹ค์ œ๋กœ ๋งŒ๋“ค์–ด๋ณด๋Š” ๊ฑฐ์•ผ. ์ด๋ก ๋งŒ ์•Œ๊ณ  ์žˆ์œผ๋ฉด ์†Œ์šฉ์—†์–ด. ์ž‘์€ ํ”„๋กœ์ ํŠธ๋ผ๋„ ์ข‹์œผ๋‹ˆ๊นŒ ์ง์ ‘ ๊ตฌํ˜„ํ•ด๋ณด๋ฉด์„œ ๊ฒฝํ—˜์„ ์Œ“์•„๊ฐ€๊ธธ ๋ฐ”๋ผ! ์‹ค์ˆ˜ํ•˜๊ณ  ๋ฐฐ์šฐ๋Š” ๊ณผ์ •์ด ๊ฐ€์žฅ ๊ฐ’์ง„ ๊ฒฝํ—˜์ด ๋  ๊ฑฐ์•ผ.

๊ทธ๋ฆฌ๊ณ  ๊ธฐ์–ตํ•ด! ์•„ํ‚คํ…์ฒ˜๋Š” ์€ํƒ„ํ™˜(Silver Bullet)์ด ์•„๋‹ˆ์•ผ. ํ”„๋กœ์ ํŠธ์˜ ๊ทœ๋ชจ, ํŒ€์˜ ์—ญ๋Ÿ‰, ๋น„์ฆˆ๋‹ˆ์Šค ์š”๊ตฌ์‚ฌํ•ญ์— ๋”ฐ๋ผ ์ ์ ˆํ•œ ์ˆ˜์ค€์˜ ์•„ํ‚คํ…์ฒ˜๋ฅผ ์„ ํƒํ•˜๋Š” ๊ฒŒ ์ค‘์š”ํ•ด. ๊ณผ๋„ํ•œ ์—”์ง€๋‹ˆ์–ด๋ง์€ ์˜คํžˆ๋ ค ๋…์ด ๋  ์ˆ˜ ์žˆ๊ฑฐ๋“ . โš–๏ธ

๋งˆ์ง€๋ง‰์œผ๋กœ, ๊ฐœ๋ฐœ์€ ํ˜ผ์ž ํ•˜๋Š” ๊ฒŒ ์•„๋‹ˆ์•ผ. ์ปค๋ฎค๋‹ˆํ‹ฐ์— ์ฐธ์—ฌํ•˜๊ณ , ๋‹ค๋ฅธ ๊ฐœ๋ฐœ์ž๋“ค๊ณผ ์†Œํ†ตํ•˜๊ณ , ์ฝ”๋“œ ๋ฆฌ๋ทฐ๋ฅผ ๋ฐ›๊ณ , ์˜คํ”ˆ์†Œ์Šค์— ๊ธฐ์—ฌํ•˜๋ฉด์„œ ์„ฑ์žฅํ•ด๋‚˜๊ฐ€์ž! ์šฐ๋ฆฌ ๋ชจ๋‘ ํ•จ๊ป˜ ๋ฐœ์ „ํ•˜๋Š” ๊ฑฐ์•ผ! ๐ŸŒŸ

์˜ค๋Š˜ ๋ฐฐ์šด ๋‚ด์šฉ์ด ๋„ˆ์˜ ํ”„๋กœ์ ํŠธ์— ๋„์›€์ด ๋˜๊ธธ ๋ฐ”๋ผ! ์งˆ๋ฌธ์ด๋‚˜ ๊ถ๊ธˆํ•œ ์ ์ด ์žˆ์œผ๋ฉด ์–ธ์ œ๋“  ์ปค๋ฎค๋‹ˆํ‹ฐ์— ๋ฌผ์–ด๋ด. ๋‹ค๋“ค ๊ธฐ๊บผ์ด ๋„์™€์ค„ ๊ฑฐ์•ผ. ํ™”์ดํŒ…! ๐Ÿ”ฅ

Happy Coding! ์ฆ๊ฑฐ์šด ๊ฐœ๋ฐœ ๋˜๊ธธ! ๐Ÿš€โœจ

๋Œ“๊ธ€ ์ž‘์„ฑ

์ด ๊ธ€์— ๋Œ€ํ•œ ์—ฌ๋Ÿฌ๋ถ„์˜ ์ƒ๊ฐ์„ ๋“ค๋ ค์ฃผ์„ธ์š”

๋Œ“๊ธ€ 0
๋Œ“๊ธ€์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘์ž…๋‹ˆ๋‹ค.

์•„์ง ๋Œ“๊ธ€์ด ์—†์Šต๋‹ˆ๋‹ค.