[Effective C# item 15] 불필요한 객체를 만들지 말라

Effective C# - item 15. 불필요한 객체를 만들지 말라

1. 참조

2. 소개

  • 가비지 콜렉터는 개발자를 대신해서 메모리를 훌륭히 관리하며 사용하지 않는 객체를 효율적으로 제거합니다.
  • 하지만, 힙에서 새로운 객체를 생성하고 삭제하는 작업은 생각보다 많은 프로세서 시간을 사용하고 너무 많은 객체를 생성하면 심각한 성능 저하 문제를 초래할 수 있습니다.
  • 모든 참조 타입의 객체는 지역 변수라도 동적으로 메로리를 할당하는데 이렇게 할당된 객체는 이 객체를 참조하는 상위 멤버가 삭제 되면 가비지가 되는 것입니다.

3. 참조 타입의 지역 변수를 멤버 변수로 선언

  • 가장 흔히 저지르는 나쁜 예 중 하나로 매우 자주 호출되는 이벤트 핸들러에 참조 타입의 객체를 지역 변수로 선언하면 가비지 콜렉터가 생성, 제거를 계속 반복하여 성능에 무리를 줍니다.
  • 아래 예제가 대표적인 상황입니다.
public class FirstControl : Control
{
    public FirstControl() { }
    protected override void OnPaint(PaintEventArgs e)
    {
        // 나쁜 예, Paint 이벤트가 발생할 때마다 동일한 폰트를 생성한다.
        using (Font MyFont = new Font("Arial", 10.0f))
        {
            e.Graphics.DrawString(DateTime.Now.ToString(),
                MyFont, Brushes.Black, new PointF(0, 0));
        }

        base.OnPaint(e);
    }
}
  • OnPaint는 동일한 Font 객체를 매번 다시 생성합니다.
  • 이 객체 생성, 제거에 따른 메모리 할당을 자주 반복하면 사용되는 메모리 양이 많아져서 가비지 콜렉션이 자주 수행될 가능성이 높고 해당 코드는 매우 비효율 적인 코드가 됩니다.
  • 이런 경우는 Font 객체를 지역 변수가 아닌 멤버 변수 로 변경하여 폰트 객체를 한 번만 생성한 후 이를 재사용 하도록 개선할 수 있습니다.
public class SecondControl : Control
{
    public SecondControl() { }
    // 멤버 변수
    public Font MyFont { get; private set; }

    protected override void OnPaint(PaintEventArgs e)
    {
        e.Graphics.DrawString(DateTime.Now.ToString(),
            MyFont, Brushes.Black, new PointF(0, 0));

        base.OnPaint(e);
    }
}

4. 종속성 삽입을 활용하여 자주 사용되는 객체를 정적 멤버 변수로 선언하여 재사용

  • 종속성 삽입을 활용하여 자주 사용되는 객체를 정적 멤버 변수로 선언하여 재사용 하는 방법도 있습니다.
  • 아래 예제 코드가 대표적인 예 입니다.
  • 아래 소스코드를 보면 Black을 최초로 요청했을 때 해당 객체를 생성합니다.
  • Brush 클래스에서는 이렇게 생성된 객체를 저장해두고 동일한 요청이 있을 때마다 해당 객체를 돌려줍니다.
  • 객체의 수를 최소한으로 유지하기 위해 객체 생성을 제한하는 방법입니다.
  • 하지만 이런 방법도 부정적인 면 이 있습니다.
  • 경우에 따라서 생성된 객체가 사용하지 않는데도 메모리에 오래 남아 있을 수 있다는 단점이 있습니다.
  • 또한, Dispose() 메서드를 호출해야 할 시점을 결정할 수 없기 때문에 비관리 리소스를 삭제할 수 없다는 것도 큰 단점입니다.
private static Brush blackBrush;
public static Brush Black
{
    get
    {
        if (blackBrush == null)
        {
            blackBrush = new SolidBrush(Color.Black);
        }

        return blackBrush;
    }
}

5. Immutable(변경불가) 타입을 주의

  • string 객체가 생성되면 이 객체가 가지고 있는 문자열의 내용은 수정이 불가능합니다.
  • 하지만 프로그래밍을 하다보면 마치 string 객체 내의 문자열을 변경할 수 있는 것처럼 보입니다.
  • 이는 문자열이 변경되는 것이 아니라 새로운 문자열을 가진 새로운 string 객체가 다시 생성되는 것입니다.
public void TestString()
{
    string msg = "Hello, ";
    msg += "Hi~Hi~Hi~";
    msg += ". Today is ";
    msg += System.DateTime.Now.ToString();
}
  • 위와 같은 예제 코드가 있습니다.
  • 해당 코드 작업이 실제로는 아래와 같이 매우 비효율적인 작업으로 이루어 지는 것입니다.
  • string 클래스 내의 += 연산자는 기존 문자열에 새로운 문자열을 더하는 것이 아니라 완전히 새로운 string 객체를 생성하여 반환합니다.
public void TestString2()
{
    string msg = "Hello, ";

    // 설명을 위한 코드이며, 유효한 코드는 아니다.
    string tmp1 = new String(msg + "Hi~Hi~Hi~");
    msg = tmp1 // "Hello, "는 가비지가 된다.

    string tmp2 = new String(msg + ". Today is ");
    msg = tmp2 // "Hello, "Hi~Hi~Hi~""는 가비지가 된다.

    string tmp3 = new String(msg + DateTime.Now.ToString());
    msg = tmp3 // "Hello, "Hi~Hi~Hi~". Today is "는 가비지가 된다.
}
  • 위처럼 문자열을 사용하는 것이 아니라, 문자열 보간 을 사용해서 코드를 작성하는 것이 좋습니다.
string msg = stirng.Format("Hello, {0}. Today is {1}", 
    thisUser.Name, DateTime.Now.ToString());
  • 만약 문자열 만드는 방식이 복잡한 경우라면 StringBuilder 클래스를 사용하는 것이 좋습니다.
public void TestStringBuilderExample()
{
    StringBuilder sb = new StringBuilder("ABC", 50);
    sb.Append(new char[] { 'D', 'E', 'F' });
    sb.AppendFormat("GHI{0}{1}", 'J', 'k');
    sb.Insert(0, "Alphabet: ");
    sb.Replace('k', 'K');
}

6. 정리

  • 객체를 과도하게 생성하는 것을 피하고, 불필요한 객체를 생성하지 않습니다.
  • 지역 변수를 멤버 변수로 변경하거나 자주 사용하는 인스턴스를 정적 멤버로 변경하는 것을 고려해 봅니다.
  • Immutbale(변경불가) 의 경우 이에 대응하는 변경 가능한 빌더 클래스를 같이 작성하여 사용합니다.
728x90

이 글을 공유하기

댓글

Designed by JB FACTORY