CodeGym /행동 /C# SELF /고전적인 Thread에서의 예외

고전적인 Thread에서의 예외

C# SELF
레벨 61 , 레슨 3
사용 가능

1. 소개

Task나 비동기 메서드(async/await)로 작업할 때는 보통 익숙한 방식으로 예외를 잡을 수 있습니다 — await 주변에 try-catch를 쓰거나 ContinueWith를 사용하는 식으로요. 예외는 “사라지지” 않고 호출하는 쪽으로 반환됩니다.

하지만 Thread로 스레드를 만들면 상황이 복잡해집니다. 각 스레드는 자체 진입점(ThreadStart)과 실행 컨텍스트를 가집니다. 스레드 내부에서 처리되지 않은 예외가 발생하면 그 예외는 주 스레드로 "돌아오지" 않고 해당 스레드에서만 던져집니다.

  • .NET Framework: 한 스레드의 처리되지 않은 예외가 애플리케이션 전체를 종료시킵니다.
  • .NET (Core/5+): 그건 해당 스레드만 종료되며, 애플리케이션은 계속 실행됩니다(이로 인해 은밀한 버그가 생길 수 있음).

결론: 예외를 스레드 내부에서 잡지 않으면 대부분 보지 못할 겁니다. 그래서 스레드에서의 올바른 오류 처리는 필수입니다.

흥미로운 사실: Thread에서 날아간 예외는 잡기 힘든 닌자와 같아서, 사라지고 나중에 왜 로직이 동작하지 않는지 골머리를 앓게 만듭니다.

2. 스레드 내부에서 예외는 어떻게 동작하는가?

using System;
using System.Threading;

class Program
{
    static void Main()
    {
        Thread thread = new Thread(DoWork);
        thread.Start();

        // 스레드가 어떻게 되는지 보기 위해 종료를 기다립니다
        thread.Join();

        Console.WriteLine("Main이 정상적으로 종료되었습니다");
    }

    static void DoWork()
    {
        Console.WriteLine("별도 스레드에서 작업 중...");
        throw new Exception("큰일이다! 스레드에서 오류가 발생했습니다.");
    }
}

플랫폼(.NET Framework 구버전 또는 최신 .NET Core/5/6/7/8/9)에 따라 동작이 달라집니다: 애플리케이션 전체가 죽거나, 아니면 해당 스레드만 죽습니다. 하지만 중요한 건 — 예외가 기본 스레드로 오지 않으니 외부에서 처리할 수 없다는 점입니다.

중요! thread.Join()try-catch로 감싸는 시도는 다른 스레드의 예외를 잡지 못합니다 — 예외는 그 스레드 안에서 "살고" "죽기" 때문입니다.

3. Thread에서 예외를 어떻게 잡아야 할까?

스레드 내부 — Thread 생성자에 전달한 함수 안에서만 가능합니다. 예외를 던질 수 있는 모든 코드를 try-catch로 감싸세요.

static void DoWork()
{
    try
    {
        Console.WriteLine("작업 중...");
        throw new Exception("또 무언가 잘못되었네요!");
    }
    catch (Exception ex)
    {
        Console.WriteLine($"[스레드] 예외 잡음: {ex.Message}");
        // 여기서 로깅하거나 UI/서버로 전송하는 등 처리 가능
    }
}

스레드에서의 예외 처리는 스레드 코드의 책임입니다. 호출하는 코드가 자동으로 오류를 잡아주리라 기대하면 안 됩니다.

4. 메인 스레드에서 다른 스레드의 문제를 어떻게 알 수 있을까?

실제 애플리케이션에서는 오류 정보를 메인 스레드로 전달하는 것이 중요합니다.

  • 스레드 안전한 메커니즘(예: ConcurrentQueue<Exception>)을 사용해 예외를 스레드에서 전달하세요.
  • 작업 스레드에서 이벤트/델리게이트를 발생시키세요.
  • 가능하면 Task를 선호하세요 — await에서 예외를 "자동"으로 전달해줍니다.

예제: 오류를 특정 장소에 모으기

using System;
using System.Threading;

class Program
{
    static Exception? threadException = null;

    static void Main()
    {
        Thread thread = new Thread(DoWork);
        thread.Start();
        thread.Join();

        if (threadException != null)
        {
            Console.WriteLine($"다른 스레드에서 오류가 발생했습니다: {threadException.Message}");
        }
        else
        {
            Console.WriteLine("스레드가 오류 없이 종료되었습니다.");
        }
    }

