
Property<T>Unity를 다루기 전, C#에서 프로퍼티를 배운 순간부터 어떻게 하면 효율적으로 사용할 수 있을지 고민을 했던 것 같다. 외부에서 접근 가능하도록 하면서 캡슐화의 성질까지 갖도록 하는 프로퍼티는 생각보다 유용하게, 전략적으로 사용된다.
Property프로퍼티를 사용하는 이유는 객체지향의 정보 은닉을 지향하기 위함이다. 객체 내부의 데이터를 외부에 노출시키지 않고도 특정 메서드(필드)로 호출하여 사용할 수 있도록 하는 것이다.
따라서 아래와 같이 setter에 조건을 설정하여 내부 필드를 외부에서 사용할 수 있도록 만든다.
public class Player : MonoBehaviour
{
private int _hp;
public int Hp
{
get
{
return _hp;
}
set
{
if( value > 0 && value <= 100 )
{
_hp = value;
}
}
}
}
물론 외부에서 건드리는 것(쓰는 것)을 허용하지 않는다면 get 속성만 남기면 될 것이다.
public class Player : MonoBehaviour
{
private int _hp;
public int Hp
{
get
{
return _hp;
}
}
}
하지만 속성이 필요한 필드마다 다음과 같이 프로퍼티를 설정하게 된다면 소드 코드가 길어져 가독성이 떨어지게 되기에 이를 보완할 필요가 있는데, C#에서는 다음과 같은 문법을 지원한다.
public class Player : MonoBehaviour
{
pulbic int Hp { get; set; }
}
하지만 만약 값이 바뀔 때 이벤트를 호출하도록 하고 싶다면 어떻게 될까?
Event간단히 MVC 패턴의 Model class를 예시로 들어보자. 플레이어의 체력이 변화하면 HUD가 갱신되는 반응형 UI를 구현할 때 Model은 다음과 같이 작성할 수 있을 것이다.
public class PlayerModel : MonoBehaviour
{
[SerializeField] private int maxHP = 100;
public event Action<int> OnHPChanged;
private int _hp;
public int HP
{
get
{
return _hp;
}
set
{
_hp = value;
OnHPChanged?.Invoke(_hp);
}
}
private void Start()
{
Init();
}
private void Init()
{
_hp = maxHP;
OnHPChanged?.Invoke(_hp);
}
}
이처럼 setter에 이벤트를 호출하여 값의 변동을 인지할 수 있도록 구현하는데, 만약 반응형 UI에 체력뿐만 아니라 정신력, 마나, 허기 등 다양한 필드를 넣는다고 가정해보자. 모든 필드를 위처럼 작성한다고 하면 굉장히 코드가 길어지고 읽기 싫어질 것이다.
따라서 이를 보완하기 위한 방법은 이러한 필드를 하나의 클래스로 생성하는 것이다.
Property<T>간단히 Property를 제네릭으로 구현하여 이를 활용하는 것이다.
public class Property<T>
{
private T _value;
public T Value
{
get { return _value; }
set
{
_value = value;
OnChanged?.Invoke(_value);
}
}
public event Action<T> OnChanged;
public Property(T value)
{
_value = value;
}
}
이렇게 Property<T>를 구현했다면 이를 활용하여 위의 Model을 다시 작성할 수 있다. 오히려 비교를 확실하게 하기 위해 위에서 말한 정신력, 마나를 추가해보자.
public class PlayerModel : MonoBehaviour
{
[SerializeField] private int maxHP = 100;
[SerializeField] private int maxMP = 100;
[SerializeField] private int maxMental = 100;
public Property<int> HP;
public Property<int> MP;
public Property<int> Mental;
private void Start()
{
Init();
}
private void Init()
{
HP = new Property<int>(maxHP);
MP = new Property<int>(maxMP);
Mental = new Property<int>(maxMental);
}
}
이 정도면 꽤나 가독성 측면에서 훌륭하다고 할 수 있지만 조금 더 기능을 추가하고 싶다.
만약 변경된 값이 기존 값과 일치할 경우 굳이 덮어 쓸 필요가 있을까?
Property<T> with EqualityComparer제네릭 T를 비교하기 위한 연산자 !EqualityComparer<T>.Default.Equals 를 사용하여 변경 후 값과 변경 전 값이 일치할 경우엔 굳이 이벤트를 호출하지 않도록 했다.
public class Property<T>
{
private T _value;
public T Value
{
get { return _value; }
set
{
// 변경된 값이 기존의 값과 일치하지 않는 경우에만
if(!EqualityComparer<T>.Default.Equals(_value, value))
{
_value = value;
OnChanged?.Invoke(_value);
}
}
}
public event Action<T> OnChanged;
public Property(T value)
{
_value = value;
}
}
이렇게 구현하면 특정 상황에서의 리소스를 아낄 수 있을 것이다.
다만 굳이 struct가 아닌 class로 구현할 필요가 있을까? 라는 고민을 했지만, 아무래도 이벤트를 포함하고 있기에 class로 구현하는 것을 선택했다.