[C#] 가비지 컬렉터(GC)와 소멸자

세동네·2022년 10월 24일
0
post-thumbnail

C#의 객체는 생성자와 소멸자를 가질 수 있다. 생성자는 객체가 생성되는 시점, 소멸자는 소멸되는 시점에 호출된다. 이때 주의해야 할 것은 생성자는 사용자가 원할 때 객체를 생성하기 때문에 언제 호출될지 예상 가능하지만, 소멸자는 그렇지 않다는 것이다.

가장 대표적인 비교 대상으로 C++의 소멸자를 들 수 있는데, C++의 메모리는 프로그래머가 크게 관여하기 때문에 이를 위해 객체의 소멸 시점도 명확하다. 하지만 C#은 메모리를 CLR의 GC가 관리하기 때문에 객체의 소멸 시점을 프로그래머가 정확하게 알 수 없다.

이번 포스팅의 결론은 일반적으로 C# 객체의 소멸자 호출은 성능의 저하를 가져올 수 있다는 것이다. 요즘은 MS 공식적으로 소멸자 대신 종료자라는 명칭을 사용한다.

· C#에서의 소멸자 호출

앞서 말한 것처럼 C#의 객체가 언제 소멸될지 알 수 없다. 소멸 타이밍은 GC가 결정하기 때문이다. 프로그래머가 메모리 관리에 적극적으로 관여해야 하는 C++와 다르게 C#은 전혀 신경쓰지 않아도 괜찮다.

C#에서 GC는 객체를 소멸할 타이밍에 소멸자가 있는지 검사하고, 소멸자가 있다면 Finalize()를 호출한다. Finalize()는 내부적으로 다음과 같이 구현되어 있다.

protected override void Finalize()
{
    try
    {
        // Cleanup statements...
    }
    finally
    {
        base.Finalize();
    }
}

객체의 Finalize()는 기본 클래스의 Finalize()를 호출하는 재귀 형태로 이루어져 있다. 이러한 작업을 위해 GC는 해당 객체를 즉시 메모리에서 해제하지 않고 다른 작업 큐에 넣어 두었다가 Finalize() 처리가 모두 마무리된 후에 메모리에서 해제한다.

즉, 쓸데없이 메모리에 자리를 차지하는 시간이 길어지는 것이다. 따라서 객체 소멸시 반드시 필요한 작업이 있는 것이 아니라면 소멸자를 선언하지 않는 것이 성능적으로 유리하다.

또한, 디버깅을 위해 소멸자를 구현한다 하여도 .NET 5(.NET Core 포함) 이상 버전에서 프로그램 실행 종료시 소멸자를 호출하지 않는다. 따라서 콘솔 창에서 소멸자를 확인할 수 없고, GC를 명시적으로 호출하는 GC.Collect() 함수를 호출했을 때 소멸자가 호출되지만, 성능에 좋지 않으니 직접 GC를 호출하는 것은 최소화해야 한다.

· 소멸자를 명시적으로 호출하는 방법

하지만 예외적으로 직접 객체를 소멸해야 하는 경우가 있다. 예를 들어 비용이 많이 드는 외부 리소스를 사용하는 경우 GC를 기다리기 보다는 직접 해제해주는 것이 성능에 도움이 될 수 있다. 파일, 창, 네트워크나 DB의 연결 등의 리소스는 GC가 관리하지 않기 때문에 직접 처리해주어야 한다.

이때 간단하게 소멸자를 구현할 수도 있지만, IDispose 인터페이스 등을 사용하는 것이 성능에 더 좋다고 한다. 이 내용은 추후 다시 정리해보도록 하겠다.

· foreach 문의 이해 (본문과 무관함)

이 포스팅이 쓰여지게 된 이유는 아래와 같다.

예전에 작성했던 C#의 열거자 포스팅을 리뷰하던 중,

public IEnumerator GetEnumerator()
{
    return new PlayerEnum(players);
}

이라는 코드를 보고 궁금증이 시작됐다. IEnumerable 인터페이스에 선언돼있는 해당 함수는 객체에 열거 작업이 있을 때 호출되며, 위 코드 기준 PlayerEnum이라는 열거자 인스턴스를 생성해 반환한다.

foreach (Player player in playerGroup)
{
	Console.WriteLine(player.GetName());
}

이때 playerGroup이라는 객체에서 열거자를 반환받고자 하는데, 반복문을 진행할 때마다 playerGroup에 접근해 열거자 인스턴스를 생성하는 것이 아닌가, 어떻게 position이 유지되는가 궁금했다.

이건 아주 단순히 일반 for문을 떠올리면 생각할 수 있는 문제인데 왜 헷갈렸나 싶다. foreach 문에서 in 키워드 뒤의 열거할 대상 객체를 선택하는 것이 for문의 첫 번째 영역인 초기화 역할을 해주는 것이며, 반복문동안 단 1회 호출된다. 즉, 한번 인스턴스를 생성하면 반복문이 끝나기 전까지 새로 인스턴스를 생성할 일은 없다.

처음엔 저 열거자 인스턴스가 객체 내부에 생성되는 건가.. 그럼 열거자는 언제 없어지지.. 객체가 소멸될 때 같이 소멸되나.. 확인해 봐야지.. 어라.. C#에선 이렇게 소멸자를 쓰면 안 되는구나.. 하면서 이 내용을 알게 되었다. 그냥 뭐 그렇다고..


· 참고

[MS Document] 소멸자(C# 프로그래밍 가이드)
C# 소멸자(destructor)
C#/Programming 생성자와 소멸자
Effective C# 가비지 컬렉터(Garbage Collector) 기초 / finalizer 이해하기
C# 소멸자 - Finalize

0개의 댓글