C++ File Operations | "파일 연산" 가이드

C++ File Operations | "파일 연산" 가이드

이 글의 핵심

C++ File Operations에 대한 실전 가이드입니다.

들어가며

C++17 <filesystem> 라이브러리는 파일과 디렉토리 조작을 위한 표준 API를 제공합니다. 이전의 플랫폼별 API(POSIX, Windows API)를 대체하여 이식성 높은 코드를 작성할 수 있습니다.


1. 파일 연산 기본

기본 설정

#include <filesystem>
#include <iostream>

namespace fs = std::filesystem;

int main() {
    // 파일 존재 확인
    if (fs::exists("test.txt")) {
        std::cout << "파일 존재" << std::endl;
    }
    
    // 파일 크기
    auto size = fs::file_size("test.txt");
    std::cout << "크기: " << size << " bytes" << std::endl;
    
    return 0;
}

파일 복사

// 기본 복사
fs::copy_file("src.txt", "dst.txt");

// 덮어쓰기
fs::copy_file("src.txt", "dst.txt", 
              fs::copy_options::overwrite_existing);

// 디렉토리 복사 (재귀)
fs::copy("src_dir", "dst_dir", 
         fs::copy_options::recursive);

파일 이동 및 삭제

// 이름 변경 (이동)
fs::rename("old.txt", "new.txt");

// 파일 삭제
fs::remove("file.txt");

// 디렉토리 삭제 (재귀)
fs::remove_all("dir");

2. 디렉토리 연산

디렉토리 생성

// 단일 디렉토리
fs::create_directory("mydir");

// 중첩 디렉토리 (재귀)
fs::create_directories("path/to/dir");

// 이미 존재하면 false 반환
bool created = fs::create_directory("existing_dir");
if (!created) {
    std::cout << "이미 존재하거나 생성 실패" << std::endl;
}

디렉토리 순회

#include <filesystem>
#include <iostream>

namespace fs = std::filesystem;

int main() {
    // 현재 디렉토리 순회
    for (const auto& entry : fs::directory_iterator(".")) {
        std::cout << entry.path() << std::endl;
    }
    
    // 재귀 순회
    for (const auto& entry : fs::recursive_directory_iterator(".")) {
        if (entry.is_regular_file()) {
            std::cout << "파일: " << entry.path() << std::endl;
        } else if (entry.is_directory()) {
            std::cout << "디렉토리: " << entry.path() << std::endl;
        }
    }
    
    return 0;
}

3. 복사 옵션

copy_options 플래그

#include <filesystem>

namespace fs = std::filesystem;

// 기본: 이미 존재하면 에러
fs::copy_file("src.txt", "dst.txt");

// 덮어쓰기
fs::copy_file("src.txt", "dst.txt",
              fs::copy_options::overwrite_existing);

// 건너뛰기 (이미 존재하면 무시)
fs::copy_file("src.txt", "dst.txt",
              fs::copy_options::skip_existing);

// 업데이트 (더 최신이면 복사)
fs::copy_file("src.txt", "dst.txt",
              fs::copy_options::update_existing);

// 재귀 복사 (디렉토리)
fs::copy("src_dir", "dst_dir",
         fs::copy_options::recursive);

// 심볼릭 링크 복사 (링크 자체)
fs::copy("link", "new_link",
         fs::copy_options::copy_symlinks);

copy_options 조합:

// 재귀 + 덮어쓰기
fs::copy("src_dir", "dst_dir",
         fs::copy_options::recursive | 
         fs::copy_options::overwrite_existing);

4. 실전 예제

예제 1: 안전한 파일 복사

#include <filesystem>
#include <iostream>

namespace fs = std::filesystem;

bool copyFileSafe(const fs::path& src, const fs::path& dst) {
    try {
        // 원본 파일 존재 확인
        if (!fs::exists(src)) {
            std::cerr << "원본 파일이 존재하지 않습니다: " << src << std::endl;
            return false;
        }
        
        // 대상 디렉토리 생성
        fs::create_directories(dst.parent_path());
        
        // 파일 복사
        fs::copy_file(src, dst, 
                      fs::copy_options::overwrite_existing);
        
        std::cout << "복사 완료: " << src << " -> " << dst << std::endl;
        return true;
        
    } catch (const fs::filesystem_error& e) {
        std::cerr << "에러: " << e.what() << std::endl;
        return false;
    }
}

