본문 바로가기

Develop/.NET 가이드

[C#] 리플렉션 Reflection (2) : 타입에 따라 동적으로 객체 생성하기

반응형

Reflection
Reflection

타입에 따라 동적으로 객체 생성하기

바로 사용할 수 있는 예제로 리플렉션(Reflection) 기술을 설명드리도록 하겠습니다.
먼저 예제를 사용하는 상황을 설명드리자면, XML 파일을 Parse 해서 태그별로 객체를 생성하려는 상황입니다.

제가 맨 처음 도입한 방식은 누구나 쉽게 떠올릴 수 있는 방법인 태그 이름에 따라 switch 문을 사용한 방법입니다.

var tags = new List<Tag>();
while (xmlReader.Read())
{
    if (xmlReader.NodeType == XmlNodeType.Element)
    {
        switch (xmlReader.Name)
        {
            case "A":
            // A 태그를 해석하여 객체를 만들고 저장합니다.
            var a = new TagA(xmlReader.GetAttribute("name"));
            tags.Add(a);
            break;

            case "B":
            // B 태그를 해석하여 객체를 만들고 저장합니다.
            var b = new TagB(xmlReader.GetAttribute("name"));
            tags.Add(b);
            break;
        }
    }
}

이렇게 Switch 문으로 XML 태그를 해석하려고 하니, 새로운 태그가 생길 때마다 또는 태그의 속성이 변할 때마다 switch 문을 변경해야만 합니다. 그리고 중복되는 코드도 굉장히 많아지게 됩니다. 이러한 설계상 문제점을 해결하기 위해, 저는 리플렉션 (Reflection) 기술을 사용해서 클래스에 대한 정보를 얻어 태그 객체를 생성할 수 있는 방법을 찾았습니다. 적용 순서는 아래와 같습니다.

  1. Tag를 정의합니다.
  2. 1번에 사용할 Attirubte를 정의합니다.
  3. TagFactory에서 활용할 TagInfo를 정의합니다.
  4. Switch 문을 변경합니다.

1. Tag를 정의합니다.

[TagName("A")]
public class TagA : Tag
{
    [TagProperty("name")]
    public string Name { get; set; }
}

[TagName("B")]
public class TagB : Tag
{
    [TagProperty("name")]
    public string Name { get; set; }
}

2. 1번에 사용할 Attirubte를 정의합니다.

[AttributeUsage(AttributeTargets.Class)]
public sealed class TagNameAttribute : Attribute
{
    public string Name { get; private set; }

    public TagNameAttribute(string name)
    {
        Name = name;
    }
}
[AttributeUsage(AttributeTargets.Property)]
public sealed class TagPropertyAttribute : Attribute
{
    public string Name { get; private set; }

    public TagPropertyAttribute(string name)
    {
        Name = name;
    }
}

3. TagFactory에서 활용할 TagInfo를 정의합니다.

internal sealed class TagInfo
{
    public string Name { get; set; }
    public Type Type { get; set; }

    public TagInfo(string name, Type type)
    {
        Name = name;
        Type = type;
    }
}

3. Tag를 만드는 TagFactory를 정의합니다.

static class TagFactory
{
        // 파트 A
        private static IList<TagInfo> _tagInfos;
        public static IList<TagInfo> TagInfos
        {
            get
            {
                if(_tagInfos == null)
                {
                    var types = from type in typeof(TagFactory).Assembly.GetExportedTypes();
                    var tagTypes = from type in types
                                   where type.GetCustomAttributes(typeof(TagNameAttribute), true).Length > 0 && type.IsSubclassOf(typeof(Tag))
                                   select new TagInfo
                                   {
                                       Name = ((TagNameAttribute)type.GetCustomAttributes(typeof(TagNameAttribute), true)[0]).Name,
                                       Type = type
                                   };
                    _tagInfos = tagTypes.ToList();
                }

                return _tagInfos;
            }
        }

        // 파트 B
        public static Tag CreateTag(XmlReader xmlReader)
        {
            var name = xmlReader.LocalName;
            Tag tag = null;

            if (TagInfos
                .ToDictionary(tag => tag.Name, tag => tag)
                .TryGetValue(name, out TagInfo tagInfo))
            {
                tag = (Tag)Activator.CreateInstance(TagInfo.Type);
                SetAttribute(xmlReader, tag);
                return tag;
            }

            return null;
        }

        // 파트 C
        private static void SetAttribute(XmlReader xmlReader, Tag tag)
        {
            while (xmlReader.MoveToNextAttribute())
            {
                SetPropertyValue(tag, xmlReader.LocalName, xmlReader.Value);
            }
        }

        // 파트 D
        private static void SetPropertyValue(Tag tag, string attributeName, string attributeValue)
        {
            var tagType = tag.GetType();
            var propertyDescriptors = TypeDescriptor.GetProperties(tagType, new[] { new TagPropertyAttribute(attributeName) });
            if(propertyDescriptors.Count > 0)
            {
                var propertyDescriptor = propertyDescriptors[0];
                propertyDescriptor.SetValue(tag, propertyDescriptor.Converter.ConvertFromInvariantString(attributeValue));
            }
        }
}

TagFactory를 나눠서 설명드리도록 하겠습니다.

파트 A : TagInfos

        private static IList<TagInfo> _tagInfos;
        public static IList<TagInfo> TagInfos
        {
            get
            {
                if(_tagInfos == null)
                {
                    // 리플렉션 기술로 현재 assembly 에서 노출한 type 목록을 읽습니다.
                    var types = from type in typeof(TagFactory).Assembly.GetExportedTypes();
                    // 타입 목록 중 TagNameAttribute가 정의되어 있고 Tag를 상속한 클래스를 골라,
                    // TagInfo를 만듭니다.
                    var tagTypes = from type in types
                                   where type.GetCustomAttributes(typeof(TagNameAttribute), true).Length > 0 && type.IsSubclassOf(typeof(Tag))
                                   select new TagInfo
                                   {
                                       Name = ((TagNameAttribute)type.GetCustomAttributes(typeof(TagNameAttribute), true)[0]).Name,
                                       Type = type
                                   };
                    _tagInfos = tagTypes.ToList();
                }

                return _tagInfos;
            }
        }

파트 B : CreateTag

        // 실제 태그를 생성하는 함수입니다.
        public static Tag CreateTag(XmlReader xmlReader)
        {
            var name = xmlReader.LocalName;
            Tag tag = null;

            // TagInfo 목록에서 태그 이름과 매칭되는 타입을 찾습니다.
            if (TagInfos
                .ToDictionary(tag => tag.Name, tag => tag)
                .TryGetValue(name, out TagInfo tagInfo))
            {
                // 매칭되는 타입이 있다면 리플랙션으로 객체를 생성합니다.
                tag = (Tag)Activator.CreateInstance(TagInfo.Type);
                // 태그 속성도 생성된 tag 객체에 객체 속성으로 매칭합니다. 
                SetAttribute(xmlReader, tag);
                // 완성된 tag 객체를 반환합니다.
                return tag;
            }

            return null;
        }

파트 C : SetAttribute

SetAttribute 함수는 단순히 XML Parse 를 위한 함수일 뿐입니다. 핵심 함수는 SetPropertyValue 함수입니다.

        private static void SetAttribute(XmlReader xmlReader, Tag tag)
        {
            // XML 파일에서 현재 Tag의 속성을 읽어 나갑니다.
            while (xmlReader.MoveToNextAttribute())
            {
                SetPropertyValue(tag, xmlReader.LocalName, xmlReader.Value);
            }
        }

파트 D : SetPropertyValue

        private static void SetPropertyValue(Tag tag, string attributeName, string attributeValue)
        {
            var tagType = tag.GetType();
            // tag 클래스 정의 내에서 TagPropertyAttribute 가 지정된 속성들 중 attributeName과 동일한 이름을 가진 속성을 찾아 속성 데이터를 가져옵니다.
            var propertyDescriptors = TypeDescriptor.GetProperties(tagType, new[] { new TagPropertyAttribute(attributeName) });
            if(propertyDescriptors.Count > 0)
            {
                var propertyDescriptor = propertyDescriptors[0];
                // tag 클래스의 속성 값을 실제로 변경합니다.
                propertyDescriptor.SetValue(tag, propertyDescriptor.Converter.ConvertFromInvariantString(attributeValue));
            }
        }

4. Switch 문을 변경합니다.

var tags = new List<Tag>();
while (xmlReader.Read())
{
    if (xmlReader.NodeType == XmlNodeType.Element)
    {
        switch (xmlReader.Name)
        {
            case "A":
            // A 태그를 해석하여 객체를 만들고 저장합니다.
            var a = new TagA(xmlReader.GetAttribute("name"));
            tags.Add(a);
            break;

            case "B":
            // B 태그를 해석하여 객체를 만들고 저장합니다.
            var b = new TagB(xmlReader.GetAttribute("name"));
            tags.Add(b);
            break;
        }
    }
}

위에 switch 문을 아래 switch 문으로 변경합니다.

var tags = new List<Tag>();
while (xmlReader.Read())
{
    switch(xmlReader.NodeType)
    {
        case XmlNodeType.Element:
        var tag = TagFactory.CreateTag(xmlReader);
        tags.Add(tag);
        break;
    }
}

이제 새로운 태그가 생기거나 태그 속성이 변하더라도 Attribute 만 잘 지정하면 됩니다.

반응형