[로봇활용_10주차] C# 속성(Property)

최윤호·2025년 10월 11일
post-thumbnail

속성: 캡슐화를 위한 문지기

C#에서 클래스 내의 데이터를 외부에서 접근해야 할 때가 많습니다.
가장 쉬운 방법은 변수를 public으로 열어두는 것이지만, 이건 마치
우리 집 대문을 활짝 열어두고 "아무나 들어오세요!" 하는 것과 같습니다.
데이터의 무결성을 지키기 어렵고, 클래스는 자신의 상태를 통제할 수 없게 되죠.
C#은 이런 문제를 해결하기 위해 속성(Property)이라는 강력한 기능을 제공합니다.
이번 글에서는 이 속성이 왜 필요한지, 그리고 어떻게 사용하는지 알아보겠습니다!

1)Public 필드의 위험성

먼저 속성이 왜 필요한지 알기위해 Person클래스를 만들고
나이(Age)를 public필드로 선언해 보겠습니다.

[코드]

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

class Program
{
    static void Main()
    {
        // 외부에서 필드에 직접 접근
        Person person = new Person();
        person.Age = -30; // 으악! 나이가 음수가 되어버렸어요!

        Console.WriteLine($"나이: {person.Age}");
    }
}

[실행 결과]

나이: -30

Agepublic으로 두었더니, 외부에서 아무런 제약 없이
-30이라는 말도 안 되는 값을 할당할 수 있습니다.
이것은 캡슐화(Encapsulation) 원칙이 깨진 상태입니다.

2)속성(Property)의 등장

속성은 데이터를 안전하게 제어하는 창구 역할을 합니다.
필드에 직접 접근하는 것을 막고, 속성이라는
'안전한 문'을 통해서만 접근하게 만드는 것입니다.

[코드]

public class Person
{
    // 1. 실제 데이터는 private 필드에 안전하게 보관 (backing field)
    private int _age;

    // 2. 외부에 공개되는 Age 속성 (public Property)
    public int Age
    {
        // get 접근자: 값을 읽어갈 때 호출됨
        get
        {
            return _age;
        }

        // set 접근자: 값을 할당할 때 호출됨
        set
        {
            // 'value'는 set 접근자에서만 사용할 수 있는 특별한 키워드입니다.
            // 할당하려는 값(예: person.Age = 30; 에서 30)을 의미합니다.
            if (value > 0) // 유효성 검사 로직!
            {
                _age = value;
            }
            else
            {
                Console.WriteLine("오류: 유효하지 않은 나이입니다.");
            }
        }
    }
}

class Program
{
    static void Main()
    {
        Person person = new Person();
        person.Age = 30;  // 내부적으로 set 접근자가 호출됨 -> 유효성 검사 통과
        Console.WriteLine($"나이: {person.Age}"); // 내부적으로 get 접근자가 호출됨

        person.Age = -10; // set 접근자 호출 -> 유효성 검사 실패
        Console.WriteLine($"나이: {person.Age}"); // get 접근자 호출 -> _age는 바뀌지 않음
    }
}

[실행 결과]

나이: 30
오류: 유효하지 않은 나이입니다.
나이: 30

3)자동 구현 속성

get, set내부에 별다른 로직이 없을 때 사용하는 일반적인 방법입니다.
컴파일러가 눈에 보이지 않는 private필드를 자동으로 만들어줍니다.

[사용법]

// 1. 자동 구현 속성
public string Name { get; set; }

// 2. 선언과 동시에 초기화 수행
public string Hobby { get; set; } = "영화보기";

// 3. init: 생성자나 초기화 시점에만 설정 가능
public int Age { get; init; } = 0;

4)읽기 전용 속성

속성을 읽을 수만 있고 쓸 수는 없게 만들고 싶을 때가 있습니다.

[사용법]

// 1. set 접근자 제거하기
public string Id { get; } // 생성자에서만 값을 할당할 수 있음

// 2. private set 사용하기
public int Point { get; private set; } // 클래스 내부에서만 값을 변경할 수 있음

5)쓰기 전용 속성

매우 드물게 사용되지만 쓰기만 할 수 있는 속성도 있습니다.
주로 비밀번호, PIN 번호 등 민감한 데이터를 처리할 때 사용합니다.

[코드]

public class Person
{
    // 실제 데이터를 저장하는 비밀 공간
    private string _name;

