Ver3.0 ๐ C#์ผ๋ก ๊ตฌํํ๋ ํ์ฅ ๊ฐ๋ฅํ ์น ์๋น์ค ์ํคํ ์ฒ: ์ฑ์ฅํ๋ ์๋น์ค๋ฅผ ์ํ ์ค์ ์ค๊ณ ๊ฐ์ด๋

๐ C#์ผ๋ก ๊ตฌํํ๋ ํ์ฅ ๊ฐ๋ฅํ ์น ์๋น์ค ์ํคํ ์ฒ: ์ฑ์ฅํ๋ ์๋น์ค๋ฅผ ์ํ ์ค์ ์ค๊ณ ๊ฐ์ด๋
์น๊ตฌ์ผ, ์ค๋์ ์ง์ง ์ค๋ฌด์์ ์จ๋จน์ ์ ์๋ C# ์น ์๋น์ค ์ํคํ ์ฒ ์ด์ผ๊ธฐ๋ฅผ ํด๋ณผ๊ฒ! ๐ฏ
์ผ, ๋๋ ๊ฐ๋ฐํ๋ค ๋ณด๋ฉด ์ด๋ฐ ๊ฒฝํ ์์ง ์์? ์ฒ์์ ๊ฐ๋จํ๊ฒ ๋ง๋ค์๋ ์๋น์ค๊ฐ ์ฌ์ฉ์๊ฐ ๋์ด๋๋ฉด์ ์ ์ ๋๋ ค์ง๊ณ , ์ฝ๋๋ ์คํ๊ฒํฐ์ฒ๋ผ ์์ผ๊ฐ๊ณ ... ๐ ๊ทธ๋ด ๋๋ง๋ค "์, ์ฒ์๋ถํฐ ์ ๋๋ก ์ค๊ณํ ๊ฑธ" ํ๊ณ ํํํ๊ฒ ๋์์?
์ค๋์ ๊ทธ๋ฐ ๊ณ ๋ฏผ์ ํด๊ฒฐํด์ค ํ์ฅ ๊ฐ๋ฅํ ์น ์๋น์ค ์ํคํ ์ฒ์ ๋ํด ๊น์ด ์๊ฒ ํํค์ณ๋ณผ ๊ฑฐ์ผ. C#๊ณผ .NET ์ํ๊ณ๋ฅผ ํ์ฉํด์ ๋ง์ด์ง! ์ค์ ๋ก ๋ด๊ฐ ํ๋ก์ ํธ์์ ๊ฒช์๋ ์ํ์ฐฉ์ค๋ค๊ณผ ํด๊ฒฐ ๋ฐฉ๋ฒ๋ค์ ๋ชจ๋ ๋ด์์ผ๋๊น, ๋๊น์ง ์ง์คํด์ค! ๐ช
๐ฏ ํ์ฅ ๊ฐ๋ฅํ ์ํคํ ์ฒ๊ฐ ๋ญ๊ธธ๋?
๋จผ์ ๊ธฐ๋ณธ๋ถํฐ ์ง๊ณ ๋์ด๊ฐ์. ํ์ฅ ๊ฐ๋ฅํ ์ํคํ ์ฒ(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();
}
}
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");
}
}
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();
}
}
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
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์์ง ๋๊ธ์ด ์์ต๋๋ค.