애플리케이션에서 메인 스레드를 멈추지 않고 뭔가 다른 일을 하고 싶다면 보통은 스레드를 만들어서 사용할 수밖에 없다. 그런데, 스레드를 만들어서 사용하려고 하다 보면 스레드 간 데이터 전달이나 스레드를 멈추는 방법, 스레드가 종료되었을 때 알아내는 방법이 의외로 복잡해서 쉽게 접근하기가 어려운 면이 있다. 그런데, 그런 스레드를 쉽게 사용할 수 있게 도와주는 클래스가 있는데, 그 이름은 바로 BackgroundWork이다. 

 

BackgroundWork 클래스를 사용할 때 스레드에서 호출할 함수만 지정하면 간단하게 사용이 가능하고,

작업의 진척도를 알아 내거나, 완료되는 시점도 알 수 있고, 작업 도중 취소도 가능하다.

 

다음은 WPF 예제 프로그램을 실행했을 때의 모습이다.

예제 툴 실행 모습

Start를 누르면 가상의 백그라운드 작업을 시작하고, 진행률은 상단 프로그레스 바와 우측의 TextBlock으로 알려준다.

작업이 중단되거나 종료되면 그 결과 또한 우측의 TextBlock에 메시지로 표시해 주는 간단한 예제이다.

 

Start 버튼 아래에 있는 라디오버튼은 테스트를 위한 실행 옵션인데, Normal을 선택하면 정상적으로 종료가 되고, Fail은 스레드 작업 도중 문제가 생겨 강제로 중단한 경우를 가정한 것이고, 마지막 Error는 예외(Exception)가 발생해서 스레드가 예상치 못하게 종료된 경우를 가정한 것이다.

작업 도중 진행율 표시. 작업 중에는 Start 버튼이 Cancel 버튼으로 바뀐다.
Cancel 버튼을 누르면 작업이 중단된다.
성공적으로 종료.
실패한 경우.
예외가 발생한 경우.

주의) 비주얼 스튜디오에서 디버그 모드 상태에서 Error 옵션으로 실행하면, 비주얼 스튜디오가 예외를 먼저 잡아서 일시 중단되지만, 계속 진행을 하면 위의 스샷과 같은 결과를 볼 수 있다. 릴리즈 모드에서는 바로 저렇게 표시된다.

 

다음은 위의 예제의 WPF UI 코드이다.

<Window x:Class="WpfBackgroundWorkApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfBackgroundWorkApp"
        mc:Ignorable="d"
        Title="WPF-BackgroundWorkApp" Height="350" Width="577">
    <Grid>
        <Button x:Name="button" Content="Start" HorizontalAlignment="Left" Margin="17,70,0,0" VerticalAlignment="Top" Width="75" Click="buttonStart_Click"/>
        <ProgressBar x:Name="progressBar" HorizontalAlignment="Left" Height="15" Margin="17,29,0,0" VerticalAlignment="Top" Width="264"/>
        <TextBlock x:Name="textBlock" HorizontalAlignment="Left" Margin="151,70,0,0" TextWrapping="Wrap" Text="Press Start" VerticalAlignment="Top" Width="408" Height="239"/>
        <RadioButton x:Name="radioButtonSuccess" GroupName="option" Content="Normal" IsChecked="True" HorizontalAlignment="Left" Margin="27,109,0,0" VerticalAlignment="Top" Checked="radioButtonSuccess_Checked"/>
        <RadioButton x:Name="radioButtonFail" GroupName="option" Content="Fail" HorizontalAlignment="Left" Margin="27,129,0,0" VerticalAlignment="Top" Checked="radioButtonFail_Checked"/>
        <RadioButton x:Name="radioButtonError" GroupName="option" Content="Error" HorizontalAlignment="Left" Margin="27,149,0,0" VerticalAlignment="Top" Checked="radioButtonError_Checked"/>
    </Grid>
</Window>

 

아래는 c# 소스코드이다.

using System;
using System.ComponentModel;
using System.Windows;

namespace WpfBackgroundWorkApp
{
	/// <summary>
	/// Interaction logic for MainWindow.xaml
	/// </summary>
	public partial class MainWindow : Window
	{
		enum RunOption { Normal, Fail, Error }

		RunOption _runOption = RunOption.Normal;

		BackgroundWorker _worker;

		public MainWindow()
		{
			InitializeComponent();

			UpdateControls();
		}

		private void UpdateControls()
		{
			if (_worker == null)
				button.Content = "Start";
			else
				button.Content = "Cancel";
		}

