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;
}
정리
핵심 요약
- 복사:
copy_file(),copy()(재귀) - 이동:
rename()(원자적, 같은 파일시스템) - 삭제:
remove(),remove_all()(재귀) - 디렉토리:
create_directories()(재귀 생성) - 옵션:
copy_options(덮어쓰기, 건너뛰기, 업데이트) - 예외:
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 |