int main() {
    copyFileSafe("data/input.txt", "backup/input.txt");
    return 0;
}

예제 2: 디렉토리 백업

#include <filesystem>
#include <iostream>
#include <chrono>
#include <iomanip>
#include <sstream>

namespace fs = std::filesystem;

std::string getCurrentTimestamp() {
    auto now = std::chrono::system_clock::now();
    auto time = std::chrono::system_clock::to_time_t(now);
    
    std::stringstream ss;
    ss << std::put_time(std::localtime(&time), "%Y%m%d_%H%M%S");
    return ss.str();
}

void backupDirectory(const fs::path& src, const fs::path& backupRoot) {
    try {
        // 타임스탬프로 백업 디렉토리 생성
        fs::path backupPath = backupRoot / (src.filename().string() + "_" + getCurrentTimestamp());
        
        // 재귀 복사
        fs::copy(src, backupPath, 
                 fs::copy_options::recursive | 
                 fs::copy_options::overwrite_existing);
        
        std::cout << "백업 완료: " << backupPath << std::endl;
        
    } catch (const fs::filesystem_error& e) {
        std::cerr << "백업 실패: " << e.what() << std::endl;
    }
}

int main() {
    backupDirectory("project", "backups");
    // 결과: backups/project_20260329_143025/
    return 0;
}

예제 3: 오래된 파일 정리

#include <filesystem>
#include <iostream>
#include <chrono>

namespace fs = std::filesystem;

void cleanupOldFiles(const fs::path& dir, int days) {
    try {
        auto now = fs::file_time_type::clock::now();
        auto threshold = now - std::chrono::hours(24 * days);
        
        int deletedCount = 0;
        
        for (const auto& entry : fs::directory_iterator(dir)) {
            if (entry.is_regular_file()) {
                auto mtime = fs::last_write_time(entry);
                
                if (mtime < threshold) {
                    auto size = fs::file_size(entry);
                    fs::remove(entry.path());
                    
                    std::cout << "삭제: " << entry.path() 
                              << " (" << size << " bytes)" << std::endl;
                    deletedCount++;
                }
            }
        }
        
        std::cout << "총 " << deletedCount << "개 파일 삭제" << std::endl;
        
    } catch (const fs::filesystem_error& e) {
        std::cerr << "에러: " << e.what() << std::endl;
    }
}

int main() {
    cleanupOldFiles("temp", 7);  // 7일 이상 된 파일 삭제
    return 0;
}

예제 4: 파일 동기화

#include <filesystem>
#include <iostream>

namespace fs = std::filesystem;

void syncDirectories(const fs::path& src, const fs::path& dst) {
    try {
        for (const auto& entry : fs::recursive_directory_iterator(src)) {
            auto relativePath = fs::relative(entry.path(), src);
            auto dstPath = dst / relativePath;
            
            if (entry.is_directory()) {
                // 디렉토리 생성
                fs::create_directories(dstPath);
            } else if (entry.is_regular_file()) {
                // 파일이 없거나 더 최신이면 복사
                if (!fs::exists(dstPath) || 
                    fs::last_write_time(entry) > fs::last_write_time(dstPath)) {
                    
                    fs::copy_file(entry.path(), dstPath,
                                  fs::copy_options::overwrite_existing);
                    std::cout << "동기화: " << relativePath << std::endl;
                }
            }
        }
        
    } catch (const fs::filesystem_error& e) {
        std::cerr << "에러: " << e.what() << std::endl;
    }
}

int main() {
    syncDirectories("source", "destination");
    return 0;
}

5. 자주 발생하는 문제

문제 1: 덮어쓰기 에러

#include <filesystem>
#include <iostream>

namespace fs = std::filesystem;

int main() {
    // ❌ 이미 존재하면 에러
    try {
        fs::copy_file("src.txt", "existing.txt");
    } catch (const fs::filesystem_error& e) {
        std::cerr << "에러: " << e.what() << std::endl;
        // filesystem error: cannot copy file: File exists
    }
    
    // ✅ 옵션 지정
    fs::copy_file("src.txt", "existing.txt",
                  fs::copy_options::overwrite_existing);
    
    return 0;
}