		private void buttonStart_Click(object sender, RoutedEventArgs e)
		{
			// 백그라운드 작업이 이미 있는 지 확인
			if (_worker == null)
			{
				progressBar.Value = 0;

				_worker = new BackgroundWorker();
				_worker.WorkerReportsProgress = true;
				_worker.WorkerSupportsCancellation = true;

				_worker.DoWork += DoWork;
				_worker.ProgressChanged += ProgressChanged;
				_worker.RunWorkerCompleted += RunWorkerCompleted;

				// 작업 시작
				_worker.RunWorkerAsync();
			}
			else
			{
				// 작업 취소
				_worker.CancelAsync();
			}

			UpdateControls();
		}
		
		// 백그라운드 스레드 작업
		public void DoWork(object sender, DoWorkEventArgs e)
		{
			BackgroundWorker worker = sender as BackgroundWorker;

			int count = 10;
			for (int i = 0; i < count; ++i)
			{
				// 작업이 취소 되었을 경우
				if ((worker.CancellationPending == true))
				{
					e.Cancel = true;
					break;
				}

				// 작업이 실패 했을 때
				if (_runOption == RunOption.Fail && (i == count / 2))
				{
					e.Result = "Failed";
					return;
				}

				// 작업 중 예기치 않은 오류가 발생했을 때
				if (_runOption == RunOption.Error && (i == count / 2))
				{
					throw new Exception("Custom Error");
				}

				System.Threading.Thread.Sleep(100);

				worker.ReportProgress((i + 1) * 100 / count);
			}
		}

		// 진행 상황 - UI 스레드
		public void ProgressChanged(object sender, ProgressChangedEventArgs e)
		{
			progressBar.Value = e.ProgressPercentage;
			textBlock.Text = e.ProgressPercentage + "%";
		}

		// 작업 종료 - UI 스레드
		private void RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
		{
			string message = "";

			if (e.Cancelled)
			{
				message = "Cancelled";
			}
			else if (e.Error != null)
			{
				message = "Error: " + e.Error.ToString();
			}
			else if (e.Result != null)
			{
				message = e.Result.ToString();
			}
			else
			{
				message = "Success";
			}

			textBlock.Text = message;

			_worker = null;

			UpdateControls();
		}

		// 실행 옵션
		private void radioButtonSuccess_Checked(object sender, RoutedEventArgs e)
		{
			_runOption = RunOption.Normal;
		}

		private void radioButtonFail_Checked(object sender, RoutedEventArgs e)
		{
			_runOption = RunOption.Fail;
		}

		private void radioButtonError_Checked(object sender, RoutedEventArgs e)
		{
			_runOption = RunOption.Error;
		}
	}
}

 

스레드에서의 취소 처리는 worker.CancellationPending 값을 검사해서 e.Cancel에 true를 직접 넣고 리턴해야 한다. 스레드가 강제로 종료되는 게 아니라는 점에 주의해야 한다.

 

이 예제에서는 RunOption.Error인 경우 임의로 예외를 만들어서 throw 하는 코드를 작성해서 넣었는데, 실제 어플을 작성할 때는 필요 없는 코드이다. 이 예제에서는 딱히 오류 날 부분이 없어서 임의로 추가한 것이다.

 

작업 도중 실패(fail)가 난 경우, 즉 외부에서 중단한 게 아니라 스레드 내부에서 작업 도중에 더 이상 진행이 불가능할 때도 e.Cancel에 값을 넣어서 중단해도 되지 않을까 싶지만, 해당 변수는 그런 용도로 만들어진 게 아니라서 권장하지 않는다고 한다. 정상적으로 종료하되 e.Result에 값을 넣는 게 좋다.

 

MSDN 예제를 보면 성공했을 때에도 e.Result에 무언가 값을 넣어서 종료 처리할 수도 있다. 하지만, 이 예제에서는 성공했을 때는 아무 값을 안 넣은 상태를 완전한 성공으로 간주했다. 이건 코드 작성자의 선택사항이다.

 

이 예제에서는 e.Result에 string을 넣었으나 다른 타입의 변수, 예를 들어 오류코드를 담을 수 있는 int 값이나, enum 등을 사용하는 것도 가능하다.

 

RunWorketCompleted 함수에서 e.Cancelled가 true인 상태에서는 e.Result 변수에 접근할 수 없다. 예외가 발생하니 주의가 필요하다.

 

이 예제에서는 백그라운드 작업의 진행 여부를 판단하기 위해 _worker 변수가 null 인지 아닌지를 검사하도록 했는데, 이것도 코드 작성자의 선택사항이다.

 

