← C# 강의 목록으로
🧩
객체지향
객체지향 · 선수: 클래스

07. 프로퍼티와 캡슐화

필드를 그대로 외부에 열어 두면 무엇이든 일어날 수 있습니다. C# 의 프로퍼티는 외형은 필드처럼, 내부는 메서드처럼 동작해 캡슐화를 자연스럽게 만듭니다.

C#.NET 8객체지향프로퍼티
소요 시간
약 1~1.5시간
난이도
📊 중급
선수 조건
🎯 클래스와 객체
결과물
필드를 그대로 외부에 열어 두면 무엇이든 일어날 수 있습니다. 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

csharp
class Person
{
    public string Name { get; set; } = "";   // 자동 생성된 숨은 필드와 연결
    public int Age { get; set; }
}

컴파일러가 내부에 `_name`, `_age` 같은 숨은 필드를 만들고 `get`/`set` 도 자동 작성해 줍니다.

2) `get` 만 가진 읽기 전용 프로퍼티

csharp
public string Name { get; }            // 생성자에서만 설정 가능

객체가 만들어진 뒤에는 바꾸지 못합니다.

3) `init` 전용 setter (C# 9+)

csharp
public string Name { get; init; } = "";

객체 초기화(`new Person { Name = "A" }`) 시점이나 생성자 안에서만 설정 가능. 그 이후엔 읽기 전용. **불변(immutable) 객체** 를 만들 때 유용합니다.

csharp
var p = new Person { Name = "Alice" };
// p.Name = "Bob";   // 컴파일 에러

4) `required` 키워드 (C# 11)

"이 프로퍼티는 객체를 만들 때 반드시 값을 줘야 한다"는 표시입니다.

csharp
class User
{
    public required string Email { get; init; }
}

var u = new User { Email = "a@b.com" };  // OK
// var u2 = new User();                  // 컴파일 에러

생성자 + `init` 의 안전성과 객체 초기화 문법의 편리함을 모두 잡습니다.

5) `readonly` 필드

프로퍼티 말고 **필드** 자체를 한 번만 쓰게 막는 키워드.

csharp
class Circle
{
    public readonly double Pi = 3.14159;
}

생성자에서만 값 대입 가능. 보통 상수에 가까운 값에 씁니다.

6) full property — backing field + 검증

auto-property 만으로는 값 검증을 못 합니다. 검증이 필요하면 직접 backing field 를 두고 `set` 안에 로직을 둡니다.

csharp
class Temperature
{
    private double celsius;

    public double Celsius
    {
        get => celsius;
        set
        {
            if (value < -273.15)
                throw new ArgumentException("절대영도보다 낮음");
            celsius = value;
        }
    }
}

`set` 안의 `value` 는 "들어온 새 값"을 가리키는 예약 매개변수입니다.

핵심 예제

예제 1 — `AutoProperty` : 가장 기본 형태

csharp
// Program.cs
using CodingNow.Lecture.Oop07;

var p = new Person();
p.Name = "Alice";
p.Age = 30;
Console.WriteLine($"{p.Name} / {p.Age}");
csharp
// Person.cs
namespace CodingNow.Lecture.Oop07;

internal class Person
{
    public string Name { get; set; } = "";
    public int Age { get; set; }
}

**실행 결과**

text
Alice / 30

**메모:** 필드를 직접 노출하는 것과 비슷해 보여도, 나중에 `set` 에 로직을 넣을 여지가 남습니다.

예제 2 — `GetInit` : 객체 초기화 + `init`

csharp
// Program.cs
using CodingNow.Lecture.Oop07;

var alice = new Person { Name = "Alice", Age = 30 };
Console.WriteLine($"{alice.Name} / {alice.Age}");

// alice.Name = "Bob";   // init 이라 이후 변경 불가 (컴파일 에러)
csharp
// Person.cs
namespace CodingNow.Lecture.Oop07;

internal class Person
{
    public string Name { get; init; } = "";
    public int Age { get; init; }
}

**실행 결과**

text
Alice / 30