    // 쓰기 전용 속성
    public string Name
    {
        set { _name = value; }
    }

    // set만 있는 자동 구현 속성은 허용되지 않음
    // public string Name { set; } // 컴파일 오류 발생!

    public void ShowName()
    {
        // 클래스 내부에서는 비밀 공간(_name)에 직접 접근 가능
        Console.WriteLine($"비밀: 저의 이름은 {_name}입니다.");
    }
}

class Program
{
    static void Main()
    {
        Person person = new Person();
        person.Name = "김철수"; // 외부에서는 값을 쓰기만 할 수 있습니다.
        person.ShowName(); // 내부 메서드를 통해 값을 확인할 수 있습니다.

        // 값을 읽어오는 'get'이 없어서 오류가 발생합니다!
        // Console.WriteLine(person.Name); // 컴파일 오류 발생!
    }
}

[실행 결과]

비밀: 저의 이름은 김철수입니다.

6)init 접근자(C# 9.0)

init접근자는 객체를 생성할 때 단 한 번만 값을 할당할 수 있습니다.

[코드]

public class Transaction
{
    // 객체 초기화 시에만 값을 설정할 수 있음
    public Guid TransactionId { get; init; }
    public DateTime Timestamp { get; init; }

    public Transaction()
    {
        TransactionId = Guid.NewGuid();
        Timestamp = DateTime.Now;
    }
}

class Program
{
    static void Main()
    {
        var tx2 = new Transaction();
        // tx2.Timestamp = DateTime.Now; // 에러! 생성 후에는 변경할 수 없음

        // GUID는 전 세계적으로 고유한 값을 보장하는 128비트 기반의 식별자입니다.
        Console.WriteLine($"GUID: {tx2.TransactionId}");
        Console.WriteLine($"현재 시각: {tx2.Timestamp}");
    }
}

[실행 결과]

GUID: 7a1c5fcb-31b5-424e-3b5b-9b84g4510c72
현재 시각: 2025-08-11 오후 10:17:25

7)required 키워드(C# 11.0)

required키워드는 객체를 초기화할 때 반드시 값을 할당해야 합니다.
만약 해당 속성을 초기화하지 않으면 컴파일러가 친절하게 알려줍니다.
덕분에 우리는 실수를 조기에 발견하고 안정적인 코드를 작성할 수 있어요.

[코드]

class Person
{
    public required string Name { get; set; }
    public required int Age { get; set; }
}

class Program
{
    static void Main()
    {
        // 올바른 방식
        Person p2 = new Person
        {
            Name = "김철수",
            Age = 21
        };

        // 아래 코드는 컴파일 오류 발생
        // Person p1 = new Person();

        Console.WriteLine($"이름: {p2.Name}, 나이: {p2.Age}");
    }
}

[실행 결과]

이름: 김철수, 나이: 21

8)인터페이스 속성: 강제 구현

인터페이스에 정의된 속성은 실제 데이터를 저장하는 공간이 아닙니다.
오직 '속성을 제공할 수 있어야 한다!'라는 약속만 강제할 뿐입니다.

[인터페이스의 속성 정의]
인터페이스에서는 getset접근자 중 필요한 것만 명시하여,
구현 클래스가 어떤 접근자를 만들어야 하는지 지정할 수 있습니다.

public interface IProduct
{
    // 읽고 쓰기가 모두 가능한 속성을 만들어야 한다는 약속
    string Name { get; set; }

    // 읽기만 가능한 속성을 만들어야 한다는 약속
    int ProductId { get; }
}

[인터페이스의 속성 구현]
IProduct인터페이스를 구현하는 Book클래스는
NameProductId속성을 반드시 자신의 방식으로 구현해야 합니다.

public interface IProduct
{
    // 읽고 쓰기가 모두 가능한 속성을 만들어야 한다는 약속
    string Name { get; set; }

    // 읽기만 가능한 속성을 만들어야 한다는 약속
    int ProductId { get; }
}

public class Book : IProduct
{
    // 1. Name 속성 구현 (자동 구현 속성 사용)
    public string Name { get; set; }

    // 2. ProductId 속성 구현 (읽기 전용, private set 사용)
    //    생성자에서 초기화 후 내부에서만 변경 가능하게 구현
    public int ProductId { get; private set; }

    public Book(int id, string name)
    {
        this.ProductId = id;
        this.Name = name;
    }
}

