티스토리 뷰

 

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

가비지 콜렉터는 개발자를 대신해서 메모리를 훌륭히 관리하며 사용하지 않는 객체를 효율적으로 제거한다. 하지만 힙에서 새로운 객체를 생성하고 삭제하는 작업은 생각보다 많은 프로세서 시간을 사용하고 너무 많은 객체를 생성하면 심각한 성능 문제를 초래할 수 있다.

모든 참조 타입의 객체는 지역 변수라도 동적으로 메모리를 할당하는데 이렇게 할당된 객체는 이 객체를 참조하는 상위 멤버가 삭제되면 가비지가 된다.


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

가장 흔히 저지르는 나쁜 예 중 하나로 매우 자주 호출되는 이벤트 핸들러에 참조 타입의 객체를 지역 변수로 선언하면 가비지 콜렉터가 생성, 제거를 많이 반복하여 성능에 무리를 준다.

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 객체를 지역변수가 아닌 멤버 변수로 변경하여 폰트 객체를 한 번만 생성한 후 이를 재사용하도록 개선할 수 있다.

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

참조 타입의 지역 변수를 정적 멤버 변수로 선언

앞의 예제를 다시 생각해보자. 윈도우에 검정색으로 무엇을 그린다면 검정 브러시가 많이 필요하다. 그런데 검정 브러시가 필요할 때마다 새로 생성하고 제거하고 다시 생성한다는 말이다. 프로그램이 수행되는 동안 여러 개의 창과 컨트롤들이 생성될 텐데 그 때마다 수십 개의 검정 브러시를 생성하기엔 부담이 된다.

이를 해결하기 위해 .NET 프레임워크 설계자들은 필요할 때마다 재사용할 수 있는 검정 브러시를 만들었다. Brush 클래스는 내부적으로 지연 평가(lazy evaluation) 알고리즘을 사용하기 때문에 브러시가 최초로 요청될 때 비로소 필요한 객체를 생성한다.

private static Brush blackBrush public static Brush Black { get { if (blackBrush == null) blackBrush = new SolidBrush(Color.Black); return blackBrush; } }

이 코드를 살펴보면 검정 브러시를 최초로 호출했을 때 비로소 해당 객체를 생성하고 이후에 호출될 때는 미리 생성된 객체를 이용하는 것을 알 수 있다.

하지만 이런 방법도 부정적인 면이 있는데 경우에 따라서는 생성된 객체가 사용하지 않는데도 메모리에 오래 남아 있을 수 있다. 또한 Dispose() 메서드를 호출해야 할 시점을 결정할 수 없기 때문에 비관리 리소스를 삭제할 수 없다는 것도 큰 단점이다.

변경 불가능한 타입

string 객체가 생성되면 이 객체가 가지고 있는 문자열의 내용은 수정이 불가능하다. 하지만 프로그래밍을 하다보면 마치 string 객체 내의 문자열을 변경할 수 있는 것처럼 보이는데 이는 문자열이 변경되는 것이 아니라 새로운 문자열을 가진 새로운 string 객체가 생성되는 것이다.

public static void Main(string[] args) { string msg = "Hello, "; msg += thisUser.Name; msg += ". Today is "; msg += System.DateTime.Now.ToString(); }

이런 코드 작업이 실제로는 아래와 같이 비효율적인 작업으로 이뤄진다.

string msg = "Hello, "; // 설명을 위한 코드이며, 유효한 코드는 아니다. string tmp1 = new String(msg + thisUser.Name); msg = tmp1 // "Hello, "는 가비지가 된다. string tmp2 = new String(msg + ". Today is "); msg = tmp2 // "Hello, <user>"는 가비지가 된다. string tmp3 = new String(msg + DateTime.Now.ToString()); msg = tmp3 // "Hello, <user>. Today is "는 가비지가 된다.

string 클래스 내의 += 연산자는 기존 문자열에 새로운 문자열을 더하는 것이 아니라 완전히 새로운 string 객체를 생성하여 반환한다. 위 코드는 문자열 보간을 사용해서 다음과 같이 코드를 작성하는 것이 좋다.

string msg = stirng.Format("Hello, {0}. Today is {1}", thisUser.Name, DateTime.Now.ToString());

이보다 문자열을 만들어내는 방식이 복잡한 경우라면 StringBuilder 클래스를 사용할 수도 있다. 이러한 클래스 사용을 통해서 사용자는 객체를 여러 단계를 거쳐 구성할 수 있으며 최종적으로 변경이 불가능한 객체를 얻어올 수 있으므로 객체의 불변성이라는 특성을 유지하는 데도 도움이 된다.

정리

1. 객체를 과도하게 생성하는 것을 피하고 불필요한 객체를 생성하지 말자

2. 지역변수를 멤버 변수로 변경하거나 자주 사용하는 인스턴스를 정적 멤버로 변경하는 것을 고려해보자.

3. 변경 불가능한 타입의 경우 이에 대응하는 변경 가능한 빌더 클래스를 같이 작성해보자.


가비지 컬렉터 동작 방식에 대한 이해가 조금씩 이해가 되는 것을 느낀다.

사실 어지간한 변수는 다 지역 변수로 돌리는 것이 유리하다고 생각했었는데

나도 모르게 자주 사용하는 참조 타입 변수임에도 전부 지역 변수로 사용하고 있지는 않았는지 돌아봐야겠다.

참조 - Effective C# <강력한 C# 코드를 구현하는 50가지 전략과 기법, 이펙티브>, 빌 와그너, 김명신, 한빛미디어

반응형
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/02   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28
글 보관함