2014년 1월 27일 월요일

C# 메모리 관리 기법 - WeakReference와 IDisposable

상식적으로 가비지 컬렉터가 작동된다면 메모리가 늘어나는 것에 대한 걱정은 없어야 하겠지만 실제로는 메모리가 늘어나기도 합니다. 
C#에서는 메모리 해제 명령이 없어서 메모리 릭이 발생하게 되면 당황스러울 수 밖에 없죠.

그러면 어떤 경우에 메모리가 늘어나게 될까요?

가비지는 더 이상 참조가 없는 메모리를 뜻합니다. 
메모리가 어디선가 늘어나고 있다는 뜻은 결국 어디선가 의도하지 않은 참조가 일어나고 있어서 가비지화 되지 못하고 계속 누적되고 있다는 말이 되겠죠.

어떤 경우에 그런 일이 발생할까요?


이렇게 캐릭터 매니져가 세개의 캐릭터를 만들었습니다. 


그리고 캐릭터의 위치를 보여주는 객체가 캐릭터 매니저에 접근해서 캐릭터를 모두 참조하게 됩니다. 


그 후에 필요 없어진 캐릭터를 캐릭터 매니저가 삭제하게 됩니다. 
그러면 가비지가 될것이라 생각하겠지만 여전히 지워진 캐릭터를 디스플레이 캐릭터 포지션 객체가 참조하고 있기때문에 가비지가 되지 않습니다. 
여전히 메모리에 남아서 메모리를 증가시키게 되죠.

이런 일을 만들지 않으려면 위의 경우처럼 의도치 않은 참조를 지워줘야 합니다. 

WeakReference는 가비지 컬렉션에 의한 객체 회수를 허용하면서 객체를 참조하게 됩니다. 
이때, 인스턴스를 참조하려면 WeakReference.Target을 사용하는데 해당 인스턴스가 가비지 컬렉터에게 회수되면 null값을 반환하게 됩니다. 
public class Sample
{
    private class Fruit
    {
        public Fruit(string name) 
        { 
            this.Name = name; 
        }

        public string Name 
        { 
            private set; 
            get; 
        }
    }

    public static void TestWeakRef()
    {
        Fruit apple     = new Fruit("Apple");
        Fruit orange    = new Fruit("Orange");
           
        Fruit fruit1    = apple;   // 강한 참조

        // WeakReference를 이용 
        WeakReference fruit2    = new WeakReference(orange); 

        Fruit target;
           
        target          = fruit2.Target as Fruit;

        // 이 경우 결과는 애플과 오렌지가 나오게 됩니다.
        Console.WriteLine(" (1) Fruit1 = \"{0}\", Fruit2 = \"{1}\"", 
            fruit1.Name, target == null ? "" : target.Name);

        // 모두 참조하지 않도록 null값을 넣어줍니다.
        apple   = null;
        orange  = null;

        // 가비지 컬렉터를 작동시킨다
        System.GC.Collect(0, GCCollectionMode.Forced);
        System.GC.WaitForFullGCComplete();

        // 그 후 같은 방법으로 결과를 확인해보면
        // fruit1과 fruit2의 값을 바꾼 적은 없지만, fruit2의 결과가 달라집니다.
        target          = fruit2.Target as Fruit;

        // 결과는 애플만 나오게 된다.
        // 오렌지는 가비지 컬렉터에게 회수되버렸기때문입니다
        Console.WriteLine(" (2) Fruit1 = \"{0}\", Fruit2 = \"{1}\"", 
            fruit1 == null ? "" : fruit1.Name,
            target == null ? "" : target.Name);
    }
}
매니저처럼 객체를 직접 생성하고 삭제하는 모듈이 아닌 이상 가능하다면 WeakReference를 사용하시는 것이 좋습니다. 
그렇게 해서 의도치 않은 참조로 인해 메모리가 누적되게 되는 실수를 방지할 수 있습니다. 

주의할 것은 WeakReference.Target의 값을 보관하면 안된다는 것입니다. 
이값을 보관하게 되면 강한 참조가 일어나 가비지 컬렉터가 회수를 하지 않게 됩니다. 

C#은 메모리를 원하는 시점에 정확히 해제하는 것이 불가능합니다만 C/C++처럼 원하는 시점에 삭제하기를 희망한다면 IDisposable을 사용할 수 있습니다. 

이것은 관리되지 않는 메모리(리소스)들을 해제할때 사용하는 인터페이스입니다.
서로 다른 type의 객체라 하더라도 모든 type의 메모리를 정리할 수 있는 장점이 있습니다. 

