강건한 오류 처리를 갖춘 프로그램 작성
실습 개요
이번 실습에서는 지금까지 학습한 예외 처리 기법을 실제 프로그램에 적용해볼 것입니다.
우리는 간단한 파일 압축 프로그램을 구현할 것이며, 이 과정에서 다양한 예외 상황을 고려하고 처리하는 방법을 익힐 것입니다.
프로그램 요구사항
- 사용자가 지정한 디렉토리 내의 모든 파일을 압축
- 압축 진행 상황을 실시간으로 표시
- 압축된 파일을 사용자가 지정한 위치에 저장
- 다양한 예외 상황(파일 접근 권한 없음, 디스크 공간 부족 등)을 적절히 처리
기본 클래스 구조
먼저 우리 프로그램의 기본 클래스 구조를 설계해봅시다.
#include <iostream>
#include <string>
#include <vector>
#include <filesystem>
#include <fstream>
#include <stdexcept>
namespace fs = std::filesystem;
class Compressor {
private:
fs::path sourceDir;
fs::path destinationFile;
std::vector<fs::path> files;
public:
Compressor(const fs::path& src, const fs::path& dest)
: sourceDir(src), destinationFile(dest) {}
void scanDirectory();
void compress();
void showProgress(float percentage) const;
private:
void compressFile(const fs::path& file, std::ofstream& out);
};
사용자 정의 예외 클래스 설계
이제 우리 프로그램에 필요한 사용자 정의 예외 클래스들을 만들어봅시다.
class CompressionException : public std::runtime_error {
public:
CompressionException(const std::string& message)
: std::runtime_error("Compression error: " + message) {}
};
class FileAccessException : public CompressionException {
public:
FileAccessException(const fs::path& file)
: CompressionException("Cannot access file: " + file.string()) {}
};
class DiskFullException : public CompressionException {
public:
DiskFullException()
: CompressionException("Disk is full") {}
};
class InvalidDirectoryException : public CompressionException {
public:
InvalidDirectoryException(const fs::path& dir)
: CompressionException("Invalid directory: " + dir.string()) {}
};
메서드 구현
이제 Compressor
클래스의 메서드들을 구현해봅시다.
void Compressor::scanDirectory() {
if (!fs::exists(sourceDir) || !fs::is_directory(sourceDir)) {
throw InvalidDirectoryException(sourceDir);
}
for (const auto& entry : fs::recursive_directory_iterator(sourceDir)) {
if (fs::is_regular_file(entry)) {
files.push_back(entry.path());
}
}
if (files.empty()) {
throw CompressionException("No files found in the directory");
}
}
void Compressor::compress() {
std::ofstream out(destinationFile, std::ios::binary);
if (!out) {
throw FileAccessException(destinationFile);
}
for (size_t i = 0; i < files.size(); ++i) {
try {
compressFile(files[i], out);
showProgress((i + 1.0f) / files.size() * 100);
} catch (const std::exception& e) {
std::cerr << "Error compressing " << files[i] << ": " << e.what() << std::endl;
// 개별 파일 압축 실패는 전체 압축을 중단하지 않습니다.
}
}
}
void Compressor::compressFile(const fs::path& file, std::ofstream& out) {
std::ifstream in(file, std::ios::binary);
if (!in) {
throw FileAccessException(file);
}
// 여기서는 실제 압축 대신 단순히 파일을 복사합니다.
// 실제 압축 알고리즘은 이 부분에 구현될 것입니다.
out << in.rdbuf();
if (out.fail()) {
if (errno == ENOSPC) { // errno는 <cerrno> 헤더에 정의되어 있습니다.
throw DiskFullException();
} else {
throw CompressionException("Failed to write compressed data");
}
}
}
void Compressor::showProgress(float percentage) const {
std::cout << "\rProgress: " << std::fixed << std::setprecision(2)
<< percentage << "%" << std::flush;
}
메인 함수 구현
이제 이 모든 것을 종합하여 메인 함수를 작성해봅시다.
int main() {
try {
fs::path sourceDir, destinationFile;
std::cout << "Enter source directory: ";
std::cin >> sourceDir;
std::cout << "Enter destination file: ";
std::cin >> destinationFile;
Compressor compressor(sourceDir, destinationFile);
compressor.scanDirectory();
compressor.compress();
std::cout << "\nCompression completed successfully!" << std::endl;
} catch (const CompressionException& e) {
std::cerr << "Compression failed: " << e.what() << std::endl;
return 1;
} catch (const std::exception& e) {
std::cerr << "An unexpected error occurred: " << e.what() << std::endl;
return 1;
}
return 0;
}
예외 안전성 고려
이 프로그램에서 우리는 몇 가지 예외 안전성 문제를 고려해야 합니다.
- 리소스 관리 :
std::ofstream
과std::ifstream
은 RAII를 따르므로 예외 발생 시 자동으로 파일이 닫힙니다. - 강력한 예외 보장 :
compress
메서드는 개별 파일 압축 실패 시에도 계속 진행됩니다. 이는 부분적 성공을 허용하는 설계 결정입니다. - 예외 중립성 : 대부분의 메서드들이 예외를 그대로 전파하여 상위 레벨에서 처리할 수 있게 합니다.
성능 고려사항
예외 처리는 성능에 영향을 줄 수 있습니다.
다음과 같은 점을 고려해볼 수 있습니다.
- 예외는 실제로 발생할 때만 비용이 듭니다. 정상적인 실행 경로에서는 거의 오버헤드가 없습니다.
- 매우 빈번하게 발생하는 오류 상황에 대해서는 예외 대신 오류 코드를 사용하는 것을 고려할 수 있습니다.
- 예외 객체를 값으로 던지고 const 참조로 잡는 것이 일반적으로 가장 효율적입니다.
실습
- 이 프로그램에 실제 압축 알고리즘을 구현해보세요. (힌트 : zlib 라이브러리를 사용해볼 수 있습니다)
- 압축 해제 기능을 추가하고, 이에 따른 새로운 예외 클래스들을 설계하세요.
- 멀티스레딩을 도입하여 여러 파일을 동시에 압축할 수 있게 만드세요. 이 때 발생할 수 있는 동시성 관련 예외들을 고려하세요.
- 그래픽 사용자 인터페이스(GUI)를 추가하고, 예외 발생 시 사용자에게 적절한 메시지를 표시하세요.
- 단위 테스트를 작성하여 다양한 예외 상황을 테스트하세요.
결론
이번 실습을 통해 우리는 실제 프로그램에 예외 처리를 적용하는 방법을 배웠습니다. 예외 처리는 프로그램의 안정성과 신뢰성을 크게 향상시킬 수 있지만, 신중하게 설계하고 구현해야 합니다. 항상 예외 안전성과 성능을 고려하며, 적절한 수준의 오류 처리 전략을 선택하는 것이 중요합니다. 예외 처리는 C++의 핵심적인 기능 중 하나이며, 이를 능숙하게 다루는 것은 숙련된 C++ 프로그래머가 되는 데 필수적입니다. 계속해서 다양한 상황에서 예외 처리를 연습하고, 실제 프로젝트에 적용해보면서 경험을 쌓아나가시기 바랍니다.