[C#] Reflection를 이용한 클래스 복제


Development note/C#  2019. 7. 12. 09:00

안녕하세요. 명월입니다.


이 글은 C#에서 Reflection을 통한 클래스 복제에 대한 설명입니다.


디자인 패턴중에 프로토 타입 패턴이 있는데 이 패턴은 클래스를 복제하는 패턴입니다.


링크 - [Design pattern] 프로토타입 패턴 (Prototype pattern)

public class Node
{
  public int Data { get; set; }
  // 클래스 복제
  public Node Clone()
  {
    // Object 클래스에 protected 형태로 존재한다.
    return this.MemberwiseClone() as Node;
  }
}
class Program
{
  static void Main(string[] args)
  {
    Node node = new Node();
    Node node1 = node.Clone();

    // node과 node1의 메모리 해쉬코드가 다르기 때문에 힙에 선언된 클래스는 다른 클래스이다.
    Console.WriteLine("node  hashcode  - " + node.GetHashCode());
    Console.WriteLine("node1 hashcode - " + node1.GetHashCode());
    Console.WriteLine("Press any key...");
    Console.ReadKey();
  }
}

위 결과를 보면 MemberwiseClone메서드를 통해서 클래스를 복제해서 HashCode값이 다르다는 것을 확인할 수 있습니다. 이는 C++에서 memcpy와 같은 형식의 동작입니다.

그런데 C#에서는 이 클래스 복제를 클래스 내부에서는 가능한데(위 예제를 보면 Node클래스 안에서 MemberwiseClone를 사용함), 외부에서는 불가능합니다.

즉, 위 예제의 Node클래스 안에 Clone메서드가 없다면 일일이 변수값을 옮겨 담아야 하고, 또 private의 변수가 있을 경우에는 복제가 되지 않겠네요.

public class Entity
{
  public String Data { get; set; }
}
public class Node
{
  private int privatedata;
  public int Data { get; set; }
  public int Data1 { get; set; }
  public Entity Entity { get; set; }

  public void SetData(int data)
  {
    privatedata = data;
  }

  public void Print()
  {
    Console.WriteLine("Data - " + Data);
    Console.WriteLine("Data1 - " + Data1);
    Console.WriteLine("privatedata - " + privatedata);
    Console.WriteLine("Entity - " + Entity.Data);
    Console.WriteLine();
  }
}
class Program
{
  static Node CopyNode(Node node)
  {
    Node ret = new Node();
    ret.Data = node.Data;
    ret.Data1 = node.Data1;
    // 이건 클래스 복제가 아닌 단순히 포인터를 옮겨 담는 것일뿐.!!
    ret.Entity = node.Entity;
    
    //private 변수는 어떻게 복제를 하지?
    return ret;
  }
  static void Main(string[] args)
  {
    Node node = new Node();
    node.SetData(1);
    node.Data = 2;
    node.Data1 = 3;
    node.Entity = new Entity();
    node.Entity.Data = "Hello world";

    // 클래스 복제..
    Node node1 = CopyNode(node);
    Console.WriteLine("node");
    node.Print();
    Console.WriteLine("node1");
    node1.Print();

    // 포인터만 옮겨 담았기 때문에 node1의 entity를 수정하면 node의 entity까지 수정이 되어 버린다.
    node1.Entity.Data = "Modify!!";

    // node의 entity값을 보면 역시나 Modify로 출력이 되어버린다.
    Console.WriteLine("node");
    node.Print();

    Console.WriteLine("node  hashcode  - " + node.GetHashCode());
    Console.WriteLine("node1 hashcode - " + node1.GetHashCode());
    Console.WriteLine("Press any key...");
    Console.ReadKey();
  }
}

사실 클래스 복제할 때 위와 같이 작성하는 사람이 의외로 많습니다. 어찌어찌 private도 데이터를 넣을 수 있다고 해도 만약에 Node클래스가 수정이 된다면 그에 맞추어 CopyNode함수도 수정해야 겠네요.

예로 int Data2가 추가되면 CopyNode에도 Data2를 복제하는 식을 넣어야 합니다. 또 위 함수의 경우는 int 같은 자료형이라면 문제가 없겠지만 class형이라고 하면 복제가 아닌 포인터를 옮겨 담는 것 뿐이라 완벽한 복제가 되지 않을 확율도 높습니다.


그래서 이걸 해결할 수 있는 방법은 Reflection을 이용해서 복제하는 방법이 있습니다.

using System;
using System.Reflection;

namespace Example
{
  public class Entity
  {
    public String Data { get; set; }
  }
  public class Node
  {
    private int privatedata;
    public int Data { get; set; }
    public int Data1 { get; set; }
    public Entity Entity { get; set; }

    public void SetData(int data)
    {
      privatedata = data;
    }

    public void Print()
    {
      Console.WriteLine("Data - " + Data);
      Console.WriteLine("Data1 - " + Data1);
      Console.WriteLine("privatedata - " + privatedata);
      Console.WriteLine("Entity - " + Entity.Data);
      Console.WriteLine();
    }
  }
  class Program
  {
    static T CopyNode<T>(T node)
    {
      Type type = node.GetType();
      T clone = (T)Activator.CreateInstance(type);
      // 클래스 내부에 있는 모든 변수를 가져온다.
      foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance))
      {
        // 변수가 Class 타입이면 그 재귀를 통해 다시 복제한다. 단, String은 class이지만 구조체처럼 사용되기 때문에 예외
        if (field.FieldType.IsClass && field.FieldType != typeof(String))
        {
          // 새로운 클래스에 데이터를 넣는다.
          field.SetValue(clone, CopyNode(field.GetValue(node)));
          continue;
        }
        // 새로운 클래스에 데이터를 넣는다.
        field.SetValue(clone, field.GetValue(node));
      }
      return clone;
    }
    static void Main(string[] args)
    {
      Node node = new Node();
      node.SetData(1);
      node.Data = 2;
      node.Data1 = 3;
      node.Entity = new Entity();
      node.Entity.Data = "Hello world";

      // 클래스 복제.
      Node node1 = CopyNode(node);

      Console.WriteLine("node");
      node.Print();
      Console.WriteLine("nod1");
      node1.Print();
      // node1의 Entity가 수정이 되어도 Entity 클래스가 복제가 되었기 때문에 node에 영향이 없다.
      node1.Entity.Data = "Modify!!";

      Console.WriteLine("node");
      node.Print();

      Console.WriteLine("node  hashcode  - " + node.GetHashCode());
      Console.WriteLine("node1 hashcode - " + node1.GetHashCode());
      Console.WriteLine("Press any key...");
      Console.ReadKey();
    }
  }
}

여기까지 C#에서 Reflection을 통한 클래스 복제에 대한 설명이었습니다.


궁금한 점이나 잘못된 점이 있으면 댓글 부탁드립니다.