한 줄 요약: C# 기본 생성자(Primary Constructor)는 C# 12부터 클래스와 구조체의 선언부에 생성자 매개변수를 바로 쓸 수 있게 해 주는 문법입니다. 의존성 주입(DI) 코드의 필드 선언과 대입 보일러플레이트를 크게 줄여 주지만, 매개변수는 자동으로 속성이 되지 않는다는 점을 반드시 알아야 합니다.
C# 기본 생성자(Primary Constructor)란 무엇인가요?
C# 기본 생성자는 타입 이름 뒤 괄호에 매개변수 목록을 선언하면 해당 매개변수를 타입 본문 전체에서 사용할 수 있게 하는 C# 12 문법입니다. .NET 8과 함께 제공되며, 이전에는 record 타입에서만 가능했던 형태를 일반 class와 struct로 확장한 것입니다.
public class OrderService(ILogger<OrderService> logger, IOrderRepository repository)
{
public async Task<Order?> GetAsync(int id)
{
logger.LogInformation("주문 조회: {Id}", id);
return await repository.FindAsync(id);
}
}
위 코드에서 logger와 repository는 별도의 private 필드를 선언하지 않았는데도 메서드 안에서 바로 사용할 수 있습니다. 컴파일러가 매개변수를 사용하는 멤버가 있을 때 내부적으로 캡처해 보관하기 때문입니다.
기존 생성자 방식과 무엇이 다른가요?
기존 방식은 필드 선언, 생성자 선언, 대입의 세 단계가 필요했지만 기본 생성자는 선언부 한 줄로 끝납니다. 아래는 동일한 동작을 하는 두 코드를 비교한 것입니다.
// C# 11 이하: 필드 + 생성자 + 대입
public class OrderServiceOld
{
private readonly ILogger<OrderServiceOld> _logger;
private readonly IOrderRepository _repository;
public OrderServiceOld(ILogger<OrderServiceOld> logger, IOrderRepository repository)
{
_logger = logger;
_repository = repository;
}
}
// C# 12: 기본 생성자
public class OrderServiceNew(ILogger<OrderServiceNew> logger, IOrderRepository repository)
{
}
| 구분 | 기존 생성자 | 기본 생성자(C# 12) |
|---|---|---|
| 코드량 | 필드, 생성자, 대입 필요 | 선언부 한 줄 |
| readonly 보장 | 필드에 readonly 가능 | 매개변수는 readonly가 아님 |
| 속성 자동 생성 | 직접 작성 | class/struct는 생성 안 됨 (record만 생성) |
| 생성자 오버로드 | 자유롭게 추가 | 다른 생성자는 this(...)로 기본 생성자 호출 필요 |
기본 생성자 매개변수는 속성이 되나요?
아니요. class와 struct의 기본 생성자 매개변수는 속성이나 필드가 아니라, 타입 본문에서 접근할 수 있는 매개변수일 뿐입니다. record 타입만 위치 매개변수에 대해 public 속성을 자동으로 만들어 줍니다. 외부에 값을 노출하려면 속성을 직접 선언하고 매개변수로 초기화해야 합니다.
public class Product(string name, decimal price)
{
// 외부에 노출하려면 속성을 직접 선언
public string Name { get; } = name;
public decimal Price { get; } = price;
public string Display() => $"{Name}: {Price:N0}원";
}
var p = new Product("키보드", 59000m);
Console.WriteLine(p.Display()); // 키보드: 59,000원
// Console.WriteLine(p.name); // 컴파일 오류: 매개변수는 외부에서 접근 불가
매개변수를 속성 초기화에만 사용하면 컴파일러는 별도 캡처를 만들지 않습니다. 반대로 메서드 안에서 매개변수를 직접 쓰면 캡처되어 숨은 필드가 생깁니다.
기본 생성자는 언제 사용하면 좋을까요?
가장 효과적인 사용처는 의존성 주입(DI)으로 받은 서비스를 메서드에서 읽기만 하는 경우입니다. ASP.NET Core의 컨트롤러, 서비스, 리포지토리 클래스가 대표적입니다.
1. ASP.NET Core 컨트롤러에서 의존성 주입
[ApiController]
[Route("api/[controller]")]
public class OrdersController(IOrderService orderService) : ControllerBase
{
[HttpGet("{id:int}")]
public async Task<IActionResult> Get(int id)
{
var order = await orderService.GetAsync(id);
return order is null ? NotFound() : Ok(order);
}
}
2. 상속 시 기반 클래스 생성자 호출
파생 클래스의 기본 생성자 매개변수를 기반 클래스 생성자에 그대로 전달할 수 있습니다.
public class Animal(string name)
{
public string Name { get; } = name;
}
public class Dog(string name, string breed) : Animal(name)
{
public string Breed { get; } = breed;
}
3. 매개변수 값 검증 (Guard)
필드 초기화 식에서 매개변수를 검증하면 잘못된 값이 들어왔을 때 즉시 예외를 던질 수 있습니다.
public class Connection(string connectionString)
{
private readonly string _connectionString =
!string.IsNullOrWhiteSpace(connectionString)
? connectionString
: throw new ArgumentException("연결 문자열이 필요합니다.", nameof(connectionString));
}
기본 생성자 사용 시 주의할 점은 무엇인가요?
기본 생성자 매개변수는 readonly가 아니므로 본문에서 값을 변경할 수 있다는 점이 가장 큰 함정입니다. 의도치 않은 재할당이 생길 수 있어 불변성이 중요하다면 필드나 속성으로 한 번 옮겨 담는 방식을 권장합니다.
public class Counter(int start)
{
public int Next() => start++; // 매개변수가 변경됨: 컴파일은 되지만 상태가 바뀝니다.
}
var c = new Counter(10);
Console.WriteLine(c.Next()); // 10
Console.WriteLine(c.Next()); // 11
주의할 점을 정리하면 다음과 같습니다.
- 매개변수는 readonly가 아니므로 본문에서 재할당될 수 있습니다.
- 매개변수를 초기화에도 쓰고 멤버에서도 직접 쓰면 이중 저장이 되어 컴파일러가 경고(CS9124)를 낼 수 있습니다. 한 방식으로 통일하세요.
- 기본 생성자를 선언한 타입에 다른 생성자를 추가하려면
: this(...)로 기본 생성자를 호출해야 합니다. - struct는 매개변수가 캡처되어 상태가 복사될 수 있으므로 값 의미를 반드시 확인해야 합니다.
- 클래스에서 외부 노출용 값이 필요하면 속성을 직접 선언해야 합니다. record가 자동으로 만들어 주는 것과 혼동하지 마세요.
record의 위치 매개변수와 기본 생성자는 어떻게 다른가요?
record는 위치 매개변수마다 public 속성(record class는 init 전용)을 자동 생성하고 값 기반 동등성까지 제공하지만, 일반 class의 기본 생성자는 그런 기능이 없습니다. 데이터 전달 객체에는 record, 서비스 로직과 DI에는 기본 생성자가 어울립니다. record의 불변 객체 활용은 C# record 타입과 with 식 글에서 확인할 수 있습니다.
public record ProductDto(string Name, decimal Price); // Name, Price 속성 자동 생성
public class ProductService(IProductRepository repo) // repo는 속성이 아님
{
public Task<ProductDto?> FindAsync(int id) => repo.FindDtoAsync(id);
}
자주 묻는 질문 (FAQ)
Q. C# 기본 생성자는 어느 버전부터 사용할 수 있나요?
A. C# 12부터이며 .NET 8 SDK(Visual Studio 2022 17.8 이상)에서 사용할 수 있습니다. 클래스와 구조체 모두 지원합니다.
Q. 기본 생성자 매개변수에 readonly를 붙일 수 있나요?
A. 붙일 수 없습니다. 불변이 필요하면 private readonly 필드나 get 전용 속성에 값을 복사해 두세요.
Q. 기본 생성자와 일반 생성자를 함께 쓸 수 있나요?
A. 가능합니다. 단, 추가한 생성자는 this(...)로 기본 생성자를 호출해야 합니다.
Q. ASP.NET Core의 DI 컨테이너에서 기본 생성자 클래스도 잘 동작하나요?
A. 네. 기본 생성자도 결국 public 생성자이므로 DI 컨테이너가 매개변수 타입을 해석해 주입합니다.
Q. 기본 생성자 매개변수와 같은 이름의 멤버를 만들면 어떻게 되나요?
A. 멤버가 매개변수를 가리므로 이름 충돌에 주의해야 합니다. 보통 매개변수는 camelCase, 속성은 PascalCase를 써서 구분합니다.
정리
- 기본 생성자는 C# 12(.NET 8)에서 class와 struct에 도입된 문법입니다.
- 매개변수는 본문 전체에서 쓸 수 있지만 자동으로 속성이 되지는 않습니다(record 제외).
- DI 서비스 주입처럼 읽기 전용으로 쓰는 경우에 가장 효과적입니다.
- 매개변수는 readonly가 아니므로 불변이 필요하면 필드나 속성에 복사하세요.
공식 문서: Microsoft Learn - C# 12 기본 생성자, Microsoft Learn - 인스턴스 생성자
'C#' 카테고리의 다른 글
| C# IAsyncEnumerable 사용법과 await foreach 정리 (0) | 2026.10.04 |
|---|---|
| C# Task.WhenAll 사용법: 비동기 병렬 처리와 Parallel.ForEachAsync 비교 (1) | 2026.10.04 |
| C# 컬렉션 식 사용법: C# 12 대괄호 문법과 spread 연산자 정리 (0) | 2026.10.02 |
| C# record 타입과 with 식으로 불변 객체 쉽게 다루기 (0) | 2026.10.02 |
| C# 테스트 더블링: NSubstitute와 Moq 심화 사용법 (0) | 2026.10.01 |