해결책: copy_options를 명시적으로 지정하세요.

문제 2: 디렉토리 삭제 실패

// ❌ 비어있지 않은 디렉토리
try {
    fs::remove("non_empty_dir");  // 실패!
} catch (const fs::filesystem_error& e) {
    std::cerr << e.what() << std::endl;
    // Directory not empty
}

// ✅ 재귀 삭제
int removed = fs::remove_all("non_empty_dir");
std::cout << removed << "개 항목 삭제" << std::endl;

해결책: 비어있지 않은 디렉토리는 remove_all()을 사용하세요.

문제 3: 이동 vs 복사

// 이동 (빠름, 원자적)
try {
    fs::rename("old.txt", "new.txt");
    // 같은 파일시스템 내에서만 가능
} catch (const fs::filesystem_error& e) {
    // 다른 파일시스템이면 실패
    std::cerr << "이동 실패: " << e.what() << std::endl;
}

// 복사 + 삭제 (느림, 비원자적)
fs::copy_file("src.txt", "dst.txt");
fs::remove("src.txt");
// 중간에 실패하면 두 파일이 모두 존재할 수 있음

해결책: 같은 파일시스템 내에서는 rename(), 다른 파일시스템으로는 copy() + remove()를 사용하세요.

문제 4: 예외 처리

#include <filesystem>
#include <iostream>
#include <system_error>

namespace fs = std::filesystem;

// ❌ 예외 무시 (위험)
void deleteFileUnsafe(const fs::path& path) {
    fs::remove(path);  // 실패 시 예외 발생
}

// ✅ try-catch
void deleteFileSafe1(const fs::path& path) {
    try {
        fs::remove(path);
    } catch (const fs::filesystem_error& e) {
        std::cerr << "삭제 실패: " << e.what() << std::endl;
    }
}

// ✅ error_code (예외 없음)
void deleteFileSafe2(const fs::path& path) {
    std::error_code ec;
    bool removed = fs::remove(path, ec);
    
    if (ec) {
        std::cerr << "삭제 실패: " << ec.message() << std::endl;
    } else if (removed) {
        std::cout << "삭제 완료" << std::endl;
    } else {
        std::cout << "파일이 존재하지 않음" << std::endl;
    }
}

해결책: 예외 처리를 항상 고려하거나 error_code 버전을 사용하세요.


6. 원자적 연산

rename의 원자성

#include <filesystem>
#include <iostream>

namespace fs = std::filesystem;

int main() {
    // rename은 원자적 (같은 파일시스템)
    // 성공하거나 실패하거나, 중간 상태 없음
    try {
        fs::rename("old.txt", "new.txt");
        std::cout << "이동 완료 (원자적)" << std::endl;
    } catch (const fs::filesystem_error& e) {
        std::cerr << "이동 실패: " << e.what() << std::endl;
    }
    
    return 0;
}

비원자적 복사

// copy + remove는 비원자적
// 중간에 실패하면 불일치 상태 발생 가능
bool moveFile(const fs::path& src, const fs::path& dst) {
    try {
        // 1. 복사
        fs::copy_file(src, dst);
        
        // 2. 삭제 (여기서 실패하면 두 파일 모두 존재)
        fs::remove(src);
        
        return true;
    } catch (const fs::filesystem_error& e) {
        std::cerr << "이동 실패: " << e.what() << std::endl;
        return false;
    }
}

원자적 이동 패턴:

bool atomicMove(const fs::path& src, const fs::path& dst) {
    try {
        // 같은 파일시스템이면 rename (원자적)
        fs::rename(src, dst);
        return true;
    } catch (const fs::filesystem_error&) {
        // 다른 파일시스템이면 copy + remove
        try {
            fs::copy_file(src, dst);
            fs::remove(src);
            return true;
        } catch (const fs::filesystem_error& e) {
            std::cerr << "이동 실패: " << e.what() << std::endl;
            return false;
        }
    }
}

7. 파일 연산 비교

연산함수원자성파일시스템 간속도
복사copy_file()느림
이동rename()빠름
삭제remove()-빠름
재귀 삭제remove_all()-느림

8. 실전 예제: 파일 관리자