    static void DoWork()
    {
        try
        {
            throw new Exception("다른 스레드에서 문제 발생!");
        }
        catch (Exception ex)
        {
            threadException = ex;
        }
    }
}

참고: 이 접근 방식은 동기적 대기(Join())에 적합합니다. 스레드가 백그라운드에서 계속 실행되거나 오류가 많다면 ConcurrentQueue<Exception>, 이벤트 등 다른 통신 수단을 사용하세요.

5. Task와 비교: 왜 오류 처리 방식이 더 쉬운가

async Task FooAsync()
{
    throw new Exception("작업에서의 오류!");
}

try
{
    await FooAsync();
}
catch (Exception ex)
{
    Console.WriteLine($"오류 잡음: {ex.Message}");
}

여기서는 모든 것이 투명합니다: 오류는 당신이 await하는 위치까지 도달합니다. 고전적인 Thread에서는 오류가 스레드 내부에 남아 특별한 조치 없이는 전달되지 않습니다. 이것이 Task와 현대적 추상화를 선호하는 이유 중 하나입니다.

6. 실전 예

UI 애플리케이션(WPF/WinForms)에서는 인터페이스가 블로킹되지 않도록 스레드를 사용합니다. 예외를 처리하지 않으면 '회색 화면'이나 이상한 정지 상태가 발생할 수 있습니다.

나쁜 예(오류 처리 없는 스레드)

Thread thread = new Thread(() =>
{
    // 오래 생각 중
    Thread.Sleep(5000);
    throw new Exception("모든 게 끝났다!"); // 아무도 잡지 못함
});
thread.Start();

좋은 예(오류를 잡아 사용자에게 알림)

Thread thread = new Thread(() =>
{
    try
    {
        Thread.Sleep(5000);
        throw new Exception("뭔가 잘못되었음");
    }
    catch (Exception ex)
    {
        // MessageBox를 띄우거나, 로깅하거나 UI로 전달 가능
        Console.WriteLine($"스레드 오류: {ex.Message}");
    }
});
thread.Start();

7. 유용한 팁

스레드 처리되지 않은 예외에 대한 글로벌 훅

AppDomain.CurrentDomain.UnhandledException += (sender, args) =>
{
    Console.WriteLine($"글로벌로 잡힌 오류: {((Exception)args.ExceptionObject).Message}");
};

Thread thread = new Thread(() =>
{
    throw new Exception("엑스터미네이투스!");
});
thread.Start();

AppDomain.CurrentDomain.UnhandledException 핸들러는 스레드에서 처리되지 않은 예외에 대해 호출되지만, .NET Framework에서는 스레드를 부활시키거나 프로세스 종료를 막을 수 없습니다. .NET (Core/5+)에서는 보통 에러를 로그하고 애플리케이션이 계속 실행될 수 있습니다(다른 스레드가 살아있다면).

예외 처리 차이 — Thread vs Task

Thread
Task
어디서 잡을까 스레드 내부 호출 코드(await, ContinueWith 등)
결과 예외가 사라지거나/스레드를 죽임(.NET Framework에서는 애플리케이션 전체를 죽일 수 있음) 예외가 대기 지점(await)까지 도달함
위로 알림 명시적으로만 (변수, 이벤트, 큐) await로 전달, 동기 대기 시 AggregateException
로깅 스레드 코드에서 수동으로 해야 함 보통 try-catchawait 주변에서 처리
컨텍스트 부모 스레드와 독립적 Task는 호출 코드의 동기화 컨텍스트(예: WPF의 UI 컨텍스트)를 사용함

8. Thread에서 예외 작업 시 흔한 실수들

실수 #1: 스레드 내부에서 예외를 잡지 않음.
결과적으로 애플리케이션의 일부가 암묵적으로 종료되거나 때로는 전체 프로세스가 종료될 수 있으며, 진단이 어려워집니다.

실수 #2: 메인 스레드에서 다른 스레드의 예외를 '잡으려' 함.
이건 작동하지 않습니다: try-catchthread.Join()thread.Start()를 감싸도 다른 스레드 내부에서 던져진 예외를 잡지 못합니다.

실수 #3: 오류 정보를 잃어버림.
스레드가 실패했는데 예외를 명시적으로 전달하지 않으면(변수, 큐, 이벤트 등) 원인과 세부 정보를 알 수 없습니다. 이는 '유령' 버그로 이어집니다.

실수 #4: 로깅 없음.
스레드에서의 오류는 항상 로깅하세요. '별 일 아닐 거야'라고 생각하면 안 됩니다.

코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION