Menu

C# 속성(프로퍼티): get, set, 자동 속성, init, required

C# 속성이 동작하는 방식: 지원 필드 위의 get과 set 접근자, 자동 구현 속성, private set과 get 전용 속성, 계산 속성, setter에서의 검증, 그리고 C# 9와 11의 init과 required 키워드를 알아봅니다.

이 페이지에는 실행 가능한 에디터가 있습니다 - 편집하고 실행하면 결과를 바로 볼 수 있습니다.

속성은 사용하는 코드에게는 필드처럼 보이지만(order.Total, order.Total = 5) 실제로는 메서드 쌍입니다. 값을 반환하는 get 접근자와 값을 받는 set 접근자입니다. 이 간접 단계 덕분에 클래스는 호출하는 쪽의 사용법을 바꾸지 않고 값을 검사하거나, 계산하거나, 쓰기를 거부할 수 있습니다.

필드와 속성

public 필드는 객체의 저장소에 대한 직접 접근을 내줍니다. 누구나 무엇이든 넣을 수 있습니다:

public class Account
{
    public decimal Balance;   // any code can write -500 here
}

속성은 호출하는 쪽과 저장소 사이에 코드를 둡니다. 다음은 직접 작성한 완전한 형태로, private 지원 필드와 두 접근자를 가진 속성입니다. set 안에서 value 키워드는 대입되는 값입니다.

출력:

250
Rejected a negative balance
250

검증 말고도 속성이 중요한 이유는 .NET 생태계 대부분이 속성으로 일하고 필드는 무시하기 때문입니다. 인터페이스는 속성은 선언할 수 있지만 인스턴스 필드는 선언할 수 없고, WPF와 MAUI의 데이터 바인딩은 속성에 바인딩하며, System.Text.Json은 기본적으로 public 속성을 직렬화하고 필드는 건너뜁니다.

자동 속성

접근자가 필드를 읽고 쓰기만 할 거라면 컴파일러가 작성하게 하세요. { get; set; }은 자동 구현 속성으로, 컴파일러가 숨겨진 지원 필드를 대신 생성합니다.

출력:

Notebook: 9.99, 10 left

필드처럼 보이는데 왜 굳이 쓸까요? public 필드를 나중에 속성으로 바꾸면 그것을 쓰는 컴파일된 코드(그리고 ref와 out 인수)에 호환성이 깨지는 변경이지만, 자동 속성을 검증이 있는 완전한 속성으로 바꾸는 것은 호출하는 쪽에 보이지 않기 때문입니다. { get; set; }으로 시작하면 그 선택지를 공짜로 열어 둘 수 있습니다.

private set과 get 전용 속성

대부분의 상태는 모두가 읽을 수 있고 객체 자신만 바꿀 수 있어야 합니다. setter에 더 좁은 접근 한정자를 주세요:

출력:

Ines: 2 items, total 15.75

