From ecc
관용적인 C# 및 .NET 패턴, 컨벤션, 의존성 주입(DI), async/await, 견고하고 유지 관리가 용이한 .NET 애플리케이션 구축을 위한 모범 사례입니다.
npx claudepluginhub sam42-lab/everything-claude-code-krThis skill uses the workspace's default tool permissions.
견고하고 성능이 뛰어나며 유지 관리가 쉬운 애플리케이션을 구축하기 위한 관용적인 C# 및 .NET 패턴입니다.
Mandates invoking relevant skills via tools before any response in coding sessions. Covers access, priorities, and adaptations for Claude Code, Copilot CLI, Gemini CLI.
Share bugs, ideas, or general feedback.
견고하고 성능이 뛰어나며 유지 관리가 쉬운 애플리케이션을 구축하기 위한 관용적인 C# 및 .NET 패턴입니다.
데이터 모델에는 record와 init 전용 속성을 사용하세요. 가변성(Mutability)은 명확하고 정당한 이유가 있을 때만 선택해야 합니다.
// 좋음: 불변 값 객체(Value Object)
public sealed record Money(decimal Amount, string Currency);
// 좋음: init 세터를 가진 불변 DTO
public sealed class CreateOrderRequest
{
public required string CustomerId { get; init; }
public required IReadOnlyList<OrderItem> Items { get; init; }
}
// 나쁨: 공개 세터(public setter)를 가진 가변 모델
public class Order
{
public string CustomerId { get; set; }
public List<OrderItem> Items { get; set; }
}
Null 허용 여부(nullability), 접근 제한자, 그리고 코드의 의도를 명확하게 밝히세요.
// 좋음: 명시적인 접근 제한자와 null 허용 설정
public sealed class UserService
{
private readonly IUserRepository _repository;
private readonly ILogger<UserService> _logger;
public UserService(IUserRepository repository, ILogger<UserService> logger)
{
_repository = repository ?? throw new ArgumentNullException(nameof(repository));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public async Task<User?> FindByIdAsync(Guid id, CancellationToken cancellationToken)
{
return await _repository.FindByIdAsync(id, cancellationToken);
}
}
서비스 경계에는 인터페이스를 사용하세요. DI 컨테이너를 통해 등록합니다.
// 좋음: 인터페이스 기반 의존성
public interface IOrderRepository
{
Task<Order?> FindByIdAsync(Guid id, CancellationToken cancellationToken);
Task<IReadOnlyList<Order>> FindByCustomerAsync(string customerId, CancellationToken cancellationToken);
Task AddAsync(Order order, CancellationToken cancellationToken);
}
// 등록 예시
builder.Services.AddScoped<IOrderRepository, SqlOrderRepository>();
// 좋음: 처음부터 끝까지 비동기 유지 및 CancellationToken 사용
public async Task<OrderSummary> GetOrderSummaryAsync(
Guid orderId,
CancellationToken cancellationToken)
{
var order = await _repository.FindByIdAsync(orderId, cancellationToken)
?? throw new NotFoundException($"Order {orderId} not found");
var customer = await _customerService.GetAsync(order.CustomerId, cancellationToken);
return new OrderSummary(order, customer);
}
// 나쁨: 비동기 작업 시 블로킹(Blocking) 발생
public OrderSummary GetOrderSummary(Guid orderId)
{
var order = _repository.FindByIdAsync(orderId, CancellationToken.None).Result; // 데드락 위험
return new OrderSummary(order);
}
// 좋음: 독립적인 작업을 동시에 실행
public async Task<DashboardData> LoadDashboardAsync(CancellationToken cancellationToken)
{
var ordersTask = _orderService.GetRecentAsync(cancellationToken);
var metricsTask = _metricsService.GetCurrentAsync(cancellationToken);
var alertsTask = _alertService.GetActiveAsync(cancellationToken);
await Task.WhenAll(ordersTask, metricsTask, alertsTask);
return new DashboardData(
Orders: await ordersTask,
Metrics: await metricsTask,
Alerts: await alertsTask);
}
구성(Configuration) 섹션을 강력한 형식의 객체에 바인딩합니다.
public sealed class SmtpOptions
{
public const string SectionName = "Smtp";
public required string Host { get; init; }
public required int Port { get; init; }
public required string Username { get; init; }
public bool UseSsl { get; init; } = true;
}
// 등록 예시
builder.Services.Configure<SmtpOptions>(
builder.Configuration.GetSection(SmtpOptions.SectionName));
// 주입을 통한 사용 예시
public class EmailService(IOptions<SmtpOptions> options)
{
private readonly SmtpOptions _smtp = options.Value;
}
예상되는 실패에 대해서는 예외를 던지는 대신 명시적인 성공/실패 여부를 반환합니다.
public sealed record Result<T>
{
public bool IsSuccess { get; }
public T? Value { get; }
public string? Error { get; }
private Result(T value) { IsSuccess = true; Value = value; }
private Result(string error) { IsSuccess = false; Error = error; }
public static Result<T> Success(T value) => new(value);
public static Result<T> Failure(string error) => new(error);
}
// 사용 예시
public async Task<Result<Order>> PlaceOrderAsync(CreateOrderRequest request)
{
if (request.Items.Count == 0)
return Result<Order>.Failure("주문에는 최소 하나의 항목이 포함되어야 합니다.");
var order = Order.Create(request);
await _repository.AddAsync(order, CancellationToken.None);
return Result<Order>.Success(order);
}
public sealed class SqlOrderRepository : IOrderRepository
{
private readonly AppDbContext _db;
public SqlOrderRepository(AppDbContext db) => _db = db;
public async Task<Order?> FindByIdAsync(Guid id, CancellationToken cancellationToken)
{
return await _db.Orders
.Include(o => o.Items)
.AsNoTracking()
.FirstOrDefaultAsync(o => o.Id == id, cancellationToken);
}
public async Task<IReadOnlyList<Order>> FindByCustomerAsync(
string customerId,
CancellationToken cancellationToken)
{
return await _db.Orders
.Where(o => o.CustomerId == customerId)
.OrderByDescending(o => o.CreatedAt)
.AsNoTracking()
.ToListAsync(cancellationToken);
}
public async Task AddAsync(Order order, CancellationToken cancellationToken)
{
_db.Orders.Add(order);
await _db.SaveChangesAsync(cancellationToken);
}
}
// 커스텀 미들웨어 예시
public sealed class RequestTimingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<RequestTimingMiddleware> _logger;
public RequestTimingMiddleware(RequestDelegate next, ILogger<RequestTimingMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
var stopwatch = Stopwatch.StartNew();
try
{
await _next(context);
}
finally
{
stopwatch.Stop();
_logger.LogInformation(
"Request {Method} {Path} completed in {ElapsedMs}ms with status {StatusCode}",
context.Request.Method,
context.Request.Path,
stopwatch.ElapsedMilliseconds,
context.Response.StatusCode);
}
}
}
// 라우트 그룹으로 정리된 API
var orders = app.MapGroup("/api/orders")
.RequireAuthorization()
.WithTags("Orders");
orders.MapGet("/{id:guid}", async (
Guid id,
IOrderRepository repository,
CancellationToken cancellationToken) =>
{
var order = await repository.FindByIdAsync(id, cancellationToken);
return order is not null
? TypedResults.Ok(order)
: TypedResults.NotFound();
});
orders.MapPost("/", async (
CreateOrderRequest request,
IOrderService service,
CancellationToken cancellationToken) =>
{
var result = await service.PlaceOrderAsync(request, cancellationToken);
return result.IsSuccess
? TypedResults.Created($"/api/orders/{result.Value!.Id}", result.Value)
: TypedResults.BadRequest(result.Error);
});
// 좋음: 명확한 유효성 검사를 통한 조기 반환
public async Task<ProcessResult> ProcessPaymentAsync(
PaymentRequest request,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
if (request.Amount <= 0)
throw new ArgumentOutOfRangeException(nameof(request.Amount), "금액은 양수여야 합니다.");
if (string.IsNullOrWhiteSpace(request.Currency))
throw new ArgumentException("통화 정보는 필수입니다.", nameof(request.Currency));
// 중첩 없이 정상 경로(Happy path)를 계속 진행함
var gateway = _gatewayFactory.Create(request.Currency);
return await gateway.ChargeAsync(request, cancellationToken);
}
| 안티패턴 | 해결책 |
|---|---|
async void 메서드 | Task를 반환하세요 (이벤트 핸들러 제외) |
.Result 또는 .Wait() | await를 사용하세요 |
catch (Exception) { } | 에러를 처리하거나 컨텍스트와 함께 다시 던지세요(rethrow) |
생성자에서 new Service() 호출 | 생성자 주입(DI)을 사용하세요 |
public 필드 | 적절한 접근자를 가진 속성(property)을 사용하세요 |
비즈니스 로직에서의 dynamic | 제네릭이나 명시적인 타입을 사용하세요 |
가변 static 상태 | DI 스코프나 ConcurrentDictionary를 사용하세요 |
루프 안에서의 string.Format | StringBuilder나 보간된 문자열 핸들러를 사용하세요 |