기본 패턴
// class에 primary constructor 적용 (C# 12)
public class UserService(ILogger<UserService> logger, IUserRepository repo)
{
// 파라미터가 클래스 전체 스코프에서 사용 가능
public async Task<User?> FindAsync(int id)
{
logger.LogInformation("Finding user {Id}", id);
return await repo.FindByIdAsync(id);
}
}
// struct에도 동일하게 적용 가능
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);
}설명
보일러플레이트를 없애야 하는 이유 — 생성자 주입 반복 패턴
서비스 클래스에서 의존성을 주입받는 생성자는 늘 같은 패턴을 반복합니다. 필드 선언 → 생성자 파라미터 선언 → this.field = param 할당. 이 세 줄짜리 의식(ritual)이 의존성마다 반복되면 핵심 로직보다 배선 코드가 더 길어집니다.
Primary constructor는 클래스 선언부에 파라미터를 직접 표기하고, 그 파라미터가 클래스 본문 전체 스코프에서 살아있도록 컴파일러가 처리합니다. 별도의 필드 선언과 할당 없이도 메서드 안에서 파라미터를 그대로 참조할 수 있습니다.
// 이전 방식 — 같은 이름이 세 번 반복
public class OrderService
{
private readonly IOrderRepository _repo;
private readonly ILogger<OrderService> _logger;
public OrderService(IOrderRepository repo, ILogger<OrderService> logger)
{
_repo = repo;
_logger = logger;
}
public Task<Order?> GetAsync(int id) => _repo.FindAsync(id);
}
// primary constructor — 선언부에서 끝
public class OrderService(IOrderRepository repo, ILogger<OrderService> logger)
{
public Task<Order?> GetAsync(int id) => repo.FindAsync(id);
}파라미터 캡처 방식 — 파라미터는 필드가 아님
Primary constructor 파라미터는 자동으로 필드가 되지 않습니다. 컴파일러는 파라미터가 클래스 본문 어디에서 사용되는지 분석하고, 필요한 경우에만 내부적으로 캡처 필드(<paramName>)를 생성합니다. 이 캡처 필드는 소스 코드에 보이지 않습니다.
public class Calculator(int seed)
{
// seed를 여러 곳에서 참조 → 컴파일러가 캡처 필드 생성
public int Add(int value) => seed + value;
public int Multiply(int value) => seed * value;
}
// 주의: 파라미터를 필드처럼 외부에서 접근할 수 없음
var calc = new Calculator(10);
// calc.seed ← 컴파일 오류. 필드가 아님캡처 방식은 컴파일러가 결정하므로, 파라미터를 명시적 프로퍼티나 필드에 할당해두는 것이 더 명확한 의도 표현입니다.
public class Calculator(int seed)
{
// 외부 노출이 필요하면 명시적으로 프로퍼티 생성
public int Seed { get; } = seed;
public int Add(int value) => Seed + value;
}init-only setter, required members와의 조합
Primary constructor와 init-only 프로퍼티, required 키워드를 함께 쓰면 생성 시점 유효성을 컴파일러가 보장하는 불변 객체를 간결하게 만들 수 있습니다.
public class Config(string environment)
{
// primary constructor 파라미터로 init 프로퍼티 초기화
public string Environment { get; init; } = environment;
// required: 객체 초기화 시 반드시 설정해야 하는 프로퍼티
public required string ConnectionString { get; init; }
public required int MaxRetries { get; init; }
}
// 사용 — required 프로퍼티 누락 시 컴파일 오류
var cfg = new Config("production")
{
ConnectionString = "Server=...;",
MaxRetries = 3,
};record vs class — primary constructor의 동작 차이
record의 primary constructor는 자동으로 공개 init-only 프로퍼티를 생성합니다. 반면 class의 primary constructor는 파라미터만 노출하고 프로퍼티를 자동 생성하지 않습니다. 이 차이를 모르면 record처럼 동작할 것을 기대하다가 외부 접근 불가로 당황할 수 있습니다.
// record — 파라미터가 public init 프로퍼티로 자동 생성됨
public record UserRecord(string Name, int Age);
var r = new UserRecord("Mina", 25);
Console.WriteLine(r.Name); // ✅ "Mina" — 자동 생성된 프로퍼티
// class — 파라미터는 스코프 내에서만 접근 가능
public class UserClass(string name, int age)
{
public string Name { get; } = name; // 명시적으로 만들어야 외부 접근 가능
}
var c = new UserClass("Mina", 25);
Console.WriteLine(c.Name); // ✅ 명시적 프로퍼티
// c.name ← 컴파일 오류빠른 정리
| 상황 | 적합한 선택 |
|---|---|
| DI 서비스 클래스, 필드 공개 불필요 | primary constructor 파라미터 직접 사용 |
| 파라미터를 외부에 프로퍼티로 노출 | public T Prop { get; } = param; 명시 |
| 불변 데이터 전달 객체 (DTO) | record로 선언 — 프로퍼티 자동 생성 |
| 생성 시 필수 설정 강제 | required + init 프로퍼티 조합 |
| 추가 생성자 오버로드 필요 | this(...) 로 primary constructor 위임 |
주의할 점
Primary constructor 파라미터는 필드가 아닙니다. 직렬화(JSON, XML), 리플렉션, 외부 접근이 필요한 경우에는 반드시 명시적 프로퍼티나 필드를 선언해야 합니다.
// ❌ 파라미터를 직접 참조하면 외부에서 접근 불가
public class UserService(string connectionString)
{
// connectionString은 내부에서만 사용 가능
}
// ✅ 외부 노출이 필요하면 명시적으로 프로퍼티 선언
public class UserService(string connectionString)
{
public string ConnectionString { get; } = connectionString;
}또한 추가 생성자를 정의할 때는 반드시 this(...)로 primary constructor를 위임해야 합니다. 위임하지 않으면 컴파일 오류가 발생합니다.