_worker 변수를 딱히 쓸 일이 없는 경우, 클래스 멤버 변수가 아니라 로컬변수로 정의해서 사용해도 사실 상관없다. 내부적으로 레퍼런스가 있기 때문에 스레드 작업이 완전히 종료될 때까지 가비지 컬렉터가 지우지 않는다.

 

BackgroundWorker 클래스는 나온지 오래되었기 때문에 사실 꽤 안정적이고 하위 호환성도 좋다.

 

이 예제의 소스코드는 WPF용으로 만들어졌지만, WinForm에서도 약간만 수정하면 그대로 사용이 가능하다.

 

ProgressChanged, RunWorkerCompleted 함수는 WinForm과 WPF에서는 UI 스레드이기 때문에 UI 컨트롤을 사용하는 데 지장이 없다.

 

하지만, 이 예제를 C# 콘솔버전에서 돌리면 Main 스레드가 아닌 다른 스레드에서 실행된다. 

콘솔 버전에서는 스레드 ID가 제각각이다.

즉, 콘솔용으로는 적합하지 않을 수 있다. 애초에 콘솔 어플에서 백그라운드 스레드를 돌릴 이유가 없어서 고려를 안 했다는 얘기도 있다. 이건 그냥 참고로 알고 있자.

압축율 : 7z > zip >> lz4

압축속도 : lz4 >> zip >> 7z

압축해제속도 : lz4 >> zip > 7z

 

zip : 일반적으로 무난한 용도

7z(LZMA) : 네트워크로 전송해야 하는 파일

lz4(lz4hc) : 빠른 압축과 해제가 필요한 경우 (멀티코어 활용)

 

참고 링크 : catchchallenger.first-world.info/wiki/Quick_Benchmark:_Gzip_vs_Bzip2_vs_LZMA_vs_XZ_vs_LZ4_vs_LZO

 

 

stackoverflow.com/questions/222030/how-do-i-create-7-zip-archives-with-net

 

How do I create 7-Zip archives with .NET?

How can I create 7-Zip archives from my C# console application? I need to be able to extract the archives using the regular, widely available 7-Zip program. Here are my results with the examples

stackoverflow.com

7z(7-zip)을 c#에서 사용하는 방법을 구글에서 검색하면, 일단 제일 먼저 위의 글을 발견하게 된다.

뭔가 방법이 많은데.. 그만큼 고민이 많다는 뜻이 되겠다.

 

먼저 개념부터 잡고 가자. 7z은 파일을 압축할 때는 LZMA라고 하는 암호화 라이브러리로 압축을 한다. 좀 더 정확하게 말하자면, 파일 하나의 내용물만 압축할 때는 이 LZMA를 사용한다. 하지만, 여러 개의 압축된 파일을 하나로 묶고 싶다면? 이때 필요한 것이 7z이라는 파일 포맷이다. 7z 파일 포맷에는 파일명과 파일 사이즈, 날짜 등 각종 추가 정보도 포함되어 있다. 즉, 알맹이는 LZMA이고 껍데기는 7z이라는 파일의 형태인 것이다. (이런 방식은 zip 등 다른 압축 파일 형식도 유사하다)

 

7-zip 공식 홈페이지를 방문해보자. (www.7-zip.org/)

여기를 자세히 보면 LZMA SDK는 있지만, 7z SDK는 보이지 않는다. NuGet 홈페이지에서 LZMA 패키지(www.nuget.org/packages/LZMA-SDK/)를 살펴봐도 똑같다. 즉, LZMA 라이브러리를 사용하면, '파일 하나의 내용'을 압축할 수 있지만, 딱 거기까지이다. 7z 파일을 만들어 주는 기능이 아닌 것이다.  혹시나 싶어 *. lzma라는 파일 포맷이 따로 있는지 살펴보니 있긴 있는데, 원본 파일의 내용을 압축한 이후에 저장 시에는 파일 헤더에 뭔가를 붙여 넣어야 한다. 자세한 내용은 링크 참고 => svn.python.org/projects/external/xz-5.0.3/doc/lzma-file-format.txt

이런 *.lzma 파일 포맷을 사용하겠다면 헤더를 직접 만들어서 넣어 줘야 하기 때문에 귀찮다. 다행히 *. lzma라는 포맷을 반디집 등에서 압축해제를 지원해주기는 한다. 하지만, 대중화된 포맷이 아니라서 부담스럽다.

 

막상 찾아보니 그리 어렵지는 않다. 아래 코드 참고 바람.

