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

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

🧰개발 언어/C#

C#으로 개발 중 놓치기 쉬운 부분_1(Dispose Pattern, LINQ, 병렬 및 비동기, Null Reference)

서브윌 2023. 2. 1. 01:33

요약

Dispose Pattern 이해 IDisposable 인터페이스를 사용하여 비관리 리소스를 해제하는 것은 중요하지만, Dispose Pattern에 대한 이해 없이 사용하면 리소스 누수 문제가 발생합니다.
LINQ 사용 LINQ는 매우 강력하지만, 효율적이지 않게 사용되면 성능 문제를 초래할 수 있습니다. 특히 데이터베이스와의 상호 작용에서 중요합니다.
병렬 및 비동기 프로그래밍 async와 await 키워드, Task 등을 사용하여 병렬 및 비동기 코드를 작성하는 것은 C#의 중요한 기능입니다. 하지만 잘못 사용하면 데드락이 발생하거나 예측하지 못한 동작이 발생합니다.
Null Reference C# 개발에서 가장 흔하게 발생하는 문제 중 하나는 null 참조입니다. null 가능성을 컴파일 타임에 검사하는 nullable reference types 기능을 활용하기 위해서는 잘이해하고 사용해야 합니다.

 



Dispose Pattern 이해

IDisposable 인터페이스는 C#에서 관리되지 않는 리소스를 적절하게 해제하는 데 사용됩니다. 이 인터페이스에는 Dispose라는 단일 메소드가 포함되어 있습니다. 개발자는 Dispose 메소드 내부에서 사용이 끝난 비관리 리소스를 해제하는 코드를 작성해야 합니다.

다음은 IDisposable 인터페이스를 사용하는 간단한 예제입니다.

public class ResourceHolder : IDisposable
{
    // Assume that this is a managed resource
    private IntPtr resource; 
    private bool disposed = false;

    public ResourceHolder()
    {
        this.resource = /* Allocate the resource */;
    }

    public void UseResource()
    {
        if (this.disposed)
        {
            throw new ObjectDisposedException("ResourceHolder");
        }

        // Use the resource
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (!this.disposed)
        {
            if (disposing)
            {
                // Dispose other managed resources.
            }

            // Dispose unmanaged resources.
            ReleaseResource(this.resource);
            this.disposed = true;
        }
    }

    ~ResourceHolder()
    {
        Dispose(false);
    }

    private void ReleaseResource(IntPtr resource)
    {
        /* Release the resource */
    }
}

위 코드에서, ResourceHolder 클래스는 IDisposable 인터페이스를 구현하고 있습니다. Dispose 메소드는 GC.SuppressFinalize(this)를 호출하여 파이널라이저가 호출되지 않도록 막습니다.

또한, Dispose 메소드는 파라미터를 true로 하여 Dispose(bool) 메소드를 호출하며, 이 메소드는 관리되지 않는 리소스를 해제합니다. 파이널라이저에서는 Dispose(bool)를 false 파라미터와 함께 호출하여 관리되지 않는 리소스만 해제합니다.

IDisposable을 사용할 때 가장 좋은 패턴은 using 문을 사용하는 것입니다. 이를 사용하면 리소스를 사용하는 블록이 끝나면 자동으로 Dispose 메소드가 호출되어 리소스가 정리됩니다.

using (ResourceHolder rh = new ResourceHolder())
{
    rh.UseResource();
} // Dispose is called here

 



LINQ 사용

LINQ (Language Integrated Query)는 데이터를 조회하고 조작하는 강력한 기능을 제공하는 C#의 구성 요소입니다. LINQ를 효율적으로 사용하려면 연산을 최소화하고 지연 실행을 활용하는 것이 중요합니다. 또한, 가능하면 DB에서 데이터를 가져오기 전에 필요한 필터를 적용해야 합니다.

다음은 Entity Framework를 사용하여 LINQ를 활용한 데이터베이스와의 효율적인 상호 작용 예제입니다.

using (var context = new MyDbContext())
{
    // 지연 실행을 사용하여 데이터베이스 쿼리를 최적화합니다.
    // Where 절은 DB에서 실행되므로, 메모리에서 필요없는 데이터를 필터링하지 않습니다.
    var query = context.Users
        .Where(u => u.IsActive)
        .OrderBy(u => u.LastName)
        .ThenBy(u => u.FirstName);

    // ToList를 호출할 때까지 쿼리는 실행되지 않습니다.
    var activeUsers = query.ToList();

    foreach (var user in activeUsers)
    {
        Console.WriteLine($"Name: {user.FirstName} {user.LastName}");
    }
}

이 코드는 LINQ를 사용하여 활성 사용자를 성과 이름순으로 정렬하고 리스트로 반환합니다. ToList 메소드를 호출하기 전까지는 실제 쿼리가 데이터베이스에서 실행되지 않는 "지연 실행"을 활용하고 있습니다.

