본문 바로가기
📁 [4] 개발자 정보 & 코드 노트/C#

C# Windows Forms 강의 70편: Task와 BackgroundWorker를 활용한 비동기 파일 처리

by wawManager 2025. 4. 13.

 

1. 강의 개요

이번 강의에서는 TaskBackgroundWorker를 활용해 비동기 파일 처리를 구현합니다.
비동기 프로그래밍은 긴 작업(예: 대용량 파일 읽기, 복사, 다운로드 등)이 실행되는 동안
UI가 멈추지 않고 응답성을 유지하도록 도와줍니다.
실습에서는 대용량 파일 복사를 예제로 들어 비동기 처리의 장점을 배워봅니다.


2. 학습 목표

  • Taskasync/await를 사용한 비동기 작업 구현
  • BackgroundWorker를 활용한 비동기 작업 구현
  • 작업 중 진행 상황(Progress)을 ProgressBar로 표시
  • 비동기 작업 중 UI 응답성 유지

3. 기능 요구사항

필수 기능

1️⃣ 파일 복사 작업:

  • 원본 파일에서 대상 파일로 비동기 복사

2️⃣ 진행 상황 표시:

  • ProgressBar를 통해 복사 진행 상황 표시

3️⃣ UI 응답성 유지:

  • 파일 복사 작업 중에도 버튼 클릭 등 UI 작업 가능

4️⃣ 취소 기능:

  • 작업 중 취소 버튼으로 파일 복사 중단

4. 실습: 비동기 파일 복사 애플리케이션 제작

1️⃣ 폼 구성

  • 폼(Form) 이름: Form1
  • 컨트롤 배치

컨트롤 타입 이름 위치 크기

TextBox txtSourcePath 폼 상단 왼쪽 (300 x 30)
Button btnBrowseSource 폼 상단 오른쪽 (100 x 30)
TextBox txtDestinationPath 폼 중앙 왼쪽 (300 x 30)
Button btnBrowseDestination 폼 중앙 오른쪽 (100 x 30)
Button btnStart 폼 하단 왼쪽 (100 x 30)
Button btnCancel 폼 하단 오른쪽 (100 x 30)
ProgressBar progressBar 폼 하단 중앙 (400 x 30)

📌 폼 디자인 예시:

--------------------------------------------------
| [Source Path: TextBox]  [Browse Source 버튼]    |
--------------------------------------------------
| [Destination Path: TextBox]  [Browse Dest 버튼] |
--------------------------------------------------
|           [Start 버튼]       [Cancel 버튼]      |
--------------------------------------------------
|                    [ProgressBar]               |
--------------------------------------------------

2️⃣ 코드 작성

(1) 파일 복사를 위한 Task 구현

