Properties

Properties

properties
Published

April 8, 2025

Abstract

프로퍼티(Property)는 클래스, 구조체, 인터페이스에서 데이터 접근을 제어하고 캡슐화를 구현하는 데 사용되는 멤버입니다. 프로퍼티는 필드처럼 보이지만 실제로는 접근자(Accessor)라고 불리는 특수한 메소드(get 접근자와 set 접근자)를 통해 필드의 값을 읽고 쓰는 방식을 제공합니다. 이를 통해 데이터 접근에 대한 유연성과 제어력을 높일 수 있습니다.

public 필드의 유혹

클래스의 필드를 public 으로 선언하면 외부에서 직접 필드에 접근하여 값을 읽거나 수정할 수 있습니다. 이는 간단해 보이지만 다음과 같은 문제점을 야기할 수 있습니다.

  • 캡슐화 위반: 클래스 내부의 데이터를 외부에서 직접 변경할 수 있으므로, 데이터의 무결성을 보장하기 어려움
  • 제어 부족: 필드에 접근하거나 값을 수정할 때 어떠한 유효성 검사나 부가적인 로직을 수행할 수 없음
  • 유지보수 어려움: 내부 필드의 이름을 변경하거나 데이터 저장 방식을 변경할 경우, 해당 필드를 사용하는 모든 외부 코드를 수정해야 할 수 있음

아래 예제의 Person 클래스의 age 필드는 public 이므로 외부에서 음수와 같은 유효하지 않은 값을 직접 할당할 수 있습니다.

public class Person
{
    public int age; // public 필드
}

Person person = new Person();
person.age = -5; // 유효하지 않은 값 할당 가능
Console.WriteLine($"나이: {person.age}");

메소드보다 프로퍼티

public 필드의 문제점을 해결하기 위해 전통적으로 get 메소드와 set 메소드를 사용하여 필드에 접근하는 방식을 사용했습니다. 하지만 C#에서는 프로퍼티를 통해 더 간결하고 직관적인 방식으로 데이터 접근을 제어할 수 있습니다. 프로퍼티는 필드처럼 사용되지만, 실제로는 get 접근자와 set 접근자를 통해 내부 필드의 값을 읽고 씁니다. 이를 통해 필드 접근에 대한 제어를 유지하면서도 편리한 사용성을 제공합니다.

아래 예제에서 Age 프로퍼티는 set 접근자 내에서 값의 유효성을 검사하여 데이터의 무결성을 보호하고 있습니다. 외부에서는 person.Age 와 같이 필드처럼 프로퍼티에 접근하지만, 실제로는 get 또는 set 접근자가 실행됩니다.

public class Person
{
    private int _age; // private 필드

    // 프로퍼티
    public int Age
    {
        get { return _age; }
        set
        {
            if (value >= 0 && value <= 150)
            {
                _age = value;
            }
            else
            {
                Console.WriteLine("나이는 0세에서 150세 사이의 값이어야 합니다.");
            }
        }
    }
}

Person person = new Person();
person.Age = -5; // set 접근자를 통해 유효성 검사
person.Age = 30;
Console.WriteLine($"나이: {person.Age}"); // get 접근자를 통해 값 읽기

자동 구현 프로퍼티

단순히 private 필드의 값을 읽고 쓰는 역할만 하는 프로퍼티의 경우, C#은 자동 구현 프로퍼티 (Auto-Implemented Property) 라는 더 간결한 문법을 제공합니다. 자동 구현 프로퍼티는 컴파일러가 자동으로 backing 필드를 생성하고 getset 접근자를 구현해줍니다.

아래 예제에서 NameAge 프로퍼티는 자동 구현 프로퍼티로 선언되었습니다. 컴파일러는 자동으로 이 프로퍼티들을 위한 private backing 필드를 생성하고 기본적인 getset 로직을 제공합니다.

public class Person
{
    public string Name { get; set; } // 자동 구현 프로퍼티
    public int Age { get; set; }    // 자동 구현 프로퍼티
}

Person person = new Person();
person.Name = "홍길동";
person.Age = 30;
Console.WriteLine($"이름: {person.Name}, 나이: {person.Age}");

프로퍼티와 생성자

생성자는 객체가 생성될 때 프로퍼티의 초기값을 설정하는 데 유용하게 사용됩니다. 생성자를 통해 객체 생성 시 필요한 데이터를 강제하고, 객체의 초기 상태를 올바르게 설정할 수 있습니다.

public class Car
{
    public string Model { get; set; }
    public string Color { get; set; }

    // 생성자를 통해 프로퍼티 초기화
    public Car(string model, string color)
    {
        Model = model;
        Color = color;
    }
}

