icon안동민 개발노트

강건한 오류 처리를 갖춘 프로그램 작성


실습 개요

 이번 실습에서는 지금까지 학습한 예외 처리 기법을 실제 프로그램에 적용해볼 것입니다. 우리는 간단한 파일 압축 프로그램을 구현할 것이며, 이 과정에서 다양한 예외 상황을 고려하고 처리하는 방법을 익힐 것입니다.

프로그램 요구사항

  1. 사용자가 지정한 디렉토리 내의 모든 파일을 압축
  2. 압축 진행 상황을 실시간으로 표시
  3. 압축된 파일을 사용자가 지정한 위치에 저장
  4. 다양한 예외 상황(파일 접근 권한 없음, 디스크 공간 부족 등)을 적절히 처리

기본 클래스 구조

 먼저 우리 프로그램의 기본 클래스 구조를 설계해봅시다.

#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;
}

예외 안전성 고려

 이 프로그램에서 우리는 몇 가지 예외 안전성 문제를 고려해야 합니다.

  1. 리소스 관리 : std::ofstreamstd::ifstream은 RAII를 따르므로 예외 발생 시 자동으로 파일이 닫힙니다.
  2. 강력한 예외 보장 : compress 메서드는 개별 파일 압축 실패 시에도 계속 진행됩니다. 이는 부분적 성공을 허용하는 설계 결정입니다.
  3. 예외 중립성 : 대부분의 메서드들이 예외를 그대로 전파하여 상위 레벨에서 처리할 수 있게 합니다.

성능 고려사항

 예외 처리는 성능에 영향을 줄 수 있습니다. 다음과 같은 점을 고려해볼 수 있습니다.

  1. 예외는 실제로 발생할 때만 비용이 듭니다. 정상적인 실행 경로에서는 거의 오버헤드가 없습니다.
  2. 매우 빈번하게 발생하는 오류 상황에 대해서는 예외 대신 오류 코드를 사용하는 것을 고려할 수 있습니다.
  3. 예외 객체를 값으로 던지고 const 참조로 잡는 것이 일반적으로 가장 효율적입니다.

실습

  1. 이 프로그램에 실제 압축 알고리즘을 구현해보세요. (힌트 : zlib 라이브러리를 사용해볼 수 있습니다)
  2. 압축 해제 기능을 추가하고, 이에 따른 새로운 예외 클래스들을 설계하세요.
  3. 멀티스레딩을 도입하여 여러 파일을 동시에 압축할 수 있게 만드세요. 이 때 발생할 수 있는 동시성 관련 예외들을 고려하세요.
  4. 그래픽 사용자 인터페이스(GUI)를 추가하고, 예외 발생 시 사용자에게 적절한 메시지를 표시하세요.
  5. 단위 테스트를 작성하여 다양한 예외 상황을 테스트하세요.

결론

 이번 실습을 통해 우리는 실제 프로그램에 예외 처리를 적용하는 방법을 배웠습니다. 예외 처리는 프로그램의 안정성과 신뢰성을 크게 향상시킬 수 있지만, 신중하게 설계하고 구현해야 합니다. 항상 예외 안전성과 성능을 고려하며, 적절한 수준의 오류 처리 전략을 선택하는 것이 중요합니다. 예외 처리는 C++의 핵심적인 기능 중 하나이며, 이를 능숙하게 다루는 것은 숙련된 C++ 프로그래머가 되는 데 필수적입니다. 계속해서 다양한 상황에서 예외 처리를 연습하고, 실제 프로젝트에 적용해보면서 경험을 쌓아나가시기 바랍니다.