솔루션전문, -서브윌- 입니다🙂

📧도입 문의 : servewill@naver.com | beta 버전 도입 문의도 언제나 환영합니다👍

🧰개발 언어/C#

C#으로 개발 중 놓치기 쉬운 부분_2(가비지 컬렉션과 메모리 관리, Mutable 상태의 남용)

서브윌 2023. 2. 2. 05:13

C#으로 개발 중 놓치기 쉬운 부분_1 👈바로가기

 

요약

가비지 컬렉션과 메모리 관리 .NET 가비지 컬렉션(GC)을 이해하지 않고 개발을 진행하면 비효율적인 메모리 사용, 성능 저하와 비관리 리소스의 누수가 발생할 수 있습니다. 비관리 리소스에 대해서는 개발자가 직접 관리해야 하며, 이때 IDisposable 인터페이스를 활용할 수 있습니다.
Mutable 상태의 남용 mutable 상태가 남용되면, 변수나 객체의 상태를 추적하기 어려워지고, 프로그램의 동작이 예측하기 어려워집니다. 함수나 메서드가 mutable 상태를 변경하면, 이를 "부작용(side effect)"라고 합니다. 부작용은 함수의 재사용성을 떨어뜨리고, 테스트와 유지보수를 어렵게 만듭니다. 또한, 다중 스레드 환경에서 mutable 상태를 공유하면, race condition과 같은 병렬 처리 문제를 일으킬 수 있습니다.


가비지 컬렉션

가비지 컬렉션(GC)은 컴퓨터 프로그램에서 메모리 관리를 자동화하는 도구입니다. 프로그램이 동적으로 할당한 메모리 중 필요하지 않은 부분을 자동으로 찾아내어 해제하는 역할을 합니다.

.NET 환경에서의 가비지 컬렉션은 크게 3단계로 이루어집니다.

 

  1. 마킹(Marking): 가비지 컬렉터는 프로그램에서 사용 중인 메모리와 그렇지 않은 메모리를 구분하기 위해 "마킹"이라는 과정을 수행합니다. 이 과정에서 가비지 컬렉터는 더 이상 참조되지 않는 객체, 즉 가비지를 탐지합니다.
  2. 정지(Stop): 가비지 컬렉터는 메모리를 안전하게 회수하기 위해, 모든 애플리케이션 스레드를 일시 중지합니다. 이로써, 가비지 컬렉션 동작 중에는 애플리케이션에서 새로운 메모리를 할당하거나 사용하는 것을 방지합니다.
  3. 회수(Collection): 마지막으로, 가비지 컬렉터는 마킹 과정에서 찾아낸 가비지를 회수하고, 그 공간을 재활용 가능한 메모리로 변환합니다. 이렇게 해서 프로그램이 필요로 하는 추가 메모리를 확보할 수 있게 됩니다.

이러한 과정을 통해 .NET의 가비지 컬렉터는 메모리를 효과적으로 관리하며, 개발자로 하여금 메모리 해제에 대한 부담을 덜어줍니다.

예를 들어, 다음과 같은 C# 코드를 생각해보겠습니다.

public class MyObject
{
    private int[] data = new int[1000];
}

public class Program
{
    public static void Main()
    {
        MyObject obj = new MyObject(); // 객체 생성
        obj = null; // 참조 제거
    }
}

Main 메소드에서 MyObject의 인스턴스를 생성하면 해당 객체는 메모리에 할당됩니다. 이후 obj에 null을 할당함으로써, 생성했던 MyObject의 인스턴스에 대한 참조를 제거합니다.

이 시점에서 가비지 컬렉터의 마킹 과정이 발동되어 MyObject의 인스턴스가 가비지로 판단되고, 이후 정지와 회수 과정을 거쳐 메모리가 회수됩니다. 이 과정은 모두 자동으로 이루어지며, 개발자는 이 과정을 신경쓰지 않아도 됩니다. 이것이 바로 .NET 환경에서의 가비지 컬렉션의 동작 방식입니다.

 

하지만, .NET 환경 가비지 컬렉션에서 해제되지 않는 리소스가 존재합니다. 이들은 대개 네트워크 연결, 파일 핸들, 데이터베이스 연결, 그래픽 관련 리소스 등 외부 시스템과 연관된 리소스입니다.

 

아래의 C# 코드는 파일 핸들을 비관리 리소스로서 사용하는 예시를 보여줍니다.

public class UnmanagedResourceExample
{
    public void WriteToFile(string filePath, string content)
    {
        // FileStream는 파일 핸들을 내부적으로 사용하므로 비관리 리소스입니다.
        FileStream fileStream = new FileStream(filePath, FileMode.OpenOrCreate);

        // 파일 핸들을 사용하여 파일에 쓰기를 수행합니다.
        StreamWriter writer = new StreamWriter(fileStream);
        writer.Write(content);

        // 비관리 리소스를 명시적으로 해제해야 합니다.
        writer.Close();
        fileStream.Close();
    }
}

이 예시에서 FileStream은 파일 핸들을 내부적으로 사용하는데, 파일 핸들은 운영 체제에서 제공하는 리소스이므로 비관리 리소스라고 할 수 있습니다. 이런 비관리 리소스는 개발자가 직접 관리해야 합니다. Close 메소드를 호출하여 파일 핸들을 명시적으로 해제해야 합니다. 만약 이를 놓치게 되면, 리소스 누수가 발생하여 시스템 성능 저하를 가져올 수 있습니다.

이를 보완하고자 IDisposable 인터페이스와 using 블록이 사용되는데, 이를 통해 비관리 리소스의 생명주기를 효과적으로 관리할 수 있습니다.


메모리 관리

using System.IO;

public class UnmanagedResourceExample
{
    public void ProcessFile(string filePath)
    {
        // StreamReader는 IDisposable 인터페이스를 구현하고 있으므로
        // using 구문을 통해 비관리 리소스가 제대로 해제되도록 할 수 있습니다.
        using (StreamReader reader = new StreamReader(filePath))
        {
            string content = reader.ReadToEnd();
            // 파일 내용 처리 로직
        } // 여기서 reader.Dispose()가 호출되고, 내부에서 파일 핸들을 해제합니다.
    }
}

위 예제에서 StreamReader는 파일 핸들을 내부적으로 사용하므로 비관리 리소스라고 할 수 있습니다. StreamReader 객체는 IDisposable 인터페이스를 구현하므로, using 구문을 이용해 이 비관리 리소스가 사용이 끝나면 적절히 해제되도록 할 수 있습니다. using 구문이 끝나면 StreamReader의 Dispose 메서드가 호출되고, 이 메서드에서 파일 핸들을 해제하게 됩니다.

이렇게 IDisposable 인터페이스를 사용하면 GC의 관리 범위에 속하지 않는 비관리 리소스도 안전하게 처리할 수 있습니다.


IDisposable 인터페이스는 비관리 리소스를 효과적으로 관리하기 위한 중요한 도구입니다. 이 인터페이스를 이용하여 직접 비관리 리소스를 관리할 수 있습니다.

public class MyClass : IDisposable
{
    private Stream _myStream;

    public MyClass()
    {
        _myStream = new FileStream("file.txt", FileMode.Open);
    }

    public void Dispose()
    {
        _myStream?.Dispose();
        _myStream = null;
    }
}

비관리 리소스를 사용하는 클래스는 IDisposable 인터페이스를 구현해야 합니다. 이 인터페이스는 Dispose 메서드를 포함하고, 이 메서드에서는 비관리 리소스를 해제하는 로직을 작성해야 합니다.

using (MyClass myInstance = new MyClass())
{
    // myInstance 사용 코드
} // 여기서 자동으로 myInstance.Dispose()가 호출됩니다.

IDisposable 인터페이스를 구현하는 객체는 using 구문을 통해 생성하고 사용하는 것이 좋습니다. using 구문이 끝나면 자동으로 Dispose 메서드가 호출되어 비관리 리소스가 즉시 해제됩니다.

public void Dispose()
{
    try
    {
        _myStream?.Dispose();
    }
    catch (Exception ex)
    {
        // 예외 처리 코드
    }
    finally
    {
        _myStream = null;
    }
}

