프로퍼티는 필드 앞에 세운 창구다
Esc

    프로퍼티는 필드 앞에 세운 창구다

    –C#기초

    앞에서 필드를 private 으로 막고 메서드로 열었습니다.

    class Player
    {
        private int hp = 100;
    
        public int GetHp() { return hp; }
        public void SetHp(int value)
        {
            if (value < 0) value = 0;
            hp = value;
        }
    }

    안전해졌는데 쓰기가 번거로워졌습니다.

    p.SetHp(p.GetHp() - 10);      // 10 깎기

    p.Hp -= 10 이라고 쓰고 싶습니다. 그런데 그러려면 필드를 열어야 하고, 열면 확인할 기회가 사라집니다.

    밖에서는 필드처럼 보이고, 안에서는 코드가 실행되게 하면 둘 다 됩니다.

    프로퍼티는 이렇게 생겼다

    class Player
    {
        private int hp = 100;
    
        public int Hp
        {
            get { return hp; }
            set
            {
                if (value < 0) value = 0;
                hp = value;
            }
        }
    }
    p.Hp = 50;                 // set 이 실행됩니다
    Console.WriteLine(p.Hp);   // get 이 실행됩니다
    p.Hp -= 10;                // get 하고 계산하고 set 합니다

    겉보기는 필드, 실제로는 메서드입니다.

    value 는 우리가 선언한 적이 없습니다. set 안에서만 쓸 수 있는 이름이고, 대입한 값이 여기 들어옵니다. p.Hp = 50 이면 value 가 50 입니다.

    뒤에 숨어 있는 hp 를 백킹 필드라고 부릅니다. 실제로 값이 저장되는 자리입니다.

    한쪽만 열 수 있다

    읽기만 열고 쓰기는 막으려면 set 을 빼면 됩니다.

    public int Hp
    {
        get { return hp; }
    }

    밖에서는 볼 수만 있고, 값을 바꾸는 것은 클래스 안의 메서드로만 합니다. 앞에서 GetHp 만 두고 SetHp 를 안 만들었던 그 방식입니다.

    바깥에는 읽기만, 안에서는 쓰기도 열고 싶으면 set 에 접근 지정자를 붙입니다.

    public int Hp
    {
        get { return hp; }
        private set { hp = value; }
    }

    이제 클래스 안에서는 Hp = 50 이라고 쓸 수 있고 밖에서는 못 씁니다.

    계산해서 돌려줘도 된다

    프로퍼티가 반드시 필드 하나를 그대로 돌려줄 필요는 없습니다.

    class Player
    {
        private int hp;
        private int shield;
    
        public int TotalHp
        {
            get { return hp + shield; }
        }
    
        public bool IsAlive
        {
            get { return hp > 0; }
        }
    }

    TotalHp 라는 필드는 없습니다. 물어볼 때마다 계산합니다. 밖에서는 그냥 p.TotalHp 로 씁니다. 안에 필드가 몇 개인지 모릅니다.

    앞에서 감추는 것은 값이 아니라 안에서 어떻게 하는지라고 했습니다. 이게 그 이야기입니다. 나중에 방어구를 추가해서 계산식이 바뀌어도 밖은 안 고칩니다.

    읽기만 하는 프로퍼티는 화살표로 짧게 쓸 수 있습니다.

    public int TotalHp => hp + shield;
    public bool IsAlive => hp > 0;

    자동 프로퍼티

    확인할 것이 없는 필드도 있습니다. 그래도 나중을 위해 프로퍼티로 두고 싶은데, 그때마다 백킹 필드를 만드는 것이 번거롭습니다.

    public string Name { get; set; }

    중괄호 안에 get; 과 set; 만 적으면 컴파일러가 백킹 필드를 자동으로 만들어 줍니다. 우리 눈에는 안 보입니다.

    지금은 public 필드와 하는 일이 같습니다. 그런데 다른 점이 있습니다. 나중에 확인이 필요해지면 여기만 고치면 됩니다.

    private string name;
    
    public string Name
    {
        get { return name; }
        set
        {
            if (string.IsNullOrEmpty(value)) return;
            name = value;
        }
    }

    밖에서 p.Name 이라고 쓰던 코드는 한 글자도 안 바뀝니다. public 필드였다면 이 변경이 밖에 전부 영향을 줍니다.

    그래서 C# 에서는 public 필드를 거의 안 씁니다. 공개할 것은 자동 프로퍼티로 두는 것이 관습입니다. 지금 당장 얻는 것은 없지만 나중에 고칠 자유를 남겨 둡니다.

    만들고 나면 못 바꾸게

    set 대신 init 을 쓰면 객체를 만들 때만 넣을 수 있습니다.

    class Point
    {
        public int X { get; init; }
        public int Y { get; init; }
    }
    
    var p = new Point { X = 1, Y = 2 };
    p.X = 99;      // 컴파일 에러

    중괄호로 값을 넣는 문법을 개체 초기화자라고 합니다. 생성자 없이도 채울 수 있습니다. init 을 쓰면 그 자리에서만 되고 그다음부터는 못 고칩니다.

    앞에서 해시 테이블을 볼 때 키로 쓴 값을 나중에 바꾸면 안 된다고 했습니다. init 이 그걸 문법으로 막는 방법입니다.

    메서드와 무엇으로 나누나

    전부 프로퍼티로 만들면 안 됩니다. 기준이 있습니다.

    프로퍼티 메서드
    무엇 상태를 읽고 쓴다 일을 시킨다
    비용 싸야 한다 비싸도 된다
    부작용 없어야 한다 있어도 된다
    예 Name, Count, IsAlive TakeDamage(), Save()

    p.Hp 라고 쓰는 사람은 그게 그냥 값을 읽는 것이라고 생각합니다. 거기서 파일을 읽거나 네트워크를 타면 안 됩니다.

    괄호가 없으면 싸고 안전하다는 것이 읽는 사람의 기대입니다. 비싸거나 뭔가를 바꾸는 일은 메서드로 두고 괄호를 붙여야 합니다.

    list.Count 는 프로퍼티고 list.Add(5) 는 메서드인 것이 그래서입니다.

    정리

    • 프로퍼티는 겉보기엔 필드지만 읽고 쓸 때 코드가 실행됩니다.
    • get 은 읽을 때, set 은 쓸 때 실행됩니다. set 안의 value 가 대입한 값입니다.
    • 실제로 저장되는 필드를 백킹 필드라고 합니다.
    • set 을 빼면 읽기만, private set 이면 밖에서는 읽기만 됩니다.
    • 필드 없이 계산해서 돌려줘도 됩니다. 안이 어떻게 생겼는지 감춥니다.
    • { get; set; } 은 백킹 필드를 컴파일러가 만들어 주는 자동 프로퍼티입니다.
    • public 필드 대신 자동 프로퍼티를 씁니다. 나중에 고칠 자유를 남깁니다.
    • init 은 만들 때만 넣을 수 있게 합니다.
    • 상태를 읽고 쓰는 것은 프로퍼티, 일을 시키는 것은 메서드입니다.