Car myCar = new Car("소나타", "흰색");
Console.WriteLine($"모델: {myCar.Model}, 색상: {myCar.Color}");

초기화 전용 자동 구현 프로퍼티

C# 9.0부터는 초기화 전용 자동 구현 프로퍼티 (Init-Only Auto-Implemented Property) 를 사용하여 객체 초기화 시에만 값을 할당할 수 있고, 그 이후에는 읽기 전용으로 만들 수 있습니다. 이는 set 접근자 대신 init 접근자를 사용하여 정의합니다.

public class Book
{
    public string Title { get; init; } // 초기화 전용 자동 구현 프로퍼티
    public string Author { get; init; }

    public Book(string title, string author)
    {
        Title = title;
        Author = author;
    }

    public Book() { } // 기본 생성자도 허용
}

Book book1 = new Book { Title = "어린 왕자", Author = "생텍쥐페리" };
Console.WriteLine($"제목: {book1.Title}, 저자: {book1.Author}");

Book book2 = new Book("데미안", "헤르만 헤세"); // 생성자를 통한 초기화
// book1.Title = "새로운 제목"; // 컴파일 에러! 초기화 이후 값 변경 불가

프로퍼티 초기화를 강제하는 required 키워드

C# 11부터 도입된 required 키워드를 사용하면 특정 프로퍼티가 객체 초기화 시 반드시 값이 할당되어야 함을 명시할 수 있습니다. required 키워드는 자동 구현 프로퍼티에 적용할 수 있으며, 객체를 생성할 때 해당 프로퍼티를 초기화하지 않으면 컴파일러 오류가 발생합니다.

public class Product
{
    public required string Name { get; set; }
    public int Price { get; set; }
}

Product product1 = new Product { Name = "노트북", Price = 1000000 }; // Name 초기화 필수
// Product product2 = new Product { Price = 50000 }; // 컴파일 에러! Name이 초기화되지 않음
Console.WriteLine($"제품명: {product1.Name}, 가격: {product1.Price}");

레코드 형식으로 만드는 불변 객체

C# 9.0부터 도입된 레코드(Record) 형식은 불변(Immutable) 객체를 쉽게 만들 수 있도록 설계된 참조 형식입니다. 레코드는 값 기반 비교, 간결한 구문, with 표현식을 통한 비파괴적 복사 등의 기능을 기본적으로 제공합니다.

레코드 선언하기

레코드는 record 키워드를 사용하여 선언합니다. 속성을 선언하는 방식에 따라 위치 기반 레코드와 속성 기반 레코드로 나눌 수 있습니다.

  • 위치 기반 레코드: 생성자의 매개변수 목록을 통해 속성을 간결하게 선언, 컴파일러는 자동으로 해당 속성에 대한 public init-only 프로퍼티를 생성
public record PersonRecord(string FirstName, string LastName, int Age);

PersonRecord person1 = new PersonRecord("홍", "길동", 30);
Console.WriteLine($"이름: {person1.FirstName} {person1.LastName}, 나이: {person1.Age}");
  • 속성 기반 레코드: 클래스와 유사하게 속성을 명시적으로 선언
public record ProductRecord
{
    public string Name { get; init; }
    public decimal Price { get; init; }
}

ProductRecord product1 = new ProductRecord { Name = "마우스", Price = 25000 };
Console.WriteLine($"제품명: {product1.Name}, 가격: {product1.Price}");

with를 이용한 레코드 복사

레코드는 with 표현식을 사용하여 기존 레코드의 속성 값을 기반으로 새로운 레코드 인스턴스를 생성할 수 있습니다. 이때, 원하는 속성 값만 변경하고 나머지는 기존 값을 그대로 유지하는 비파괴적 복사가 이루어집니다.

public record PersonRecord(string FirstName, string LastName, int Age);

PersonRecord person1 = new PersonRecord("홍", "길동", 30);
PersonRecord person2 = person1 with { Age = 31 }; // Age만 변경된 새로운 레코드 생성

Console.WriteLine($"person1 나이: {person1.Age}"); // 출력: person1 나이: 30
Console.WriteLine($"person2 나이: {person2.Age}"); // 출력: person2 나이: 31

레코드 객체 비교하기

레코드는 값 기반 비교를 기본적으로 지원합니다. 즉, 두 레코드 인스턴스의 모든 속성 값이 동일하면 두 객체는 같은 것으로 간주됩니다.

public record PersonRecord(string FirstName, string LastName, int Age);

PersonRecord person1 = new PersonRecord("홍", "길동", 30);
PersonRecord person2 = new PersonRecord("홍", "길동", 30);
PersonRecord person3 = new PersonRecord("김", "민수", 25);