여기에는 세 종류의 읽기 전용이 있습니다:

  • { get; private set; }: 클래스는 언제든 바꿀 수 있고, 외부 코드는 바꿀 수 없습니다.
  • { get; }(get 전용 자동 속성, C# 6): 생성자나 이니셜라이저에서만 대입할 수 있습니다. 생성이 끝나면 클래스 자신을 포함해 아무도 바꿀 수 없습니다. 불변 속성을 만드는 방법입니다.
  • => expression(계산 속성): 저장소가 전혀 없습니다. 읽을 때마다 식이 실행되므로 Total은 항상 리스트와 일치합니다.

호출하는 쪽은 루프 안에서, 디버거에서 속성을 가볍게 읽으므로 계산 속성은 비용이 작고 부수 효과가 없어야 합니다. 값을 얻는 데 실제 작업(데이터베이스 쿼리, 큰 계산)이 든다면 비용이 보이도록 CalculateTotal() 같은 메서드로 만드세요.

식 본문 접근자

한 줄짜리 접근자를 가진 완전한 속성은 접근자마다 =>를 쓸 수 있습니다:

private string title;

public string Title
{
    get => title;
    set => title = value?.Trim() ?? "";
}

public decimal Total => SumPrices();는 getter만 있는 속성의 축약형이고, get => ...은 setter도 있는 속성 안에서의 같은 개념입니다.

getter와 setter 로직: 알림과 지연 값

접근자는 메서드이므로 값을 저장하는 것 이상을 할 수 있습니다. 흔한 패턴 두 가지는 setter에서 변경 알림을 발생시키는 것과, 처음 읽을 때 값을 계산해 캐시하는 것입니다.

출력:

(building summary)
Theme: light
Theme: light
Theme changed
(building summary)
Theme: dark

Changed는 이벤트이며, 이 setter 패턴이 WPF와 MAUI의 INotifyPropertyChanged 구현의 모습입니다. nameof(Theme)는 속성 이름이 바뀌어도 문자열이 맞춰지게 해 줍니다.

무한 재귀 실수

가장 흔한 속성 버그는 지원 필드 대신 속성 자신에 대입하는 setter입니다:

public string Name
{
    get { return Name; }        // calls get again, forever
    set { Name = value; }       // calls set again, forever
}

각 접근자가 자기 자신을 호출하므로 프로그램은 잡을 수 없는 StackOverflowException으로 죽습니다. 해결책은 별도의 필드(소문자 name)를 읽고 쓰거나 자동 속성을 쓰는 것입니다. C# 14는 바로 이 경우를 위해 field 키워드를 추가했습니다. 접근자 안에서 field는 컴파일러가 생성한 지원 필드를 가리키므로, 필드를 선언하지 않고도 set => field = value.Trim();이 동작합니다.

init 접근자(C# 9)

get 전용 속성은 모든 값을 생성자를 통해 넘기도록 강제합니다. C# 9는 객체 생성 중, 즉 생성자나 개체 이니셜라이저 안에서만 허용되는 setter인 init을 추가했습니다.

public class Product
{
    public string Sku { get; init; }
    public decimal Price { get; init; }
}

var p = new Product { Sku = "MUG-01", Price = 8.50m };   // fine: during creation
p.Price = 4m;   // error CS8852: Init-only property or indexer 'Product.Price' can only be assigned
                // in an object initializer, or on 'this' or 'base' in an instance constructor or an 'init' accessor

결과는 이니셜라이저 문법을 쓰는 불변 객체이며, 레코드 클래스가 위치 속성에 쓰는 것이 바로 이것입니다. 이런 객체를 "바꾸려면" 새 객체를 만드세요. 레코드는 with로 이를 한 줄로 해 줍니다.

C# 7에서 가장 가까운 대응은 get 전용 속성과 생성자 매개변수입니다:

public class Product
{
    public string Sku { get; }
    public decimal Price { get; }
    public Product(string sku, decimal price) { Sku = sku; Price = price; }
}

required 멤버(C# 11)

개체 이니셜라이저는 호출하는 쪽이 무언가를 설정하도록 강제할 수 없습니다. Sku가 절대 비어 있으면 안 되더라도 new Product { }는 컴파일됩니다. C# 11은 required 한정자를 추가했습니다:

public class User
{
    public required string Email { get; init; }
    public string DisplayName { get; init; } = "";
}

var ok = new User { Email = "ana@example.com" };
var bad = new User { DisplayName = "Ana" };   // error CS9035: Required member 'User.Email'
                                              // must be set in the object initializer or attribute constructor.

required는 set과 init 모두와 함께 쓸 수 있습니다. required 멤버를 직접 설정하는 생성자는 [SetsRequiredMembers] 특성으로 그렇다고 알릴 수 있으며, 그러면 그 생성자를 쓰는 호출하는 쪽은 다시 설정하도록 요구받지 않습니다.

흔한 실수

  • 접근자 안의 재귀. set { Name = value; }는 자기 자신을 호출합니다. 지원 필드나 자동 속성을 쓰세요.
  • 비싸거나 부수 효과가 있는 getter. 읽는 사람은 속성 읽기가 빠르고 반복해도 안전하리라 기대합니다. 작업에는 메서드를 쓰세요.
  • "일단" public 필드 쓰기. 필드를 나중에 속성으로 바꾸면 바이너리 호환성이 깨지고, 직렬 변환기는 기본적으로 필드를 무시합니다. 자동 속성으로 시작하세요.
  • 모든 것에 public setter. 잔액이나 상태에 { get; set; }을 쓰면 잘못된 상태를 부릅니다. private set, init, get 전용을 쓰고, 상태는 메서드로 바꾸세요.
  • get 전용 속성에서 변경 가능한 컬렉션 반환하기. public List<string> Tags { get; }는 호출하는 쪽이 리스트를 교체하는 것만 막고 추가하는 것은 막지 못합니다. 밖에서 컬렉션이 바뀌면 안 된다면 IReadOnlyList<string>을 반환하세요.

자주 묻는 질문

C#에서 { get; set; }은 무슨 뜻인가요?

자동 구현 속성을 선언합니다. 컴파일러가 숨겨진 private 필드와 그것을 반환하는 get 접근자, 그것에 대입하는 set 접근자를 만듭니다. public string Name { get; set; }은 호출하는 쪽에서는 public 필드처럼 동작하지만 실제로는 메서드 쌍이므로, 나중에 사용하는 코드를 바꾸지 않고 검증을 추가하거나 private set으로 바꿀 수 있습니다.

C#에서 필드와 속성의 차이는 무엇인가요?

필드는 객체에 저장되는 변수입니다. 속성은 밖에서 보면 필드처럼 생긴 메서드 쌍(get과 set)입니다. 속성을 쓰면 클래스가 값을 검증하고, 결과를 계산하고, 쓰기를 제한할 수 있으며, 데이터 바인딩, 직렬 변환기, 인터페이스가 다루는 것도 속성입니다. public 데이터는 속성으로 노출하고, 필드는 보통 private으로 둡니다.

C#의 private set은 무엇인가요?

public int Stock { get; private set; }은 모든 코드가 속성을 읽을 수 있게 하되, 클래스 안의 코드만 바꿀 수 있게 합니다. 클래스가 잔액이나 개수처럼 자신이 소유한 상태를 노출하면서, 모든 변경을 규칙을 강제하는 메서드 뒤에 두는 방법입니다.

C#에서 init은 무엇을 하나요?

init(C# 9)은 객체가 만들어지는 동안, 즉 생성자나 개체 이니셜라이저 안에서만 동작하는 setter입니다. public string Sku { get; init; }은 new Product { Sku = "A-1" }은 허용하지만 그 뒤의 product.Sku = "B-2"는 CS8852 오류로 거부합니다. 불변 객체에 이니셜라이저 문법을 줍니다.

C#의 required 속성이란 무엇인가요?

required(C# 11)로 표시한 속성은 그 타입을 만드는 모든 개체 이니셜라이저에서 반드시 설정해야 하며, 빠뜨리면 컴파일 오류(CS9035)입니다. init과 잘 어울립니다. public required string Email { get; init; }은 생성 시 반드시 제공되어야 하고 나중에 바꿀 수 없습니다.

Coddy programming languages illustration

Coddy로 코딩 배우기

시작하기