// 사용 예시
class Program
{
    static void Main()
    {
        Book myBook = new Book(105, "C# Programming");
        Console.WriteLine($"Book ID: {myBook.ProductId}, Book Name: {myBook.Name}");
    }
}

[실행 결과]

Book ID: 105, Book Name: C# Programming

보시다시피, 인터페이스는 구현을 강요만 할 뿐, Book클래스가
자동 속성을 쓰든, private set을 쓰든 그 구현 방식에는 관여하지 않습니다.

9)추상 클래스 속성: 선택 구현

추상 클래스는 인터페이스와 달리, '완성된 속성''미완성된(추상) 속성'
모두 가질 수 있습니다. 자식 클래스에게 공통된 기반을 제공하면서도,
각자에게 맞는 부분은 직접 구현하도록 유도하는 유연한 방식입니다.

[추상 클래스 속성 정의]
자동차의 '미완성 설계도(Abstract Class)'를 생각해 봅시다.
설계도에는 모든 자동차가 공통적으로 갖는 바퀴 개수(NumberOfWheels)는
4개라고 미리 완성해서 그려둘 수 있습니다.
하지만 모델명(ModelName) 같은 속성은 빈칸으로 남겨두고
"이 설계도를 가져다 쓰는 사람이 알아서 채워 넣어!"라고 강제할 수 있죠.

public abstract class Vehicle
{
    // 1. 일반 속성 (구현 완료): 모든 자식 클래스가 그대로 물려받음
    public int NumberOfWheels { get; set; } = 4;

    // 2. 추상 속성 (미완성): 자식 클래스가 반드시 재정의(override) 해야 함
    public abstract string ModelName { get; }
}

[추상 클래스 속성 상속 및 구현하기]
Vehicle을 상속받는 Car클래스는 NumberOfWheels는 그대로 물려받고,
ModelName은 반드시 직접 구현해야 합니다.

public abstract class Vehicle
{
    // 1. 일반 속성 (구현 완료): 모든 자식 클래스가 그대로 물려받음
    public int NumberOfWheels { get; set; } = 4;

    // 2. 추상 속성 (미완성): 자식 클래스가 반드시 재정의(override) 해야 함
    public abstract string ModelName { get; }
}

public class Car : Vehicle
{
    private readonly string _modelName;

    // 2. 추상 속성 ModelName을 반드시 override하여 구현
    public override string ModelName
    {
        get { return _modelName; }
    }

    public Car(string modelName)
    {
        _modelName = modelName;
    }
}

// 사용 예시
class Program
{
    static void Main()
    {
        Car myCar = new Car("Supercar");

        // 부모에게 물려받은 일반 속성
        Console.WriteLine($"바퀴 개수: {myCar.NumberOfWheels}");
        // 자식이 직접 구현한 추상 속성
        Console.WriteLine($"차량 모델명: {myCar.ModelName}");
    }
}

[실행 결과]

바퀴 개수: 4
차량 모델명: Supercar

10)정리: 속성 한눈에 보기

지금까지 C#의 속성(Property)에 대해 자세히 알아보았습니다.
속성은 객체 지향의 핵심 원칙인 캡슐화를 지키는 매우 중요한 도구입니다.

구분핵심 내용
Public 필드필드를 public으로 선언 시 데이터 무결성이 깨질 위험이 있습니다.
속성(Property)get, set 접근자를 통해 데이터 접근을 제어하는 문지기 역할을 합니다.
자동 구현 속성별도의 로직이 없을 때 코드를 간결하게 만드는 가장 일반적인 속성입니다.
읽기 전용 속성외부에서 값을 변경할 수 없도록 제한하는 속성입니다.
쓰기 전용 속성외부에서 값을 읽을 수 없고 쓰기만 가능한 속성입니다. (드물게 사용)
init 접근자객체 생성 시점에 단 한 번만 값을 할당할 수 있는 속성입니다.
required 키워드필수 속성의 누락을 컴파일 시점에 방지하여 안정성을 높입니다.
인터페이스 속성구현 클래스가 특정 속성을 반드시 갖추도록 강제하는 약속입니다.
추상 클래스 속성완성된 속성과 미완성된(추상) 속성을 모두 가질 수 있습니다.
profile
🚀 미래의 엔지니어를 꿈꾸는 훈련생의 기록 📝

0개의 댓글