[c#] 크로스-스레드인데 예외로 처리되지 않는 경우

예전에는 멀티스레드를 구현할 할 때 Thread나 ThreadPool을 이용했지만 요샌 Task를 주로 쓴다. 이건 ThreadPool을 기본으로 하여 기능들이 추가된 거다. 마이크로소프트는 비동기 작업을 할 때 Task 이용을 권장하지만 이게 좋기만 한 거는 아니다. ThreadPool을 이용하면 예외로 처리되는데 Task로 하면 예외로 처리되지 않는 경우들이 있다. 이러면 크로스-스레드로 인해 엉뚱하게 작동을 해도 프로그래머는 모를 수 있다.

오류error와 예외exception는 프로그래밍을 할 때 거의 같은 말들로 여겨진다. 이상 작동을 포괄적으로 오류라 한다면 예외는 구체적으로 exception 클래스에 대한 문제다. 오류가 일어나면 그에 대한 반응이 따른다. 예를 들어 정수를 받아야 하는 메서드에 문자열을 넘기면 이 메서드는 어떠한 반응이라도 해야 하는데 이게 저절로 이루어질 리 없다. 일일이 그 대처 방법들이 준비되어 있다.

우선 컴파일러는 예외를 raise하는데 이는 exception 클래스의 인스턴스를 만드는 작업이다. 그 다음에는 예외 객체를 throw하거나 어딘가에 저장하게 된다. 이어서 이것을 handle처리할 수 있고 하지 않을 수도 있다. 일반적으로는 raise -> throw -> handle의 과정을 거치지만 늘 이런 건 아니다. 어떠한 메서드가 호출이 된다는 건 여러 단계를 거치는 일이다. 이러한 단계는 콜 스택이라는 거에 저장된다. 예외가 raise되면 호출한 순서를 거슬러 올라가면서 handle할 주체를 찾는데 이런 과정을 propagate라 한다. 무척 어려운 단어인데 영어사전에는 보통 ‘전파하다’나 ‘번식하다’ 등의 잘 와닿지 않는 단어들로 나와 있다. 딱히 번역할 만한 말이 없어서 그냥 그대로 쓴다.

아래의 코드는 전형적인 크로스-스레드지만 예외가 raise될 뿐 throw되지는 않는다.

Task Task1;

private void button1_Click(object sender, EventArgs e)
{
    Task1 = new(Method1);

    Task1.Start();
}

void Method1()
{
    label1.Text = "a";
}

예외 객체는 쥐도 새도 모르게 AggregateException이라는 클래스에 저장된다. aggregate는 ‘總~’이라는 뜻이다. 저장된 예외 객체는 아래의 방법으로 확인할 수 있다.

private void button2_Click(object sender, EventArgs e)
{
    label1.Text = Task1.Exception.Message;
}

아래와 같이 Task가 끝날 때를 기다렸다가 throw하게 할 수도 있다.

private void button3_Click(object sender, EventArgs e)
{
Task1 = new(Method1);

Task1.Start();

try
{
Task1.Wait();
}
catch (Exception)
{
throw;
}
}

멀티스레드를 구현할 때 디버거에 의존하면 안 된다. 디버거가 크로스-스레드를 예외로 처리하지 않아도 이게 제대로 작동할 거라는 보장은 아니므로 크로스-스레드가 일어나지 않도록 주의해야 한다.