또한, LINQ 쿼리는 Entity Framework에 의해 SQL로 변환되어 DB에서 실행됩니다. 따라서 필요한 데이터만 가져오므로 메모리 사용량이 최적화되며, 네트워크 트래픽 또한 최소화합니다. 이와 같이 LINQ는 코드를 간결하게 유지하면서 데이터베이스와의 효율적인 상호 작용을 가능하게 합니다.

 



병렬 및 비동기 프로그래밍

병렬 및 비동기 프로그래밍은 매우 강력한 도구이지만, 잘못 사용하면 데드락이 발생하거나 예측치 못한 동작이 발생할 수 있습니다. C#에서는 이를 방지하기 위해 async와 await 키워드를 제공합니다. 이들은 비동기 작업을 쉽게 처리하게 해주며, 데드락이나 레이스 컨디션을 방지할 수 있습니다.

아래는 간단한 비동기 메서드를 사용한 예제입니다.

public async Task<string> DownloadContentAsync(string url)
{
    using (HttpClient client = new HttpClient())
    {
        string content = await client.GetStringAsync(url);
        return content;
    }
}

public async Task ProcessUrlsAsync(string[] urls)
{
    foreach (var url in urls)
    {
        string content = await DownloadContentAsync(url);
        Console.WriteLine($"Content from {url} is {content.Length} characters long.");
    }
}

ProcessUrlsAsync 메서드에서는 URL 배열을 순회하면서 각 URL의 내용을 다운로드합니다. await 키워드를 사용하면 DownloadContentAsync 작업이 완료될 때까지 메서드의 실행을 멈춥니다. 이 때, ProcessUrlsAsync 메서드는 블로킹되지 않고 제어를 호출자에게 반환하므로, 다른 작업을 수행할 수 있습니다. DownloadContentAsync 작업이 완료되면 ProcessUrlsAsync 메서드는 다음 줄을 실행합니다.

이렇게 비동기 메서드를 사용하면, I/O 바운드 작업(예: 파일 또는 네트워크 작업)을 실행하는 동안 CPU를 블로킹하지 않고 다른 작업을 수행할 수 있습니다. 이는 애플리케이션의 처리량을 향상시키고, 반응성을 높이는 데 도움이 됩니다.

하지만 비동기 프로그래밍에도 주의가 필요합니다. 여러 비동기 작업을 동시에 실행하면서 그 결과에 접근하려면 레이스 컨디션이 발생할 수 있으므로, 이 경우 동기화 메커니즘을 사용해야 합니다. 또한, async void 메서드는 예외를 제대로 처리할 수 없으므로, 이벤트 핸들러 외에는 async Task를 사용해야 합니다.

 



Null Reference

C# 8.0에서 소개된 Nullable Reference Types는 null 참조 예외를 방지하는 데 도움이 됩니다. 이 기능을 활성화하면, 참조 타입에 null을 할당하려고 하면 컴파일러 경고가 발생합니다. 이는 프로그램이 실행되는 도중에 NullReferenceException을 일으키는 것을 방지할 수 있습니다.

Nullable Reference Types 기능을 활성화하는 방법은 프로젝트 파일(.csproj)에 <Nullable>enable</Nullable>을 추가하거나, 소스 코드 파일 상단에 #nullable enable 지시문을 추가하는 것입니다.

다음은 Nullable Reference Types 기능을 사용하는 예제입니다.

#nullable enable
public class Example
{
    public string NonNullableString { get; set; }
    public string? NullableString { get; set; }

    public Example(string nonNullableString, string? nullableString)
    {
        NonNullableString = nonNullableString ?? throw new ArgumentNullException(nameof(nonNullableString));
        NullableString = nullableString;
    }

    public void PrintLengths()
    {
        Console.WriteLine($"Non-nullable string length: {NonNullableString.Length}");

        // 이 코드는 null을 확인하므로 안전합니다.
        if (NullableString != null)
        {
            Console.WriteLine($"Nullable string length: {NullableString.Length}");
        }
    }
}

이 코드에서 NonNullableString 속성은 null이 될 수 없는 참조 타입이며, NullableString 속성은 null이 될 수 있는 참조 타입입니다. 생성자에서는 nonNullableString 매개변수가 null인지 확인하여 NullReferenceException을 방지하고 있습니다.

Nullable Reference Types 기능은 코드의 명시성을 높여주고, 가능한 NullReferenceException을 미리 방지할 수 있도록 돕기 때문에 효과적인 코드 품질 향상 도구가 될 수 있습니다.



마치며

이 외에도 C#으로 개발 중 놓치거나 사용하지 않아 아쉬운 문법이나 패턴들이 존재합니다😄

다음 시간에는 가비지 컬렉션과 메모리 관리, Mutable 상태의 남용 등에 대해 알아보겠습니다.