Dispose 메서드는 보통 비관리 리소스를 해제하는 코드를 포함하므로, 예외를 발생시킬 수 있는 코드를 작성할 수 있습니다. 이 경우, Dispose 메서드에서 예외 처리를 잘 해주어야 합니다.



Mutable 상태의 남용

C# .NET에서 "Mutable"은 '변경 가능하다'는 의미입니다.

객체가 Mutable하다는 것은 그 객체의 상태가 프로그램 실행 도중에 변경될 수 있다는 것을 의미합니다. 예를 들어, 우리가 어떤 클래스의 필드나 속성을 선언하고 그 값을 바꿀 수 있다면, 그 필드나 속성은 Mutable하다고 할 수 있습니다.

예를 들어, 다음과 같은 클래스를 봅시다.

public class Person
{
    public string Name { get; set; }
}

위의 Person 클래스에서 Name 속성은 Mutable하다고 볼 수 있습니다. 왜냐하면 Name의 값을 나중에 변경할 수 있기 때문입니다. 즉, Person 객체를 생성한 후에 Name 속성에 새로운 문자열을 할당할 수 있습니다.

그러나 Mutable 상태는 사용에 주의가 필요합니다. 여러 스레드에서 동시에 같은 객체의 상태를 변경하려는 경우, 예기치 않은 결과나 버그를 유발할 수 있습니다. 이런 이유로, 가능한 경우 Immutable(불변의) 상태를 사용하는 것이 좋습니다.


아래의 C# 코드는 mutable 상태의 남용 예시를 보여줍니다.

public class MutableStateExample
{
    public List<int> Numbers { get; set; }

    public MutableStateExample()
    {
        Numbers = new List<int> {1, 2, 3, 4, 5};
    }

    public void AddNumber(int number)
    {
        Numbers.Add(number);
    }
}

이 예제에서 Numbers 리스트는 mutable 상태입니다. AddNumber 메소드를 통해 리스트의 상태가 언제든지 변경될 수 있습니다. 이렇게 변경 가능한 상태가 여러 메소드에 걸쳐 사용되면, 예기치 않은 버그가 발생할 가능성이 있습니다.

만약 다음과 같이 두 개의 스레드가 동시에 AddNumber 메소드를 호출한다고 가정해봅시다.

var example = new MutableStateExample();

Task.Run(() => example.AddNumber(6));
Task.Run(() => example.AddNumber(7));

이 경우, 어떤 숫자가 먼저 추가될지는 알 수 없으며, 이는 병렬 처리나 멀티 스레딩 환경에서 문제가 될 수 있습니다. 이런 상황을 "race condition"이라고 합니다. 이는 mutable 상태의 남용이 초래할 수 있는 한 가지 문제입니다.

따라서 가능한 경우에는 immutable한 객체를 사용하거나, 상태 변경을 최소화하는 방법을 고려해야 합니다.


아래의 C# 코드는 immutable한 객체의 사용 예시를 보여줍니다.

public class ImmutableStateExample
{
    public IReadOnlyList<int> Numbers { get; }

    public ImmutableStateExample()
    {
        Numbers = new List<int> {1, 2, 3, 4, 5}.AsReadOnly();
    }

    public ImmutableStateExample AddNumber(int number)
    {
        var newNumbers = new List<int>(Numbers) {number};
        return new ImmutableStateExample(newNumbers);
    }

    private ImmutableStateExample(List<int> numbers)
    {
        Numbers = numbers.AsReadOnly();
    }
}

이 예제에서 Numbers 리스트는 IReadOnlyList<int> 형식으로 선언되어 변경이 불가능하게 만들어졌습니다. 새로운 숫자를 추가하기 위해서는 AddNumber 메소드를 통해 새로운 객체를 생성하고, 이 객체에 변경된 리스트를 저장합니다. 이 방식을 통해, 기존의 ImmutableStateExample 객체는 항상 그 상태를 유지하게 됩니다. 이는 병렬 처리나 멀티 스레딩 환경에서도 안전하게 객체를 사용할 수 있게 해줍니다.



마치며

C#으로 개발 중 놓치기 쉬운_시리즈가 끝났습니다👏👏👏 정말 짧죠?

 

다음에는 더욱 흥미로운 주제를 작성해보겠습니다, 감사합니다👍