본문 바로가기

C#

C# BackgroundWorker로 백그라운드 작업 처리

UI 애플리케이션에서 시간이 걸리는 작업을 메인 스레드에서 실행하면 화면이 멈추는 문제가 발생합니다. BackgroundWorker는 이벤트 기반으로 백그라운드 작업, 진행률 보고, 취소를 간단히 처리할 수 있는 클래스로 WinForms/WPF에서 자주 사용됩니다.

1. BackgroundWorker란?

BackgroundWorker는 별도의 스레드에서 작업을 수행하고, 안전하게 UI 스레드로 진행률과 완료 결과를 전달합니다. 주요 구성 요소는 다음과 같습니다.

- DoWork: 실제 작업을 수행하는 이벤트입니다.
- ProgressChanged: 진행률을 UI에 반영하는 이벤트입니다.
- RunWorkerCompleted: 작업 완료/취소/오류를 처리하는 이벤트입니다.
- WorkerReportsProgress, WorkerSupportsCancellation: 진행률/취소 지원 여부를 설정하는 속성입니다.

2. 기본 사용 흐름

1) BackgroundWorker 생성 및 설정
2) 이벤트 핸들러 연결
3) RunWorkerAsync로 시작 (필요 시 인자 전달)
4) DoWork에서 작업 수행, ReportProgress/취소 체크
5) ProgressChanged/RunWorkerCompleted에서 UI 업데이트

3. WinForms 예제

아래 예제는 긴 루프 작업을 백그라운드로 수행하며 진행률 표시와 취소를 지원합니다. 폼에는 progressBar1, labelStatus, buttonStart, buttonCancel가 있다고 가정합니다.

using System.ComponentModel;
using System.Threading;
using System.Windows.Forms;

public partial class MainForm : Form
{
    private readonly BackgroundWorker _worker = new BackgroundWorker();

    public MainForm()
    {
        InitializeComponent();

        _worker.WorkerReportsProgress = true;
        _worker.WorkerSupportsCancellation = true;
        _worker.DoWork += Worker_DoWork;
        _worker.ProgressChanged += Worker_ProgressChanged;
        _worker.RunWorkerCompleted += Worker_RunWorkerCompleted;

        buttonStart.Click += (s, e) => StartWork();
        buttonCancel.Click += (s, e) => CancelWork();
    }

    private void StartWork()
    {
        if (_worker.IsBusy)
        {
            MessageBox.Show("이미 작업이 실행 중입니다.");
            return;
        }

        progressBar1.Value = 0;
        labelStatus.Text = "작업 시작";
        // 인자 전달이 필요하면 RunWorkerAsync(object arg) 사용
        _worker.RunWorkerAsync(100); // 예: 100회 반복
    }

    private void CancelWork()
    {
        if (_worker.IsBusy)
        {
            _worker.CancelAsync();
            labelStatus.Text = "취소 요청됨";
        }
    }

    private void Worker_DoWork(object sender, DoWorkEventArgs e)
    {
        var worker = (BackgroundWorker)sender;
        int iterations = (int)e.Argument;

        for (int i = 1; i <= iterations; i++)
        {
            if (worker.CancellationPending)
            {
                e.Cancel = true;
                return; // 즉시 종료
            }

            // 시간이 걸리는 작업 (예시)
            Thread.Sleep(50);

            // 진행률 보고 (0~100)
            int percent = (int)((i / (double)iterations) * 100);
            worker.ReportProgress(percent, $"{i}/{iterations} 처리 중...");
        }

        // 결과 전달 (필요 시)
        e.Result = "모든 작업 완료";
    }

    private void Worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
    {
        progressBar1.Value = e.ProgressPercentage;
        labelStatus.Text = e.UserState as string ?? $"진행률: {e.ProgressPercentage}%";
    }

    private void Worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {
        if (e.Cancelled)
        {
            labelStatus.Text = "작업이 취소되었습니다.";
        }
        else if (e.Error != null)
        {
            labelStatus.Text = $"오류 발생: {e.Error.Message}";
            MessageBox.Show(e.Error.ToString(), "오류", MessageBoxButtons.OK, MessageBoxIcon.Error);
        }
        else
        {
            labelStatus.Text = e.Result as string ?? "완료";
            progressBar1.Value = 100;
        }
    }
}

4. 취소와 예외 처리 팁

- 취소: DoWork 내부에서 CancellationPending을 주기적으로 확인하고 e.Cancel = true로 표시합니다. CancelAsync는 취소 요청일 뿐이며, 실제 종료는 DoWork 루프에서 처리합니다.
- 예외: DoWork에서 던진 예외는 RunWorkerCompleted의 e.Error로 전달됩니다. UI 스레드에서 안전하게 메시지를 표시하거나 로깅합니다.
- 진행률: ReportProgress(percent, userState)로 추가 상태 메시지를 함께 전달하면 UI 갱신이 편리합니다.

5. WPF에서의 사용

WPF에서도 동일한 방식으로 BackgroundWorker를 사용할 수 있습니다. ProgressChanged와 RunWorkerCompleted는 자동으로 UI 스레드(Dispatcher)에서 실행되므로 DoWork에서 직접 UI 컨트롤을 건드리지 않고 이벤트에서 UI를 갱신합니다.

using System.ComponentModel;
using System.Windows;

public partial class MainWindow : Window
{
    private readonly BackgroundWorker _worker = new BackgroundWorker();

    public MainWindow()
    {
        InitializeComponent();
        _worker.WorkerReportsProgress = true;
        _worker.WorkerSupportsCancellation = true;
        _worker.DoWork += (s, e) => { /* 긴 작업 수행 */ };
        _worker.ProgressChanged += (s, e) => { /* UI 갱신 */ };
        _worker.RunWorkerCompleted += (s, e) => { /* 완료 처리 */ };
    }
}

6. 언제 BackgroundWorker를 선택할까?

- 기존 WinForms/WPF 코드베이스에서 빠르게 백그라운드 작업을 붙이고 싶을 때 유용합니다.
- 진행률/취소가 필요한 단순 작업에 적합합니다.
- 새로운 프로젝트에서는 async/await(Task, IProgress, CancellationToken) 사용이 더 권장되는 추세입니다. 다만 레거시 UI 코드와의 호환성이나 이벤트 기반 패턴을 유지해야 하는 경우 BackgroundWorker가 실용적입니다.

7. 체크리스트

- DoWork에서 UI 접근 금지, ProgressChanged/RunWorkerCompleted에서만 UI 갱신합니다.
- IsBusy 확인 후 RunWorkerAsync 호출합니다.
- WorkerReportsProgress/WorkerSupportsCancellation을 적절히 설정합니다.
- e.Argument/e.Result로 입력/출력 데이터를 전달합니다.