참치김밥은 최고의 한식이다

[Effective C# 2판] Item 6 : 속성을 데이터처럼 동작하게 만들라 본문

책/C#

[Effective C# 2판] Item 6 : 속성을 데이터처럼 동작하게 만들라

l__j__h 2024. 2. 21. 13:15

대부분의 사용자는 속성(프로퍼티)이 데이터 멤버와 동일하게 동작할 것으로 기대하며, 그렇지 않을 경우 타입을 잘못 사용할 수 있다. 속성을 사용하는 문법이 데이터 멤버를 직접 사용하는 것과 같기 때문에, 동작 방식 또한 같으리라고 생각하게 된다.

즉, 프로퍼티가 데이터 멤버를 올바르게 모델링하도록 작성해야 사용자들이 불편하지 않다. 프로퍼티는, 다른 변경 사항이 없다면, get 접근자를 반복해서 호출할 때 늘 같은 값을 반환해야 한다.

덧붙여, 사용자들은 프로퍼티 접근자가 많은 작업을 수행할 것으로 생각하지 않는다. get 접근자가 내부적으로 너무 많은 작업을 수행하지 않도록 한다. 또, set 접근자에서도 값의 유효성 검증 정도만 처리하도록 작성하는 것이 좋다.

아래가 적절한 예시이다.

public string LastName{
	//getter는 생략했다.
	set{
		if(string.IsNullOrEmpty(value))
			throw new ArgumentException("last name can't be null or blank");
		lastName = value;
}
}

 

하지만, 프로퍼티의 get 접근자도 값을 반환하기 전에 계산을 수행하는 경우가 있따. 점(Point) 클래스가 원점에서 거리(Distance)를 프로퍼티로 갖는 경우를 보자. (아래 코드)

public class Point{
	public int x{get; set;}
	public int y{get; set;}
	public double Distance => Math.Sqrt(x*x + y*y);
}

 

위의 경우, Distance 계산은 빠르게 이루어진다. 성능 문제는 일어나지 않을 것이다. 하지만, 혹시라도, 성능 저하의 원인이 Distance를 계산하는 것이라고 한다면, 계산한 거리를 캐싱해두었다가 사용해도 된다. (아래 코드)

public class Point{
	private int x;
	public int X{
		get => x;
		set => {
			x = value;
			distance = default(double?); //distance는 null을 가지게 된다고 한다. double이 nullable이라서(?) by stackoverflow
}
}

	//y에 대해서도 위처럼 만든다

	private double? distance; //nullable한 double로 선언한다.
	public double Distance{
		get{
			if(!distance.HasValue)
				distance = Math.Sqrt(X*X + Y*Y);
			return distance.Value;
}
}
}

 

그런데, 만약, getter에서 원격 DB에 접근하는 등의 이유로 시간이 많이 걸린다면, 어떻게 하는게 좋을까?

아래에 3가지 방법이 있다.

  • 값을 검증한 후에 캐싱한다.
public class MyType{
	//생략
	private string objectName;
	public string ObjectName
		=> (objectName != null) ? objectName : GetNameFromDB();
}

 

  • .NET Framework의 Lazy<T> 클래스를 사용한다.
private Lazy<string> lazyObjectName;
public MyType(){
	lazyObjectName = new Lazy<string>(()=> GetNameFromDB());
}
public string objectName => lazyObjectName.Value;

 

  • 애초부터 프로퍼티가 아니라, 메서드를 사용한다. 
public class MyType{
	public void LoadFromDB(){ ObjectName = GetNameFromDB(); }
	public void SaveToDB(){ SaveNameToDB(ObjectName); }
	public string ObjectName {get; set;}
}

 

이때, Load와 Save 메서드를 프로퍼티의 getter와 setter에 넣으면 문제가 된다. 사용자는 프로퍼티를 사용할 때, 데이터를 그대로 가져오고 넣는 작업을 생각하지, get하고 set할때마다 DB와 통신을 하리라고 생각하지 않는다. 게다가, 만약 사용자가 프로퍼티를 일반적인 Debug.Log()용으로 사용한다고 하면, 사용자가 디버그를 찍을 때마다 DB와 통신을 해야 한다는 끔찍한 일이 벌어질 수도 있다. 또, 네트워크 오류로 통신이 제대로 되지 않으면, 프로퍼티는 아예 작동도 안할 것이다.

728x90