WeakReference와 IDisposable을 같이 사용해서 원하는 시점에 메모리를 해제할 수 있습니다. 
아래의 예제는 Disposable 인터페이스를 상속받아 구현되었습니다.
namespace MyApp
{
    public class SampleChar : IDisposable
    {
        private IRenderObject m_Render = Renderer.CreateRenderObject();

        public void Dispose()
        {
            // 이후에 더이상 업데이트가 되지 않도록 여기서 제거하면
            SampleCharManager.Remove(this);

            m_Render = null;
        }

        public bool isRemoved 
        { 
            get 
            { 
                return m_Render == null; 
            } 
        }

        public void Render()
        {
            // 화면에서 그려지지 않도록 합니다. 
            if (m_Render == null) return;
        }

        public void Update() { }
    }
}
IRenderObject 인터페이스 구현은 아래와 같습니다.
namespace MyApp
{
    public interface IRenderObject
    {
        void Render();
    }

    public static class Renderer
    {
        public static IRenderObject CreateRenderObject()
        {
            // IRenderObject를 상속받은 더미 객체
            return new DumyRenderObject(); 
        }
    }
}
아래의 코드는 캐릭터 매니저가 등록된 캐릭터들을 일괄적으로 업데이트 시키고 렌더링하는 코드입니다. 
namespace MyApp 
{
    static class SampleCharManager
    {
        private static List<samplechar> m_list = new List<samplechar>();

        public static void Update()
        {
            foreach (SampleChar obj in m_list) 
            {
                obj.Update();
            }
        }

        public static void Render()
        {
            foreach (SampleChar obj in m_list)
            {
                obj.Render();
            }
        }

        public static void Add(SampleChar obj)
        {
            m_list.Add(obj); 
        }
        
        public static void Remove(SampleChar obj)
        {
            m_list.Remove(obj);
        }
    }
}
그 다음 디버깅을 위한 캐릭터의 위치를 표시하는 코드입니다. 
namespace MyDebug
{
    static class DisplayCharInfo
    {
        private static List<weakreference> m_list            = new List<weakreference>();
        private static Queue<weakreference> m_removeQueue    = new Queue<weakreference>();

        public static void Update()
        {
            foreach (WeakReference item in m_list)
            {
                MyApp.SampleChar obj = (item.Target != null) ? item.Target as MyApp.SampleChar : null;

                if (obj == null || obj.isRemoved)
                {
                    m_removeQueue.Enqueue(item);
                }
                // 삭제되지 않은 캐릭터의 정보만을 표시합니다.
                else 
                { 
                    /* 캐릭터 정보 표시 */ 
                }
            }

            // 삭제된 캐릭터는 목록에서 지워줍니다
            while(m_removeQueue.Count > 0)
            {
                WeakReference item = m_removeQueue.Dequeue();
                m_list.Remove(item);
            }
        }

        public static void Add(MyApp.SampleChar obj)
        {
            // WeakReference를 이용해 참조하도록 해줍니다.
            // SampleCharManager에서 캐릭터를 삭제하더라도 안전하게 가비지가 회수됩니다.
            m_list.Add(new WeakReference(obj));
        }
    }
}
Unity3D는 모노에서 관리하는 메모리와 엔진에서 관리하는 메모리로 나뉩니다. 
둘 다, 메모리가 부족하면 heap에 메모리를 할당하는데 이렇게 늘어난 메모리는 줄어들지 않습니다. 
heap에서 메모리가 재사용되므로 무작정 늘어날 일은 없지만 가비지가 늘어날수록 최대 메모리 사용량이 늘어나므로 가능하면 가비지가 덜 생성되록 코드를 짜는 것이 중요하겠죠.
메모리는 한번에 잡는 것이 좋고, caching이나 memory pool을 사용하는것이 좋습니다. 



댓글 1개:

  1. 안녕하세요. 약한 참조란 개념을 알게 되어 예제를 찾아보던 중에 우연히 유입했습니다.

    https://usroom.tistory.com/entry/%ED%9A%A8%EA%B3%BC%EC%A0%81%EC%9D%B8-C-%EB%A9%94%EB%AA%A8%EB%A6%AC-%EA%B4%80%EB%A6%AC-%EA%B8%B0%EB%B2%95
    제가 찾았던 글과 매우 유사해서, 혹시 참고가 되실까봐 링크 달아놓고 갑니다.
    글 감사합니다.

    답글삭제