336x280(권장), 300x250(권장), 250x250, 200x200 크기의 광고 코드만 넣을 수 있습니다.

소개

WPF나 Silverlight를 하기 위해 배우는 개념들 중 이해하기 어려운  의존 속성 시스템(Dependency Property System)에 대해 살펴 보고의존속성(Dependency Property)의 실제 동작을 닷넷 소스를 분석하여 알아 보자.

의존속성의 필요성

의존 속성은 왜 필요한가? 2가지만 살펴 보자.

첫째, 컨트롤들의 자원을 효율적으로 다루기 위해 사용한다. 일반적인 개발환경에서 UI 컨트롤의 상태와 데이터는 Literal 값으로 유지 하는데, 이 값들은 프로그램 실행 후 한번도 바뀌지 않거나 컨트롤들 간에 중복되어 프로그램의 80%를 차지 한다고 한다. 이 점을 착안하여 WPF에서는 의존속성을 사용하여 컨트롤의 기본 값을 정의하고, 컨트롤 간 속성 값들을 공유할 수 있다.

둘째, 화면과 로직의 분리이다. 여기 간단한 시나리오가 있다.

버튼을 누를 때 콤보 박스 항목 중 한국이 선택 되는 이벤트를 처리

그림1. 예제 시나리오

private void Button_Click_1(object sender, RoutedEventArgs e)
{
    var item =
        combo.Items.OfType<string>().FirstOrDefault( (item) => string.Compare(item, "한국") == 0);
    if(string.IsNullOrEmpty(item))
    {
        combo.SelectedItem = item;
    }

코드1. 예제 시나리오 이벤트 처리 

위 코드는 클릭 이벤트 처리에서 콤보박스의 항목들을 순회하며 "한국"이라는 문자열과 일치하는 항목을 찾는다. 결국 콤보박스라는 View 영역과 한국이라는 문자열을 찾는 비지니스 영역이 혼재 되어 있음을 알 수 있다. 만약 콤보박스가 리스트박스로 바뀌어 View 영역을 수정하거나 로그를 남겨 비지니스 영역을 수정해야 된다면 서로 다른 일들을 처리 해야 되는데도 같은 영역을 수정해야 되어 잠재적인 오류 가능성은 커지게 된다. 의존속성의 바인딩(Binding) 기능은 영역을 분리해 독립적인 처리가 가능하다.

의존속성은 언급한 2가지 외에도 스타일, 리소스, 콜백, 애니메이션, 유효성 처리 등의 기능을 가지고 있다.


의존속성의 정체

의존속성을 접할 때 혼란스러운 점은 ‘의존속성이 컨트롤의 정적변수인데 인스턴스의 값들이 어떻게 다를 수 있는가?'라고 말할 수 있겠다. 필자는 닷넷 프레임워크의 DependencyProperty 클래스의 Register 메소드, DependencyObject 클래스의 SetValue 메소드, GetValue 메소드를 열어 해답을 찾고자 하였다.

DependencyProperty.Register

의존속성을 의존객체에 등록하는 메소드이다.

public class MyDependencyObject : DependencyObject
{
		public static DependencyProperty MyTouchProperty;
		static MyDependencyObject()
		{
				MyTouchProperty = DependencyProperty.Register(
				"MyTouch", typeof (bool), typeof (MyDependencyObject));
		}
}

코드2. DependencyObject.Register 예제

언급 했듯이 의존속성이 의존객체의 정적 필드이므로 static 생성자에서 등록 된다. 

private static DependencyProperty RegisterCommon(string name, Type propertyType, Type ownerType, PropertyMetadata defaultMetadata, System.Windows.ValidateValueCallback validateValueCallback)
{
    FromNameKey key = new FromNameKey(name, ownerType);
    lock (Synchronized)
    {
        if (PropertyFromName.Contains(key))
        {
            throw new ArgumentException(MS.Internal.WindowsBase.SR.Get("PropertyAlreadyRegistered", new object[] { name, ownerType.Name }));
        }
    }
    if (defaultMetadata == null)
    {
        defaultMetadata = AutoGeneratePropertyMetadata(propertyType, validateValueCallback, name, ownerType);
    }
    else
    {
        if (!defaultMetadata.DefaultValueWasSet())
        {
            defaultMetadata.DefaultValue = AutoGenerateDefaultValue(propertyType);
        }
        ValidateMetadataDefaultValue(defaultMetadata, propertyType, name, validateValueCallback);
    }
    DependencyProperty dp = new DependencyProperty(name, propertyType, ownerType, defaultMetadata, validateValueCallback);
    defaultMetadata.Seal(dp, null);
    if (defaultMetadata.IsInherited)
    {
        dp._packedData |= Flags.IsPotentiallyInherited;
    }
    if (defaultMetadata.UsingDefaultValueFactory)
    {
        dp._packedData |= Flags.IsPotentiallyUsingDefaultValueFactory;
    }
    lock (Synchronized)
    {
        PropertyFromName[key] = dp;
    }
    if (TraceDependencyProperty.IsEnabled)
    {
        TraceDependencyProperty.TraceActivityItem(TraceDependencyProperty.Register, dp, dp.OwnerType);
    }
    return dp;
}

코드3. DependencyProperty.RegisterCommon

private DependencyProperty(string name, Type propertyType, Type ownerType, PropertyMetadata defaultMetadata, ValidateValueCallback validateValueCallback)
{
    Flags uniqueGlobalIndex;
    this._metadataMap = new InsertionSortMap();
    this._name = name;
    this._propertyType = propertyType;
    this._ownerType = ownerType;
    this._defaultMetadata = defaultMetadata;
    this._validateValueCallback = validateValueCallback;
    lock (Synchronized)
    {
        uniqueGlobalIndex = (Flags) GetUniqueGlobalIndex(ownerType, name);
        RegisteredPropertyList.Add(this);
    }
    if (propertyType.IsValueType)
    {
        uniqueGlobalIndex |= Flags.IsValueType;
    }
    if (propertyType == typeof(object))
    {
        uniqueGlobalIndex |= Flags.IsObjectType;
    }
    if (typeof(Freezable).IsAssignableFrom(propertyType))
    {
        uniqueGlobalIndex |= Flags.IsFreezableType;
    }
    if (propertyType == typeof(string))
    {
        uniqueGlobalIndex |= Flags.IsStringType;
    }
    this._packedData = uniqueGlobalIndex;
}

코드4. DependencyProperty 생성자

Register 메소드의 중요한 역할을 담당하는 RegisterCommon 메소드와 DependencyProperty 생성자이다. 작업 순서를 살펴 보면,

1. Register 메소드는 몇가지 초기화 코드를 거친 후 RegisterCommon 메소드를 호출한다.

2. 인자로 전달된 ownerType과 속성 이름으로 복합 키를 만들어 중복 등록을 방지한다.

3. 의존속성을 생성하는데 생성자 내에서 GetUniqueGlobalIndex 메소드를 호출하여 의존속성의 유일 식별자인 GlobalIndex를 부여한다.

4. PropertyFromName 테이블에 복합키로 의존속성을 등록한다.

그림2.  의존 속성 등록


DependencyObject.GetValue

의존속성 참조로 의존객체 인스턴스의 속성 값을 가져온다.

var myTouch = obj.GetValue(MyDependencyObject.MyTouchProperty);

코드5. DependencyObject.GetValue

public object GetValue(DependencyProperty dp)
{
    base.VerifyAccess();
    if (dp == null)
    {
        throw new ArgumentNullException("dp");
    }
    return this.GetValueEntry(this.LookupEntry(dp.GlobalIndex), dp, null, RequestFlags.FullyResolved).Value;
}

코드6. DependencyObject.GetValue 내부

internal EntryIndex LookupEntry(int targetIndex)
{
    uint index = 0;
    uint effectiveValuesCount = this.EffectiveValuesCount;
    if (effectiveValuesCount > 0)
    {
        int propertyIndex;
        while ((effectiveValuesCount - index) > 3)
        {
            uint num4 = (effectiveValuesCount + index) / 2;
            propertyIndex = this._effectiveValues[num4].PropertyIndex;
            if (targetIndex == propertyIndex)
            {
                return new EntryIndex(num4);
            }
            if (targetIndex <= propertyIndex)
            {
                effectiveValuesCount = num4;
            }
            else
            {
                index = num4 + 1;
            }
        }
    Label_004B:
        propertyIndex = this._effectiveValues[index].PropertyIndex;
        if (propertyIndex == targetIndex)
        {
            return new EntryIndex(index);
        }
        if (propertyIndex <= targetIndex)
        {
            index++;
            if (index < effectiveValuesCount)
            {
                goto Label_004B;
            }
        }
        return new EntryIndex(index, false);
    }
    return new EntryIndex(0, false);
}

코드7. LookupEntry 메소드

if (entryIndex.Found)
{
    if ((requests & RequestFlags.RawEntry) != RequestFlags.FullyResolved)
    {
        entry = this._effectiveValues[entryIndex.Index];
    }
    else
    {
        entry = this.GetEffectiveValue(entryIndex, dp, requests);
    }
}
else
{
    entry = new EffectiveValueEntry(dp, BaseValueSourceInternal.Unknown);
}

코드8. LookupEntry 호출 후 처리 일부

작업 순서를 살펴 보면, 

1. GetValue 메소드에서 LookupEntry 메소드를 호출 하는데 의존속성의 GlobalIndex를 인자로 전달한다.

2. 의존객체(DependencyObject)의 멤버 필드인 EffectiveValueEntry 컬렉션을 이진 탐색하여 GlobalIndex에 상응하는  EffectiveValueEntry를 찾는다.

3. EffectiveValueEntry를 찾는다면, EffectiveValueEntry의 Value를 리턴하고, 그렇지 않다면 인자로 전달된 의존객체의 메타정보 중 기본 값을 리턴한다.

의존속성은 의존속성 값을 제외하고 소유주, 기본 값, 처리 등의 메타 정보를 가지고 있다. 의존객체 인스턴스에서 의존속성 컬렉션을 필드로 사용 하기엔 무리가 있으며, 이를 대신할 EffectiveValueEntry 컬렉션을 사용한다.

EffectiveValueEntry는  PropertyIndex, Value, Source 속성을 가지고 있다. PropertyIndex는 의존속성을 식별하는 GlobalIndex, Value는 실제 속성 값을 저장한다.

그림3. DependencyObject EffectiveValueEntry 컬렉션

요약 하면, 의존객체 인스턴스는 EffectiveValueEntry 컬렉션으로 의존속성을 관리하고, GlobalIndex로 탐색하여 의존속성을 탐색한다.

DependencyObject.SetValue

의존속성의 값을 설정 할 때 사용된다.

public bool MyTouch
{
    set { SetValue(MyTouchProperty, value); }
}

코드9. DependencyObject.SetValue 예제

GetValue와 달리 복잡하다. SetValue 메소드 내에는 값 설정 우선순위, 변경 알림, 콜백 호출(이전 값, 현재 값 전달) 등 고려할 점이 많기 때문이다. 이 글에서는 의존속성이 EffectiveValueEntry 컬렉션에 등록되어 있지 않을 때 처리되는 InsertEntry 메소드를 살펴 보겠다.

private void InsertEntry(EffectiveValueEntry entry, uint entryIndex)
{
    if (!this.CanModifyEffectiveValues)
    {
        throw new InvalidOperationException(MS.Internal.WindowsBase.SR.Get("LocalValueEnumerationInvalidated"));
    }
    uint effectiveValuesCount = this.EffectiveValuesCount;
    if (effectiveValuesCount > 0)
    {
        if (this._effectiveValues.Length == effectiveValuesCount)
        {
            int num2 = (int) (effectiveValuesCount * (this.IsInPropertyInitialization ? 2.0 : 1.2));
            if (num2 == effectiveValuesCount)
            {
                num2++;
            }
            EffectiveValueEntry[] destinationArray = new EffectiveValueEntry[num2];
            Array.Copy(this._effectiveValues, 0L, destinationArray, 0L, (long) entryIndex);
            destinationArray[entryIndex] = entry;
            Array.Copy(this._effectiveValues, (long) entryIndex, destinationArray, (long) (entryIndex + 1), (long) (effectiveValuesCount - entryIndex));
            this._effectiveValues = destinationArray;
        }
        else
        {
            Array.Copy(this._effectiveValues, (long) entryIndex, this._effectiveValues, (long) (entryIndex + 1), (long) (effectiveValuesCount - entryIndex));
            this._effectiveValues[entryIndex] = entry;
        }
    }
    else
    {
        if (this._effectiveValues == null)
        {
            this._effectiveValues = new EffectiveValueEntry[this.EffectiveValuesInitialSize];
        }
        this._effectiveValues[0] = entry;
    }
    this.EffectiveValuesCount = effectiveValuesCount + 1;
}

코드10. DependencyObject.InsertEntry

이 메소드 호출 전 의존속성의 GlobalIndex를 이용하여 EffectiveValueEntry를 만들고, EffectiveValueEntry 컬렉션에 추가할 위치를 찾아내어 인자로 전달된다.

EffectiveValueEntry 컬렉션에 항목이 하나도 없다면, 컬렉션을 초기화하고 새 EffectiveValueEntry를 첫번째 인덱스에 설정한다. 두번째부터는 EffectiveValueEntry 컬렉션의 실제 갯수(EffectiveValuesCount)와 여유 공간(_effectiveValues.Length)가 같은지에 따라 달라진다.

그림4. 의존객체에 EffectiveValueEntry 추가

그림4는 컬렉션의 여유 공간이 남아 있는 경우이며, 숫자는 EffectiveValueEntry의 PropertyIndex이다. LookupEntry 메소드 같이 빠른 검색을 위해선 이진 탐색이 가능해야 되므로 컬렉션을 정렬한다. 코드를 보면 PropertyIndex가 18인 EffectiveValueEntry를 Array.Copy를 호출하여 뒤로 복사하고 새 EffectiveValueEntry를 추가한다.'SortedList 클래스처럼 손쉽게 쓸수 있는 클래스가 있는데'라고 생각 할 수 있지만, 프레임워크 입장에서 보면 편의성이 좋은 SortedList 보다는, 가벼운 배열을 사용하여 필요한 시점에 정렬하는 것이 더 나은 선택이라 볼 수 있겠다.

만약, 의존속성이 EffectiveValueEntry 컬렉션에 이미 등록되어 있다면, 상응하는 EffectiveValueEntry을 찾아 이전 값과 현재 값을 구하고 우선순위나 의존속성의 메타정보등을 따져 값을 설정한다.


결론

지금까지 의존속성의 필요성과 런타임 처리 절차에 대해 살펴 보았다.

XAML/C#을 새로 시작하는 분들에게 다소 어려운 주제가 될 수 있겠지만, 의존속성의 매커니즘을 잘 이해하고 있다면 좀더 매끄럽게 의존속성을 다룰 수 있지 않을까 생각한다.


참고

  • .Net Framework 4.0


+ Recent posts