
C#에서 클래스 내의 데이터를 외부에서 접근해야 할 때가 많습니다.
가장 쉬운 방법은 변수를 public으로 열어두는 것이지만, 이건 마치
우리 집 대문을 활짝 열어두고 "아무나 들어오세요!" 하는 것과 같습니다.
데이터의 무결성을 지키기 어렵고, 클래스는 자신의 상태를 통제할 수 없게 되죠.
C#은 이런 문제를 해결하기 위해 속성(Property)이라는 강력한 기능을 제공합니다.
이번 글에서는 이 속성이 왜 필요한지, 그리고 어떻게 사용하는지 알아보겠습니다!
먼저 속성이 왜 필요한지 알기위해 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
Age를 public으로 두었더니, 외부에서 아무런 제약 없이
-30이라는 말도 안 되는 값을 할당할 수 있습니다.
이것은 캡슐화(Encapsulation) 원칙이 깨진 상태입니다.
속성은 데이터를 안전하게 제어하는 창구 역할을 합니다.
필드에 직접 접근하는 것을 막고, 속성이라는
'안전한 문'을 통해서만 접근하게 만드는 것입니다.
[코드]
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
get, set내부에 별다른 로직이 없을 때 사용하는 일반적인 방법입니다.
컴파일러가 눈에 보이지 않는 private필드를 자동으로 만들어줍니다.
[사용법]
// 1. 자동 구현 속성
public string Name { get; set; }
// 2. 선언과 동시에 초기화 수행
public string Hobby { get; set; } = "영화보기";
// 3. init: 생성자나 초기화 시점에만 설정 가능
public int Age { get; init; } = 0;
속성을 읽을 수만 있고 쓸 수는 없게 만들고 싶을 때가 있습니다.
[사용법]
// 1. set 접근자 제거하기
public string Id { get; } // 생성자에서만 값을 할당할 수 있음
// 2. private set 사용하기
public int Point { get; private set; } // 클래스 내부에서만 값을 변경할 수 있음
매우 드물게 사용되지만 쓰기만 할 수 있는 속성도 있습니다.
주로 비밀번호, 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); // 컴파일 오류 발생!
}
}
[실행 결과]
비밀: 저의 이름은 김철수입니다.
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
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
인터페이스에 정의된 속성은 실제 데이터를 저장하는 공간이 아닙니다.
오직 '속성을 제공할 수 있어야 한다!'라는 약속만 강제할 뿐입니다.
[인터페이스의 속성 정의]
인터페이스에서는 get과 set접근자 중 필요한 것만 명시하여,
구현 클래스가 어떤 접근자를 만들어야 하는지 지정할 수 있습니다.
public interface IProduct
{
// 읽고 쓰기가 모두 가능한 속성을 만들어야 한다는 약속
string Name { get; set; }
// 읽기만 가능한 속성을 만들어야 한다는 약속
int ProductId { get; }
}
[인터페이스의 속성 구현]
IProduct인터페이스를 구현하는 Book클래스는
Name과 ProductId속성을 반드시 자신의 방식으로 구현해야 합니다.
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을 쓰든 그 구현 방식에는 관여하지 않습니다.
추상 클래스는 인터페이스와 달리, '완성된 속성'과 '미완성된(추상) 속성'을
모두 가질 수 있습니다. 자식 클래스에게 공통된 기반을 제공하면서도,
각자에게 맞는 부분은 직접 구현하도록 유도하는 유연한 방식입니다.
[추상 클래스 속성 정의]
자동차의 '미완성 설계도(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
지금까지 C#의 속성(Property)에 대해 자세히 알아보았습니다.
속성은 객체 지향의 핵심 원칙인 캡슐화를 지키는 매우 중요한 도구입니다.
| 구분 | 핵심 내용 |
|---|---|
Public 필드 | 필드를 public으로 선언 시 데이터 무결성이 깨질 위험이 있습니다. |
| 속성(Property) | get, set 접근자를 통해 데이터 접근을 제어하는 문지기 역할을 합니다. |
| 자동 구현 속성 | 별도의 로직이 없을 때 코드를 간결하게 만드는 가장 일반적인 속성입니다. |
| 읽기 전용 속성 | 외부에서 값을 변경할 수 없도록 제한하는 속성입니다. |
| 쓰기 전용 속성 | 외부에서 값을 읽을 수 없고 쓰기만 가능한 속성입니다. (드물게 사용) |
init 접근자 | 객체 생성 시점에 단 한 번만 값을 할당할 수 있는 속성입니다. |
required 키워드 | 필수 속성의 누락을 컴파일 시점에 방지하여 안정성을 높입니다. |
| 인터페이스 속성 | 구현 클래스가 특정 속성을 반드시 갖추도록 강제하는 약속입니다. |
| 추상 클래스 속성 | 완성된 속성과 미완성된(추상) 속성을 모두 가질 수 있습니다. |