07. 프로퍼티와 캡슐화
필드를 그대로 외부에 열어 두면 무엇이든 일어날 수 있습니다. C# 의 프로퍼티는 외형은 필드처럼, 내부는 메서드처럼 동작해 캡슐화를 자연스럽게 만듭니다.
이 강의에서 배우는 것
- 1auto-property(`{ get; set; }`) 의 의미와 동작을 안다
- 2`init` 전용 setter 로 "한 번만 설정 가능한" 속성을 만든다
- 3`readonly` 필드와 `required` 키워드(C# 11)의 차이를 안다
- 4backing field 를 둔 full property 에서 값 검증을 한다
소개
이전 단원에서 `private` 필드 + `public` 메서드(`GetBalance` 등)로 데이터를 보호했습니다. C#에는 이 패턴을 깔끔하게 처리해 주는 **프로퍼티(property)** 가 따로 있습니다. 외부에서는 필드처럼 보이지만 내부에서는 메서드처럼 동작하는 멤버입니다.
핵심 개념
1) auto-property
class Person
{
public string Name { get; set; } = ""; // 자동 생성된 숨은 필드와 연결
public int Age { get; set; }
}컴파일러가 내부에 `_name`, `_age` 같은 숨은 필드를 만들고 `get`/`set` 도 자동 작성해 줍니다.
2) `get` 만 가진 읽기 전용 프로퍼티
public string Name { get; } // 생성자에서만 설정 가능객체가 만들어진 뒤에는 바꾸지 못합니다.
3) `init` 전용 setter (C# 9+)
public string Name { get; init; } = "";객체 초기화(`new Person { Name = "A" }`) 시점이나 생성자 안에서만 설정 가능. 그 이후엔 읽기 전용. **불변(immutable) 객체** 를 만들 때 유용합니다.
var p = new Person { Name = "Alice" };
// p.Name = "Bob"; // 컴파일 에러4) `required` 키워드 (C# 11)
"이 프로퍼티는 객체를 만들 때 반드시 값을 줘야 한다"는 표시입니다.
class User
{
public required string Email { get; init; }
}
var u = new User { Email = "a@b.com" }; // OK
// var u2 = new User(); // 컴파일 에러생성자 + `init` 의 안전성과 객체 초기화 문법의 편리함을 모두 잡습니다.
5) `readonly` 필드
프로퍼티 말고 **필드** 자체를 한 번만 쓰게 막는 키워드.
class Circle
{
public readonly double Pi = 3.14159;
}생성자에서만 값 대입 가능. 보통 상수에 가까운 값에 씁니다.
6) full property — backing field + 검증
auto-property 만으로는 값 검증을 못 합니다. 검증이 필요하면 직접 backing field 를 두고 `set` 안에 로직을 둡니다.
class Temperature
{
private double celsius;
public double Celsius
{
get => celsius;
set
{
if (value < -273.15)
throw new ArgumentException("절대영도보다 낮음");
celsius = value;
}
}
}`set` 안의 `value` 는 "들어온 새 값"을 가리키는 예약 매개변수입니다.
핵심 예제
예제 1 — `AutoProperty` : 가장 기본 형태
// Program.cs
using CodingNow.Lecture.Oop07;
var p = new Person();
p.Name = "Alice";
p.Age = 30;
Console.WriteLine($"{p.Name} / {p.Age}");// Person.cs
namespace CodingNow.Lecture.Oop07;
internal class Person
{
public string Name { get; set; } = "";
public int Age { get; set; }
}**실행 결과**
Alice / 30**메모:** 필드를 직접 노출하는 것과 비슷해 보여도, 나중에 `set` 에 로직을 넣을 여지가 남습니다.
예제 2 — `GetInit` : 객체 초기화 + `init`
// Program.cs
using CodingNow.Lecture.Oop07;
var alice = new Person { Name = "Alice", Age = 30 };
Console.WriteLine($"{alice.Name} / {alice.Age}");
// alice.Name = "Bob"; // init 이라 이후 변경 불가 (컴파일 에러)// Person.cs
namespace CodingNow.Lecture.Oop07;
internal class Person
{
public string Name { get; init; } = "";
public int Age { get; init; }
}**실행 결과**
Alice / 30**메모:** 객체 초기화 구문 `new Person { ... }` 안에서만 값을 세팅할 수 있고, 그 뒤엔 읽기 전용입니다.
예제 3 — `RequiredProp` : 필수 프로퍼티
// Program.cs
using CodingNow.Lecture.Oop07;
var u = new User { Email = "alice@example.com" };
Console.WriteLine(u.Email);
// var u2 = new User(); // Email 을 안 줘서 컴파일 에러// User.cs
namespace CodingNow.Lecture.Oop07;
internal class User
{
public required string Email { get; init; }
public string DisplayName { get; init; } = "익명";
}**실행 결과**
alice@example.com**메모:** `required` 가 붙은 프로퍼티는 객체를 만들 때 반드시 값을 줘야 합니다. 생성자가 없어도 안전합니다.
예제 4 — `FullProperty` : 값 검증 포함
// Program.cs
using CodingNow.Lecture.Oop07;
var t = new Temperature();
t.Celsius = 25;
Console.WriteLine($"{t.Celsius}°C");
try
{
t.Celsius = -500; // 절대영도 아래 → 예외
}
catch (ArgumentException ex)
{
Console.WriteLine($"에러: {ex.Message}");
}// Temperature.cs
namespace CodingNow.Lecture.Oop07;
internal class Temperature
{
private double celsius;
public double Celsius
{
get => celsius;
set
{
if (value < -273.15)
throw new ArgumentException("절대영도(-273.15°C) 아래로 내려갈 수 없습니다.");
celsius = value;
}
}
}**실행 결과**
25°C
에러: 절대영도(-273.15°C) 아래로 내려갈 수 없습니다.**메모:** 외부에서는 그냥 `t.Celsius = 25;` 처럼 필드처럼 쓰지만, 내부에선 메서드가 호출됩니다. 이게 프로퍼티의 본질.
전체 예제 코드 (src/)
src/AutoProperty/AutoProperty.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>CodingNow.Lecture.Oop07</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
src/AutoProperty/Person.cs
namespace CodingNow.Lecture.Oop07;
internal class Person
{
// auto-property: 컴파일러가 숨은 필드와 get/set 을 자동 생성한다.
public string Name { get; set; } = "";
public int Age { get; set; }
}
src/AutoProperty/Program.cs
using CodingNow.Lecture.Oop07;
var p = new Person();
p.Name = "Alice";
p.Age = 30;
Console.WriteLine($"{p.Name} / {p.Age}");
src/FullProperty/FullProperty.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>CodingNow.Lecture.Oop07</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
src/FullProperty/Program.cs
using CodingNow.Lecture.Oop07;
var t = new Temperature();
t.Celsius = 25;
Console.WriteLine($"{t.Celsius}°C");
try
{
t.Celsius = -500; // 절대영도 아래 → 예외 발생
}
catch (ArgumentException ex)
{
Console.WriteLine($"에러: {ex.Message}");
}
src/FullProperty/Temperature.cs
namespace CodingNow.Lecture.Oop07;
internal class Temperature
{
// backing field: 실제 데이터는 여기에 저장
private double celsius;
public double Celsius
{
get => celsius;
set
{
// set 안의 value 는 "들어온 새 값" 을 가리키는 예약 매개변수
if (value < -273.15)
throw new ArgumentException("절대영도(-273.15°C) 아래로 내려갈 수 없습니다.");
celsius = value;
}
}
}
src/GetInit/GetInit.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>CodingNow.Lecture.Oop07</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
src/GetInit/Person.cs
namespace CodingNow.Lecture.Oop07;
internal class Person
{
// init: 객체 초기화 시점이나 생성자 안에서만 설정 가능. 이후엔 읽기 전용.
public string Name { get; init; } = "";
public int Age { get; init; }
}
src/GetInit/Program.cs
using CodingNow.Lecture.Oop07;
// 객체 초기화 구문에서만 값을 세팅할 수 있다.
var alice = new Person { Name = "Alice", Age = 30 };
Console.WriteLine($"{alice.Name} / {alice.Age}");
// alice.Name = "Bob"; // init 이라 이후 변경 불가 (주석 풀면 컴파일 에러)
src/RequiredProp/Program.cs
using CodingNow.Lecture.Oop07;
// required 프로퍼티는 객체 초기화 시 반드시 값을 줘야 한다.
var u = new User { Email = "alice@example.com" };
Console.WriteLine($"{u.Email} / {u.DisplayName}");
// var u2 = new User(); // Email 누락 → 컴파일 에러
src/RequiredProp/RequiredProp.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>CodingNow.Lecture.Oop07</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
src/RequiredProp/User.cs
namespace CodingNow.Lecture.Oop07;
internal class User
{
// required: 객체 생성 시 반드시 값을 줘야 하는 프로퍼티 (C# 11+)
public required string Email { get; init; }
// 기본값이 있어 선택적으로 세팅 가능
public string DisplayName { get; init; } = "익명";
}
자주 하는 실수
- `public string Name;` 처럼 필드를 그대로 공개 — 가능한 한 프로퍼티 사용.
- `init` 인데 외부에서 다시 대입하려고 한다 — 객체 초기화 시점이나 생성자 안에서만 가능.
- `required` 가 붙은 프로퍼티를 빼먹고 객체 생성 → 컴파일 에러.
- `set` 안에서 `Celsius = value;` 처럼 자기 자신을 다시 호출해 **무한 재귀** 가 발생 (StackOverflow). 반드시 backing field(`celsius`) 에 대입할 것.
- `readonly` 필드를 생성자가 아닌 일반 메서드에서 바꾸려 한다 — 컴파일 에러.
정리
- 프로퍼티는 "필드처럼 보이는 메서드". 외부 인터페이스는 단순하게 두면서 내부 로직을 숨길 수 있다.
- 변경 불가 데이터에는 `init`, 필수 입력에는 `required`, 검증이 필요하면 full property + backing field.
- 캡슐화의 본질은 "외부가 객체 내부 상태를 마음대로 못 건드리게" 막는 것.
과제
**과제 - 07. 프로퍼티와 캡슐화**
문제 1 — `Temperature` (절대영도 검증)
- 프로젝트 폴더: `Homework01/`
- 핵심 개념: full property, backing field, `set` 안의 검증
요구사항
- `Temperature` 클래스에 `Celsius` 프로퍼티를 만든다.
- 값을 세팅할 때 `-273.15` 미만이면 `ArgumentException` 을 던진다.
- `Fahrenheit` 프로퍼티(읽기 전용)도 추가한다. 공식: `°F = °C × 9/5 + 32`.
- `Program.cs` 에서 정상 값과 비정상 값을 세팅해 출력한다.
예상 출력
0°C = 32°F
100°C = 212°F
에러: 절대영도(-273.15°C) 아래로 내려갈 수 없습니다.힌트
- backing field 는 `private double celsius;` 로 둔다.
- `Fahrenheit` 는 `get => celsius * 9 / 5 + 32;` 처럼 계산식.
문제 2 — `Product` (가격 0 이상 검증)
- 프로젝트 폴더: `Homework02/`
- 핵심 개념: `required`, `init`, full property + 검증
요구사항
- `Product` 클래스를 만든다.
- `Name` 은 `required string ... { get; init; }` 로 만든다.
- `Price` 는 full property 로 만들고, 음수 값을 세팅하면 `ArgumentException`.
- 객체 초기화 구문 `new Product { Name = "...", Price = ... }` 으로 만든다.
예상 출력
사과 / 1500원
배 / 0원
에러: 가격은 0 이상이어야 합니다.힌트
- `Price` 의 backing field 는 `private int price;`.
- 초기 가격을 0 으로 두려면 기본값을 따로 줄 필요 없음 (`int` 기본값 0).
정답 확인
직접 풀어 본 후 [`answer/`](./answer/) 폴더의 정답과 비교해 보세요.
정답 (answer/)
homework/answer/Homework01/Homework01.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>CodingNow.Lecture.Oop07</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
homework/answer/Homework01/Program.cs
using CodingNow.Lecture.Oop07;
var t = new Temperature();
t.Celsius = 0;
Console.WriteLine($"{t.Celsius}°C = {t.Fahrenheit}°F");
t.Celsius = 100;
Console.WriteLine($"{t.Celsius}°C = {t.Fahrenheit}°F");
try
{
t.Celsius = -300;
}
catch (ArgumentException ex)
{
Console.WriteLine($"에러: {ex.Message}");
}
homework/answer/Homework01/Temperature.cs
namespace CodingNow.Lecture.Oop07;
internal class Temperature
{
private double celsius;
public double Celsius
{
get => celsius;
set
{
if (value < -273.15)
throw new ArgumentException("절대영도(-273.15°C) 아래로 내려갈 수 없습니다.");
celsius = value;
}
}
// 읽기 전용 계산 프로퍼티
public double Fahrenheit => celsius * 9 / 5 + 32;
}
homework/answer/Homework02/Homework02.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>CodingNow.Lecture.Oop07</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
homework/answer/Homework02/Product.cs
namespace CodingNow.Lecture.Oop07;
internal class Product
{
// required: 객체 생성 시 반드시 값을 줘야 함
public required string Name { get; init; }
private int price;
public int Price
{
get => price;
set
{
if (value < 0)
throw new ArgumentException("가격은 0 이상이어야 합니다.");
price = value;
}
}
}
homework/answer/Homework02/Program.cs
using CodingNow.Lecture.Oop07;
var apple = new Product { Name = "사과" };
apple.Price = 1500;
Console.WriteLine($"{apple.Name} / {apple.Price}원");
var pear = new Product { Name = "배" };
Console.WriteLine($"{pear.Name} / {pear.Price}원");
try
{
pear.Price = -100;
}
catch (ArgumentException ex)
{
Console.WriteLine($"에러: {ex.Message}");
}
직접 해 보기
cd src/AutoProperty
dotnet run
cd ../GetInit
dotnet run
cd ../RequiredProp
dotnet run
cd ../FullProperty
dotnet run다음 단원
[08_상속](../08_상속/) — 이미 있는 클래스를 확장해 새 클래스를 만드는 법을 배웁니다.