void CompressFile(string outFile, string inFile)
{
	SevenZip.Compression.LZMA.Encoder coder = new SevenZip.Compression.LZMA.Encoder();
	FileStream input = new FileStream(inFile, FileMode.Open);
	FileStream output = new FileStream(outFile, FileMode.Create);

	// Write the encoder properties
	coder.WriteCoderProperties(output);

	// Write the decompressed file size.
	output.Write(BitConverter.GetBytes(input.Length), 0, 8);

	// Encode the file.
	coder.Code(input, output, input.Length, -1, null);
	output.Flush();
	output.Close();
}

void DecompressFile(string inFile, string outFile)
{
	SevenZip.Compression.LZMA.Decoder coder = new SevenZip.Compression.LZMA.Decoder();
	FileStream input = new FileStream(inFile, FileMode.Open);
	FileStream output = new FileStream(outFile, FileMode.Create);

	// Read the decoder properties
	byte[] properties = new byte[5];
	input.Read(properties, 0, 5);

	// Read in the decompress file size.
	byte[] fileLengthBytes = new byte[8];
	input.Read(fileLengthBytes, 0, 8);
	long fileLength = BitConverter.ToInt64(fileLengthBytes, 0);

	coder.SetDecoderProperties(properties);
	coder.Code(input, output, input.Length, fileLength, null);
	output.Flush();
	output.Close();
}

 

그럼, 7z.dll을 로딩해서 사용하는 방법은 어떨까? NuGet을 검색해보면 SevenZipSharp라는 패키지를 발견할 수 있다. 

www.nuget.org/packages/SevenZipSharp

사용 방법은 간단해 보이지만, 파일이 깨진다는 댓글도 있고, DLL 로딩하느라 그런지 느리다는 얘기도 있다. 게다가 유지보수 안 한 지 꽤 오래되어 보인다. 즉, 프로젝트가 중단되었다는 얘기이다. 게다가 이 패키지는 비주얼 스튜디오 내부의 NuGet 브라우저에서 검색도 안된다. NuGet 홈에서 수동으로 다운로드하여야 한다.

 

NuGet에서 다시 검색해 보면, 의외로 가장 인기 있는 패키지는 따로 있다.

www.nuget.org/packages/SharpCompress/ 

SharpCompress 라고 하는 패키지인데, 7z, zip, rar 등 다양한 포맷을 지원하는 걸로 보이고, 다운로드 수도 어마어마하다. 단, 이 패키지는 .Net Standard 2.0 / .Net 5.0 이상만 지원한다. 즉, 예전 윈도에서는 호환성 문제가 있다.

 

마지막으로 살펴 볼 방법은 스택오버플로우 질문 글에서 채택한 답변인데, 아주 간단하다.

그냥 7z.exe를 프로세스로 실행하는 방법이다. 너무 간단해서 허무할 수도 있는데, 그래서 NuGet을 아무리 뒤져 봐도 내가 원하는 심플한 라이브러리를 못 찾은 게 아닌가 싶다.

 

7z.exe를 사용해서 파일 하나를 압축하는 방법을 간단히 c# 콘솔 버전 코드로 작성하자면 이렇다.

        static bool Compress(string output, string input)
        {
            try
            {
                ProcessStartInfo info = new ProcessStartInfo();
                info.FileName = "7za.exe";
                info.Arguments = "a -t7z \"" + output + "\" \"" + input;

                info.WindowStyle = ProcessWindowStyle.Hidden;
                Process P = Process.Start(info);
                P.WaitForExit();

                int result = P.ExitCode;
                if (result != 0)
                {
                    Console.WriteLine("error!! code = " + result);
                    return false;
                }

                return true;
            }
            catch (Exception e)
            {
                Console.WriteLine(e.Message);
                return false;
            }
        }

        static void Main(string[] args)
        {
            if (Compress("d:\\dummy.7z", "d:\\dummy.txt"))
            {
                Console.WriteLine("Success.");
            }
            else
            {
                Console.WriteLine("Failed.");
            }

            Console.ReadKey();
        }

그런데, 본인이 작성한 코드를 자세히 보면 실행파일 이름이 7z.exe가 아니라 7za.exe이다.

 

7za는 NuGet에서 7z으로 검색하면 최상단에 뜨는 공식 패키지에서 설치할 수 있다.

링크 : www.nuget.org/packages/7-Zip.CommandLine/

일반적인 패키지와 달리 클래스 라이브러리는 없고, 저 7za.exe만 실행파일 위치에 복사해서 넣어주는 좀 이상한 패키지이다.

 

7za.exe와 7z.exe는 어떻게 다를까?

 

