C# 12 Primary Constructors — 간결한 생성자 선언
Primary Constructors는 C# 12에서 클래스와 구조체에 도입된 기능으로, 생성자 매개변수를 타입 선언부에 직접 정의합니다. 이전에는 레코드(record)에서만 가능했던 문법이 일반 클래스/구조체로 확대되었습니다.
1. 기본 문법
섹션 제목: “1. 기본 문법”// Before (C# 11 이하)public class OrderService{ private readonly IOrderRepository _repo; private readonly ILogger<OrderService> _logger;
public OrderService(IOrderRepository repo, ILogger<OrderService> logger) { _repo = repo; _logger = logger; }}
// After (C# 12)public class OrderService(IOrderRepository repo, ILogger<OrderService> logger){ // repo, logger를 클래스 전체에서 직접 사용 public async Task<Order?> GetAsync(int id) { logger.LogInformation("Getting order {Id}", id); return await repo.FindAsync(id); }}2. 매개변수 캡처와 필드 초기화
섹션 제목: “2. 매개변수 캡처와 필드 초기화”Primary Constructor 매개변수는 필드가 아닙니다. 필드로 저장하려면 명시적으로 할당해야 합니다.
public class Cache(int capacity){ // 매개변수를 필드로 저장 private readonly int _capacity = capacity;
// 매개변수를 이용한 필드 초기화 private readonly Dictionary<string, object> _store = new(capacity);}3. 기반 클래스 생성자 호출
섹션 제목: “3. 기반 클래스 생성자 호출”public class Animal(string name){ public string Name { get; } = name;}
public class Dog(string name, string breed) : Animal(name){ public string Breed { get; } = breed;}4. DI 컨테이너 패턴
섹션 제목: “4. DI 컨테이너 패턴”// 기존 DI 패턴과 동일하게 동작public class UserController( IUserService userService, ILogger<UserController> logger, IMapper mapper){ [HttpGet("{id}")] public async Task<IActionResult> Get(int id) { logger.LogDebug("Fetching user {Id}", id); var user = await userService.GetByIdAsync(id); return user is null ? NotFound() : Ok(mapper.Map<UserDto>(user)); }}5. 구조체에서의 활용
섹션 제목: “5. 구조체에서의 활용”public struct Point(double x, double y){ public double X { get; } = x; public double Y { get; } = y; public double Distance => Math.Sqrt(X * X + Y * Y);}
var p = new Point(3, 4);Console.WriteLine(p.Distance); // 56. 주의사항 — 매개변수 변이
섹션 제목: “6. 주의사항 — 매개변수 변이”public class Counter(int start){ private int _count = start; // start를 캡처해 필드 초기화
public void Increment() => _count++;
// 주의: start는 여전히 접근 가능하지만 _count와 다를 수 있음 public int Initial => start; public int Current => _count;}매개변수를 여러 곳에서 사용하면 값이 분기될 수 있습니다. 혼동을 피하려면 필드에 저장 후 매개변수 참조를 제한하세요.
7. record vs class Primary Constructor
섹션 제목: “7. record vs class Primary Constructor”| 항목 | record | class |
|---|---|---|
| 매개변수 → 속성 자동 생성 | ✓ | ✗ (직접 선언) |
with 표현식 | ✓ | ✗ |
Equals/GetHashCode 자동 생성 | ✓ | ✗ |
| 불변성 기본 | ✓ | ✗ |
8. 입력 유효성 검사 패턴
섹션 제목: “8. 입력 유효성 검사 패턴”Primary Constructor에서 직접 유효성을 검사할 수 없지만 필드 초기화로 우회합니다.
public class DatabaseOptions(string connectionString, int maxPoolSize){ // 초기화 시점에 검증 private readonly string _connectionString = string.IsNullOrWhiteSpace(connectionString) ? throw new ArgumentException("Connection string required", nameof(connectionString)) : connectionString;
private readonly int _maxPoolSize = maxPoolSize is < 1 or > 100 ? throw new ArgumentOutOfRangeException(nameof(maxPoolSize), "1~100 범위 필요") : maxPoolSize;
public string ConnectionString => _connectionString; public int MaxPoolSize => _maxPoolSize;}9. init 전용 속성과 함께 사용
섹션 제목: “9. init 전용 속성과 함께 사용”public class OrderItem(string sku, int quantity){ // Primary Constructor 매개변수로 init 속성 초기화 public string Sku { get; init; } = sku; public int Quantity { get; init; } = quantity;
// 계산 속성 public decimal Total => Quantity * GetPrice(sku);}
// with 표현식은 record에서만 가능하지만// 명시적 init 속성으로 유사한 패턴 구현var item = new OrderItem("SKU-001", 5);var updated = item with { Quantity = 10 }; // ← record여야만 가능10. 추가 생성자(overload) 정의
섹션 제목: “10. 추가 생성자(overload) 정의”Primary Constructor와 함께 추가 생성자를 정의할 때는 this()로 위임합니다.
public class HttpClientOptions(string baseUrl, TimeSpan timeout){ // 기본 타임아웃을 가진 편의 생성자 public HttpClientOptions(string baseUrl) : this(baseUrl, TimeSpan.FromSeconds(30)) { }
public string BaseUrl { get; } = baseUrl; public TimeSpan Timeout { get; } = timeout;}11. 인터페이스 구현과 조합
섹션 제목: “11. 인터페이스 구현과 조합”public interface IRepository<T>{ Task<T?> FindAsync(int id); Task SaveAsync(T entity);}
// Primary Constructor + 인터페이스 구현public class ProductRepository(AppDbContext db, ILogger<ProductRepository> logger) : IRepository<Product>{ public async Task<Product?> FindAsync(int id) { logger.LogDebug("Finding product {Id}", id); return await db.Products.FindAsync(id); }
public async Task SaveAsync(Product product) { db.Products.Add(product); await db.SaveChangesAsync(); }}Primary Constructors는 DI 의존성 주입, 간단한 값 객체, 서비스 클래스에서 생성자 보일러플레이트를 크게 줄여줍니다. 단, 매개변수가 필드가 아님을 명심하고 필요한 경우 명시적으로 필드에 저장하세요. 유효성 검사는 필드 초기화식에서, 추가 생성자는 this() 위임으로, 기반 클래스 호출은 : Base(param) 문법으로 처리합니다.