using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace WindowsFormsApp_AsyncFileCopy
{
    public partial class Form1 : Form
    {
        private CancellationTokenSource _cancellationTokenSource;

        public Form1()
        {
            InitializeComponent();
        }

        // 파일 복사 시작
        private async void btnStart_Click(object sender, EventArgs e)
        {
            string sourcePath = txtSourcePath.Text.Trim();
            string destinationPath = txtDestinationPath.Text.Trim();

            if (string.IsNullOrEmpty(sourcePath) || string.IsNullOrEmpty(destinationPath))
            {
                MessageBox.Show("소스 및 대상 경로를 입력하세요.", "오류", MessageBoxButtons.OK, MessageBoxIcon.Error);
                return;
            }

            _cancellationTokenSource = new CancellationTokenSource();
            progressBar.Value = 0;

            try
            {
                await CopyFileAsync(sourcePath, destinationPath, _cancellationTokenSource.Token);
                MessageBox.Show("파일 복사가 완료되었습니다.", "완료", MessageBoxButtons.OK, MessageBoxIcon.Information);
            }
            catch (OperationCanceledException)
            {
                MessageBox.Show("파일 복사가 취소되었습니다.", "취소됨", MessageBoxButtons.OK, MessageBoxIcon.Warning);
            }
            catch (Exception ex)
            {
                MessageBox.Show($"오류 발생: {ex.Message}", "오류", MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
            finally
            {
                _cancellationTokenSource.Dispose();
                _cancellationTokenSource = null;
            }
        }

        // 비동기 파일 복사
        private async Task CopyFileAsync(string sourcePath, string destinationPath, CancellationToken cancellationToken)
        {
            const int bufferSize = 81920; // 80KB 버퍼
            var buffer = new byte[bufferSize];

            using (var sourceStream = new FileStream(sourcePath, FileMode.Open, FileAccess.Read))
            using (var destinationStream = new FileStream(destinationPath, FileMode.Create, FileAccess.Write))
            {
                long totalBytes = sourceStream.Length;
                long totalCopied = 0;
                int bytesRead;

                while ((bytesRead = await sourceStream.ReadAsync(buffer, 0, buffer.Length, cancellationToken)) > 0)
                {
                    await destinationStream.WriteAsync(buffer, 0, bytesRead, cancellationToken);
                    totalCopied += bytesRead;

                    // ProgressBar 업데이트
                    int progress = (int)((double)totalCopied / totalBytes * 100);
                    progressBar.Invoke(new Action(() => progressBar.Value = progress));

                    cancellationToken.ThrowIfCancellationRequested(); // 취소 요청 확인
                }
            }
        }

        // 복사 취소
        private void btnCancel_Click(object sender, EventArgs e)
        {
            _cancellationTokenSource?.Cancel();
        }
    }
}

(2) 파일 경로 선택 기능

        // 소스 파일 선택
        private void btnBrowseSource_Click(object sender, EventArgs e)
        {
            using (OpenFileDialog openFileDialog = new OpenFileDialog())
            {
                if (openFileDialog.ShowDialog() == DialogResult.OK)
                {
                    txtSourcePath.Text = openFileDialog.FileName;
                }
            }
        }

        // 대상 경로 선택
        private void btnBrowseDestination_Click(object sender, EventArgs e)
        {
            using (SaveFileDialog saveFileDialog = new SaveFileDialog())
            {
                if (saveFileDialog.ShowDialog() == DialogResult.OK)
                {
                    txtDestinationPath.Text = saveFileDialog.FileName;
                }
            }
        }

(3) Designer 코드

        private void InitializeComponent()
        {
            this.txtSourcePath = new TextBox();
            this.btnBrowseSource = new Button();
            this.txtDestinationPath = new TextBox();
            this.btnBrowseDestination = new Button();
            this.btnStart = new Button();
            this.btnCancel = new Button();
            this.progressBar = new ProgressBar();

            // Source Path TextBox 설정
            this.txtSourcePath.Location = new System.Drawing.Point(10, 10);
            this.txtSourcePath.Size = new System.Drawing.Size(300, 30);

            // Browse Source Button 설정
            this.btnBrowseSource.Location = new System.Drawing.Point(320, 10);
            this.btnBrowseSource.Size = new System.Drawing.Size(100, 30);
            this.btnBrowseSource.Text = "Browse Source";
            this.btnBrowseSource.Click += new EventHandler(this.btnBrowseSource_Click);

            // Destination Path TextBox 설정
            this.txtDestinationPath.Location = new System.Drawing.Point(10, 50);
            this.txtDestinationPath.Size = new System.Drawing.Size(300, 30);

            // Browse Destination Button 설정
            this.btnBrowseDestination.Location = new System.Drawing.Point(320, 50);
            this.btnBrowseDestination.Size = new System.Drawing.Size(100, 30);
            this.btnBrowseDestination.Text = "Browse Destination";
            this.btnBrowseDestination.Click += new EventHandler(this.btnBrowseDestination_Click);

            // Start Button 설정
            this.btnStart.Location = new System.Drawing.Point(10, 100);
            this.btnStart.Size = new System.Drawing.Size(100, 30);
            this.btnStart.Text = "Start";
            this.btnStart.Click += new EventHandler(this.btnStart_Click);

            // Cancel Button 설정
            this.btnCancel.Location = new System.Drawing.Point(120, 100);
            this.btnCancel.Size = new System.Drawing.Size(100, 30);
            this.btnCancel.Text = "Cancel";
            this.btnCancel.Click += new EventHandler(this.btnCancel_Click);

            // ProgressBar 설정
            this.progressBar.Location = new System.Drawing.Point(10, 150);
            this.progressBar.Size = new System.Drawing.Size(400, 30);

            // Form 설정
            this.ClientSize = new System.Drawing.Size(450, 200);
            this.Controls.Add(this.txtSourcePath);
            this.Controls.Add(this.btnBrowseSource);
            this.Controls.Add(this.txtDestinationPath);
            this.Controls.Add(this.btnBrowseDestination);
            this.Controls.Add(this.btnStart);
            this.Controls.Add(this.btnCancel);
            this.Controls.Add(this.progressBar);
            this.Text = "비동기 파일 복사";
        }

3️⃣ 실행 결과

1️⃣ 파일 선택 및 경로 설정

  • 소스 및 대상 파일 경로를 선택

2️⃣ 비동기 파일 복사

  • "Start" 버튼 클릭 시 파일 복사 시작 → ProgressBar로 진행 상황 표시

3️⃣ 복사 취소

  • "Cancel" 버튼 클릭 시 복사 작업 중단

4️⃣ UI 응답성 유지

  • 복사 중에도 UI 작업 가능

5. 주요 개념 요약

  • Task: 비동기 작업 처리를 위한 클래스
  • CancellationToken: 비동기 작업 취소를 위한 토큰
  • ProgressBar: 작업 진행 상황을 시각적으로 표시
  • FileStream: 파일 읽기/쓰기 스트림