일단 7-zip 어플을 공식 홈페이지에서 다운로드하여서 설치하면, c:\Program Files\7-zip 혹은 c:\Program Files (x86)\7-Zip\ 폴더에 설치되는데, 거기에 있는 readme.txt를 먼저 읽어 보자.

 

7zFM.exe - 7-Zip File Manager - 압축파일을 다양한 방식으로 다룰 수 있는 UI 툴이다.
7-zip.dll - Plugin for Windows Shell - 탐색기 콘텍스트 메뉴에서 7z을 사용할 수 있게 해주는 dll
7-zip32.dll - Plugin for Windows Shell (32-bit plugin for 64-bit system) - 이건 64비트 윈도용 32비트 버전
7zg.exe - GUI module - 콘솔 버전처럼 사용하는 툴이지만 결과가 팝업 UI로 표시되는 툴
7z.exe - Command line version - 콘솔 버전이다! 개발자에게 필요한 건 이것!
7z.dll - 7-Zip engine module - 7z.exe에서는 사용하는 dll.
7z.sfx - SFX module (Windows version) - 셀프 압축 해제용 실행파일을 만들 때 필요한 모듈 (윈도용)
7zCon.sfx - SFX module (Console version) - 셀프 압축 해제용 실행파일을 만들 때 필요한 모듈 (콘솔용)

 

즉, 위의 내용을 보면 7z.exe 와 7z.dll 최소 두 개의 파일을 내가 개발한 어플에 같이 포함해서 배포해야 한다는 얘기가 된다. 용량은 1.3MB 정도로 얼마 되지는 않는다. 7z.exe가 dll을 따로 분리한 것은 추후 다른 포맷을 쉽게 추가하기 위한 구조로 보면 된다.

 

반면, 7za.exe 확장성은 없지만, 독립(stand alone) 실행이 가능한 버전이다. 7z.exe와 dll을 합쳐 놓은 거라 보면 되고, 용량은 절반 정도로 훨씬 가볍다. 그래도 여전히 다양한 포맷(7z, lzma, zip, 7z, lzma, cab, zip, gzip, bzip2, Z, tar)을 지원한다.

 

그 외에 7zr이라는 것도 있다. 7z 포맷만 필요한 경우에 사용하면 되는 더 가벼운 버전이지만, 리눅스 버전 등은 있지만 윈도용 바이너리는 따로 배포하는 거 같지는 않다. 7-zip 소스 코드를 보니 프로젝트 파일이 존재하기는 한다. 7zr이 꼭 필요하다면 소스를 컴파일해서 만들어서 써야 할 것으로 보인다.

 

길게 설명했지만, 결론은 7za.exe 파일 하나만 첨부하면 된다는 뜻.

 

그리고, NuGet에서 배포 중인 7za의 버전은 현재 16.04 버전이다. 더 최신 버전인 19.00을 다운로드하고 싶다면, 공식 홈페이지에서 다운로드하면 된다.

다운로드 링크 : www.7-zip.org/download.html

 

마지막 고민은 7za를 쓸 것인가 아니면 LZMA를 직접 사용할 것인가이다. 현재 진행 중인 프로젝트에서는 용량은 크지 않지만, 많은 파일을 압축 해제해야 하기 때문에 성능도 중요하다. 7za를 사용하면 호환성이 좋고 코드는 심플해지지만 매번 프로세스를 호출해야 하는 부담이 있다. 반면, LZMA 라이브러리를 사용하면 이미 메모리에 적재된 함수를 사용하는 거라서 아무래도 성능이 더 좋지 않을까 하는 생각이 든다. 다만 이렇게 하면 파일 포맷은 *.7z이 아닌 *.lzma나 혹은 아예 나만의 커스텀 포맷을 써야 한다는 부담이 있다.

 

추가 내용

 

c#용으로 나온 LZMA SDK를 실제로 사용해 보니, 압축속도는 7z.exe를 이용하는 것보다 2배 정도 느렸지만, 압축해제를 할 때는 2배 정도 빨랐다. c++ 코드를 그대로 c#으로 포팅한 것이다 보니 성능은 좀 애매하게 나오는 걸로 보인다.

 

또, 예전 라이브러리 같아서 사용하지 않으려고 했던 SevenZipSharp를 다시 찾아보니, 다른 이름으로 바뀌어서 명맥을 유지하고 있었다. www.nuget.org/packages/Squid-Box.SevenZipSharp/ 이 라이브러리로 테스트를 해보니 7z.exe를 직접 실행하는 것보다 압축과 해제 통틀어 1.4~2배 정도 빨랐다. 아무래도 이걸 써야겠다. 역시 구관이 명관인가 보다.

+ Recent posts