Console.WriteLine($"person1 == person2: {person1 == person2}"); // 출력: person1 == person2: True
Console.WriteLine($"person1 == person3: {person1 == person3}"); // 출력: person1 == person3: False

무명 형식

무명 형식(Anonymous Type) 은 클래스나 구조체를 명시적으로 정의하지 않고도 간단한 읽기 전용 객체를 만들 수 있는 기능입니다. 무명 형식은 new 키워드와 객체 이니셜라이저 구문을 사용하여 생성됩니다. 컴파일러는 자동으로 무명 형식의 이름과 속성을 추론합니다.

var person = new { Name = "이순신", Age = 45 };
Console.WriteLine($"이름: {person.Name}, 나이: {person.Age}");

var product = new { ProductName = "키보드", Price = 50000 };
Console.WriteLine($"제품명: {product.ProductName}, 가격: {product.Price}");

// 무명 형식의 배열
var items  = new[]
{
    new { ItemName = "사과", Price = 1000 },
    new { ItemName = "바나나", Price = 1500 }
};

foreach (var item in items)
{
    Console.WriteLine($"상품명: {item.ItemName}, 가격: {item.Price}");
}

인터페이스의 프로퍼티

인터페이스는 클래스가 구현해야 하는 멤버의 계약을 정의하며, 프로퍼티도 인터페이스의 멤버로 포함될 수 있습니다. 인터페이스에 정의된 프로퍼티는 get 접근자 및/또는 set 접근자를 선언하지만, 실제 구현은 인터페이스를 구현하는 클래스에서 제공해야 합니다.

public interface IHasName
{
    string Name { get; set; }
}

public class Employee : IHasName
{
    public string Name { get; set; } // 인터페이스 프로퍼티 구현
    public int EmployeeId { get; set; }
}

public class Customer : IHasName
{
    public string Name { get; set; } // 인터페이스 프로퍼티 구현
    public string ContactNumber { get; set; }
}

Employee employee = new Employee { Name = "박철수", EmployeeId = 123 };
Customer customer = new Customer { Name = "김영희", ContactNumber = "010-1234-5678" };

PrintName(employee); // 출력: 이름: 박철수
PrintName(customer); // 출력: 이름: 김영희

static void PrintName(IHasName namedObject)
{
    Console.WriteLine($"이름: {namedObject.Name}");
}

추상 클래스의 프로퍼티

추상 클래스는 일반적인 멤버와 함께 추상 멤버를 포함할 수 있으며, 프로퍼티도 추상 멤버로 선언될 수 있습니다. 추상 프로퍼티는 abstract 키워드를 사용하여 선언되며, 실제 구현은 추상 클래스를 상속받는 파생 클래스에서 override 키워드를 사용하여 제공해야 합니다. 추상 클래스는 virtual 프로퍼티를 가질 수도 있으며, 이는 파생 클래스에서 선택적으로 재정의할 수 있습니다.

public abstract class Shape
{
    public abstract double Area { get; } // 추상 프로퍼티 (get 접근자만 정의)

    public virtual string Color { get; set; } = "검정"; // virtual 프로퍼티 (기본 값 제공)

    public abstract void DisplayArea(); // 추상 메소드
}

public class Circle : Shape
{
    public double Radius { get; set; }

    public Circle(double radius)
    {
        Radius = radius;
    }

    public override double Area => Math.PI * Radius * Radius; // 추상 프로퍼티 구현

    public override void DisplayArea()
    {
        Console.WriteLine($"원 면적: {Area}");
    }

    public override string Color { get => base.Color; set => base.Color = value; } // virtual 프로퍼티 재정의 (선택 사항)
}

public class Rectangle : Shape
{
    public double Width { get; set; }
    public double Height { get; set; }

    public Rectangle(double width, double height)
    {
        Width = width;
        Height = height;
    }

    public override double Area => Width * Height; // 추상 프로퍼티 구현

    public override void DisplayArea()
    {
        Console.WriteLine($"사각형 면적: {Area}");
    }
}

Circle circle = new Circle(5);
Rectangle rectangle = new Rectangle(10, 5);

circle.DisplayArea(); // 출력: 원 면적: 78.53981633974483
Console.WriteLine($"원 색상: {circle.Color}"); // 출력: 원 색상: 검정 (기본 값)
circle.Color = "빨강";
Console.WriteLine($"원 색상 변경 후: {circle.Color}"); // 출력: 원 색상 변경 후: 빨강

rectangle.DisplayArea(); // 출력: 사각형 면적: 50
Console.WriteLine($"사각형 색상: {rectangle.Color}"); // 출력: 사각형 색상: 검정 (기본 값)