#include <filesystem>
#include <iostream>
#include <vector>

namespace fs = std::filesystem;

class FileManager {
public:
    // 파일 복사 (안전)
    bool copy(const fs::path& src, const fs::path& dst) {
        std::error_code ec;
        
        if (!fs::exists(src, ec)) {
            std::cerr << "원본 파일 없음: " << src << std::endl;
            return false;
        }
        
        fs::create_directories(dst.parent_path(), ec);
        fs::copy_file(src, dst, fs::copy_options::overwrite_existing, ec);
        
        if (ec) {
            std::cerr << "복사 실패: " << ec.message() << std::endl;
            return false;
        }
        
        return true;
    }
    
    // 파일 이동 (원자적)
    bool move(const fs::path& src, const fs::path& dst) {
        std::error_code ec;
        
        // 먼저 rename 시도 (빠름)
        fs::rename(src, dst, ec);
        
        if (!ec) {
            return true;
        }
        
        // 실패하면 copy + remove
        if (copy(src, dst)) {
            fs::remove(src, ec);
            return !ec;
        }
        
        return false;
    }
    
    // 오래된 파일 정리
    int cleanup(const fs::path& dir, int days) {
        std::error_code ec;
        auto now = fs::file_time_type::clock::now();
        auto threshold = now - std::chrono::hours(24 * days);
        
        int count = 0;
        
        for (const auto& entry : fs::directory_iterator(dir, ec)) {
            if (entry.is_regular_file(ec)) {
                auto mtime = fs::last_write_time(entry, ec);
                
                if (!ec && mtime < threshold) {
                    fs::remove(entry.path(), ec);
                    if (!ec) {
                        count++;
                    }
                }
            }
        }
        
        return count;
    }
    
    // 디렉토리 크기 계산
    uintmax_t directorySize(const fs::path& dir) {
        std::error_code ec;
        uintmax_t size = 0;
        
        for (const auto& entry : fs::recursive_directory_iterator(dir, ec)) {
            if (entry.is_regular_file(ec)) {
                size += fs::file_size(entry, ec);
            }
        }
        
        return size;
    }
};

int main() {
    FileManager fm;
    
    // 파일 복사
    fm.copy("data.txt", "backup/data.txt");
    
    // 파일 이동
    fm.move("temp.txt", "archive/temp.txt");
    
    // 7일 이상 된 파일 정리
    int deleted = fm.cleanup("temp", 7);
    std::cout << deleted << "개 파일 삭제" << std::endl;
    
    // 디렉토리 크기
    auto size = fm.directorySize("project");
    std::cout << "프로젝트 크기: " << size << " bytes" << std::endl;
    
    return 0;
}

정리

핵심 요약

  1. 복사: copy_file(), copy() (재귀)
  2. 이동: rename() (원자적, 같은 파일시스템)
  3. 삭제: remove(), remove_all() (재귀)
  4. 디렉토리: create_directories() (재귀 생성)
  5. 옵션: copy_options (덮어쓰기, 건너뛰기, 업데이트)
  6. 예외: try-catch 또는 error_code 사용

파일 연산 선택 가이드

상황권장 방법이유
같은 파일시스템 이동rename()빠르고 원자적
다른 파일시스템 이동copy() + remove()rename() 불가
백업copy()원본 유지
동기화last_write_time() 비교변경된 파일만
대용량 파일rename() 우선복사 비용 절감

실전 팁

안전성:

  • 항상 예외 처리 (try-catch 또는 error_code)
  • 원본 파일 존재 확인 후 작업
  • 대상 디렉토리 미리 생성 (create_directories)

성능:

  • 이동은 rename() 우선 시도 (빠름)
  • 대용량 파일은 복사 대신 이동
  • 재귀 작업은 recursive_directory_iterator 사용

유지보수:

  • error_code 버전으로 예외 없는 코드 작성
  • 로깅으로 작업 내역 기록
  • 트랜잭션 패턴으로 롤백 가능하게 설계

다음 단계

  • C++ Filesystem
  • C++ Directory Iterator
  • C++ Path

관련 글

  • C++ Directory Iterator |
  • C++ File Status |
  • C++ Filesystem 빠른 참조 |
  • C++ Filesystem 개념 정리 |
  • C++ path |