본문 바로가기
개발 기록

[C#] Property

by kkkdh 2024. 11. 7.
728x90

이 글에서는 C# 프로퍼티를 public field로 열고 사용하거나, getter, setter를 사용하는 방식과의 이점에 대해서 정리해보려 합니다.

 

개인적으로 학습하고 이해한 뒤 깨달은 점들을 정리하다 보니, 내용 중 잘못된 부분이 있을 수 있습니다.
해당 글의 내용을 참고하시되, 크로스 체크하여 학습하시면 더욱 좋을 것 같습니다!

Property가 뭐지?

처음 C# 언어를 접했을 때, 생소했던 점 중 하나는 getter, setter를 따로 이용하지 않는다는 것이었습니다.

그 대신에 이렇게 생긴 녀석을 대신 사용하더군요?

class TestClass
{
    // C# Property
    public int PropertyExample { get; private set; }
    // normal field
    public int fieldExample;
}

 

위 문법은 Property라는 문법으로 일반적인 class의 필드와는 또 다른 특징을 가집니다.

 

TestClass tc = new TestClass();
Debug.Log(tc.fieldExample);
Debug.Log(tc.PropertyExample);

 

프로퍼티와 필드 변수 둘 다 위와 같은 동일한 문법으로 접근 가능합니다. (둘 다 클래스의 인스턴스 멤버인 상황)

 

보통 필드 변수의 경우 다음과 같이 getter를 정의해서 접근할 수 있는 창구를 열어주죠

 

public int GetFieldExample()
{
    return this.fieldExample;
}

그리고 사용할 때는 위와 같은 Get method를 사용해 값을 조회합니다.

 

하지만, Property를 사용하는 경우에는 public 필드 변수에 직접 접근하는 것과 같은 문법으로 조회하고, 변수에 조회하는 순간 조회 접근자와 수정 접근자를 호출하는 로직을 타게 됩니다.

 

그래서 별도의 getter, setter 메서드를 추가 선언하여 코드를 늘려갈 필요 없다는 점이 가장 큰 장점 중 하나입니다.

 

특별한 로직을 추가하지 않고, { get; set; }과 같이 사용하는 경우에는 귀찮은 getter, setter의 선언을 C# Compiler (CSC)에게 맡길 수 있다는 점도 장점입니다.

 

💡 경우에 따라 다르겠으나, 대부분의 프로젝트에서 C# 프로퍼티는 대문자로 시작하는 naming convention을 따릅니다.

 


접근자 (get, set accessor)

Property는 두 가지의 접근자(accessor)를 포함합니다.

  • get accessor: 조회 접근자
  • set accessor: 수정 접근자

이 접근자는 getter, setter를 구현해 사용할 때 처럼 접근 및 수정함에 따라서 원하는 로직을 끼워 넣는 것이 가능하게끔 만들어줍니다.

 

다음 예시처럼 접근자를 활용하여, 클래스 멤버 변수에 단순하게 접근하는 것을 넘어 알맞은 로직을 구현해 넣을 수 있습니다.

public class Person
{
    // 자동 구현 프로퍼티 (Automatic Property)
    public string Name { get; private set; }

    // 전체 구현 프로퍼티 (Fully Implemented Property)
    private int _age;
    public int Age
    {
        get 
        { 
            Console.WriteLine("Age getter called");
            return _age; 
        }
        set
        {
            Console.WriteLine("Age setter called");
            if (value < 0)
            {
                throw new ArgumentOutOfRangeException("Age cannot be negative.");
            }
            _age = value;
        }
    }

    // 읽기 전용 프로퍼티 (Read-only Property)
    public int BirthYear
    {
        get { return DateTime.Now.Year - Age; }
    }

    // 계산된 프로퍼티 (Computed Property)
    public string AgeCategory
    {
        get
        {
            if (Age < 18) return "Minor";
            if (Age < 65) return "Adult";
            return "Senior";
        }
    }

    // 생성자 (Constructor)
    public Person(string name, int age)
    {
        Name = name;
        Age = age;
    }
}

선언 방식은 그냥 중괄호 안에 Get, Set method에서 구현하던 로직을 작성하여 사용하면 됩니다.

  • Set Method로 전달되던 parameter 값은 value라는 키워드로 사용 가능
public int Age { get; set; } = 0;

private int _age = 0;
public int Age
{
	get { return _age; }
    set { age = value; }
}

 

  • 위 코드처럼 프로퍼티를 선언하면, 아래와 같은 코드를 컴파일 과정에서 생성해 줍니다.
  • 이를 자동 속성이라고 부릅니다.

접근자에게 접근 제한자 설정 가능

그냥 getter, setter의 선언을 생략하고, 외부에서 참조시 따로 method를 호출하지 않는 것 외에도

접근 제한자를 지정하여, 참조하는 범위를 제한할 수 있습니다.

  • public, private, protected...

현재 저의 경험상 get (조회 접근자)은 public으로 열어주고, set (수정 접근자)은 private or protected로 지정하여 클래스 내부 혹은 상속 관계 내에서만 호출하도록 막아주는 경우가 많았던 것 같습니다.


활용해본다면...

public int Age
{
    get 
    { 
        Console.WriteLine("Age getter called");
        return _age; 
    }
    set
    {
        Console.WriteLine("Age setter called");
        if (value < 0)
        {
            throw new ArgumentOutOfRangeException("Age cannot be negative.");
        }
        _age = value;
    }
}

 public int BirthYear
{
    get { return DateTime.Now.Year - Age; }
}
  • Age 프로퍼티 지정 시 논리적으로 불가능한 값 기입 불가 (음수의 나이는 존재하지 않기에)
  • 접근자를 통해 값을 조회하거나, 수정할 당시  Console Log 기록
  • backing field 없이 다른 멤버의 상태에 따른 값을 조회하는 프로퍼티 또한 만들 수 있음

활용 가능성은 무궁무진할 것 같습니다. (이쯤이면, 이 기능을 두고 굳이 public 필드로 열어줄 필요가 없을 것 같다는 생각이 들었습니다.)

 

public field를 사용하는 경우에는 필드 접근에 따른 로직을 강제하기도 어려울뿐더러, 수정 또한 어느 곳에서나 가능하기에 public 필드로 제공하는 것보다는 public set을 연 상태로 property를 선언하여 사용하는 것이 추후 개선 가능하다는 점에서도 훨씬 좋은 선택지인 것 같습니다.

 


IL 코드로 변환된다면

IL 코드 (Intermediate Language)로 변환된 결과를 보면, Property 코드를 어떻게 바꿔주는지 명확하게 확인이 가능합니다.

using System;

class People
{
    private int age;
    
    public int Age
    {
        get {
            return age;
        }
        set
        {
            age = value;
        }
    }

    public People() { }
}

class Property2
{
    public static void Main()
    {
        People p = new People();
        p.Age = 10; // set {}
        int n = p.Age; // get {}
    }
}

위 코드의 변환 결과는 다음과 같은데요,

.method private hidebysig static void  Main() cil managed
{
  .entrypoint
  // 코드 크기       24 (0x18)
  .maxstack  2
  .locals init (class People V_0,
           int32 V_1)
  IL_0000:  nop
  IL_0001:  newobj     instance void People::.ctor()
  IL_0006:  stloc.0
  IL_0007:  ldloc.0
  IL_0008:  ldc.i4.s   10
  IL_000a:  callvirt   instance void People::set_Age(int32)
  IL_000f:  nop
  IL_0010:  ldloc.0
  IL_0011:  callvirt   instance int32 People::get_Age()
  IL_0016:  stloc.1
  IL_0017:  ret
} // end of method Property2::Main

 

정리하면 요렇게 변환됩니다. (컴파일 과정 중 생성됩니다.)

  • 조회 접근자는 get_Property_name으로 변환되어 별도 메서드 생성
  • 수정 접근자는 set_Property_name으로 변환되어 별도 메서드 생성

그래서 위와 같은 형태로 메서드를 선언하는 경우 중복 선언 오류가 발생하니, 주의해야 합니다.


레퍼런스 👍

    1. https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/properties
    2. Perplexity - example code

 

728x90

댓글