**메모:** 객체 초기화 구문 `new Person { ... }` 안에서만 값을 세팅할 수 있고, 그 뒤엔 읽기 전용입니다.

예제 3 — `RequiredProp` : 필수 프로퍼티

csharp
// Program.cs
using CodingNow.Lecture.Oop07;

var u = new User { Email = "alice@example.com" };
Console.WriteLine(u.Email);

// var u2 = new User();   // Email 을 안 줘서 컴파일 에러
csharp
// User.cs
namespace CodingNow.Lecture.Oop07;

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

**실행 결과**

text
alice@example.com

**메모:** `required` 가 붙은 프로퍼티는 객체를 만들 때 반드시 값을 줘야 합니다. 생성자가 없어도 안전합니다.

예제 4 — `FullProperty` : 값 검증 포함

csharp
// 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}");
}
csharp
// 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;
        }
    }
}

**실행 결과**

text
25°C
에러: 절대영도(-273.15°C) 아래로 내려갈 수 없습니다.

**메모:** 외부에서는 그냥 `t.Celsius = 25;` 처럼 필드처럼 쓰지만, 내부에선 메서드가 호출됩니다. 이게 프로퍼티의 본질.

전체 예제 코드 (src/)

src/AutoProperty/AutoProperty.csproj

xml
<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

csharp
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

csharp
using CodingNow.Lecture.Oop07;

var p = new Person();
p.Name = "Alice";
p.Age = 30;

Console.WriteLine($"{p.Name} / {p.Age}");

src/FullProperty/FullProperty.csproj

xml
<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

csharp
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

csharp
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

xml
<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

csharp
namespace CodingNow.Lecture.Oop07;

internal class Person
{
    // init: 객체 초기화 시점이나 생성자 안에서만 설정 가능. 이후엔 읽기 전용.
    public string Name { get; init; } = "";
    public int Age { get; init; }
}

src/GetInit/Program.cs

csharp
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

csharp
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

xml
<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

csharp
namespace CodingNow.Lecture.Oop07;

internal class User
{
    // required: 객체 생성 시 반드시 값을 줘야 하는 프로퍼티 (C# 11+)
    public required string Email { get; init; }

    // 기본값이 있어 선택적으로 세팅 가능
    public string DisplayName { get; init; } = "익명";
}

자주 하는 실수

  1. `public string Name;` 처럼 필드를 그대로 공개 — 가능한 한 프로퍼티 사용.
  2. `init` 인데 외부에서 다시 대입하려고 한다 — 객체 초기화 시점이나 생성자 안에서만 가능.
  3. `required` 가 붙은 프로퍼티를 빼먹고 객체 생성 → 컴파일 에러.
  4. `set` 안에서 `Celsius = value;` 처럼 자기 자신을 다시 호출해 **무한 재귀** 가 발생 (StackOverflow). 반드시 backing field(`celsius`) 에 대입할 것.
  5. `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` 에서 정상 값과 비정상 값을 세팅해 출력한다.

예상 출력

text
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 = ... }` 으로 만든다.

예상 출력

text
사과 / 1500원
배 / 0원
에러: 가격은 0 이상이어야 합니다.

힌트

  • `Price` 의 backing field 는 `private int price;`.
  • 초기 가격을 0 으로 두려면 기본값을 따로 줄 필요 없음 (`int` 기본값 0).

정답 확인

직접 풀어 본 후 [`answer/`](./answer/) 폴더의 정답과 비교해 보세요.

정답 (answer/)

homework/answer/Homework01/Homework01.csproj

xml
<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

csharp
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

csharp
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

xml
<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

csharp
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

csharp
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}");
}

직접 해 보기

bash
cd src/AutoProperty
dotnet run

cd ../GetInit
dotnet run

cd ../RequiredProp
dotnet run

cd ../FullProperty
dotnet run

다음 단원

[08_상속](../08_상속/) — 이미 있는 클래스를 확장해 새 클래스를 만드는 법을 배웁니다.

예제 코드 / 강의 자료

전체 강의 자료와 예제 코드는 GitHub에서 자유롭게 받아볼 수 있습니다.

GitHub에서 보기 ↗