C++ std::filesystem 완벽 가이드 | 경로·디렉토리·파일·권한 한 번에 정리
이 글의 핵심
설정 파일 config/settings.json을 읽는 코드를 작성했습니다. Linux와 macOS에서는 잘 동작하는데, Windows에서만 파일을 찾을 수 없습니다 에러가 납니다. 개념과 예제 코드를 단계적으로 다루며, 실무·학습에 참고할 수 있도록 구성했습니다.
들어가며: “경로가 Windows에서만 깨져요”
ifstream으로 열었는데 Linux에서는 되고 Windows에서는 안 된다
설정 파일 config/settings.json을 읽는 코드를 작성했습니다. Linux와 macOS에서는 잘 동작하는데, Windows에서만 “파일을 찾을 수 없습니다” 에러가 납니다.
원인: 경로 구분자가 다릅니다. Linux/macOS는 /, Windows는 \를 사용합니다. "config/settings.json"처럼 /를 하드코딩하면 Windows에서도 대부분 동작하지만, 경로를 문자열 연결로 조합할 때 path + "/" + filename처럼 하드코딩하면 config\settings.json과 혼용되며 문제가 생길 수 있습니다. 더 큰 문제는 실행 파일 기준 경로 vs 작업 디렉토리 기준 경로를 혼동하는 것입니다.
해결: C++17 std::filesystem의 path를 사용하면 OS에 맞는 구분자로 자동 변환되고, operator/로 경로를 안전하게 조합할 수 있습니다.
// ❌ 나쁜 예: 문자열 연결
std::string configPath = baseDir + "/config/settings.json";
// ✅ 좋은 예: std::filesystem::path
#include <filesystem>
namespace fs = std::filesystem;
fs::path configPath = fs::path(baseDir) / "config" / "settings.json";
std::ifstream file(configPath);
이 글을 읽으면:
std::filesystem::path로 크로스 플랫폼 경로를 다룰 수 있습니다.- 디렉토리 순회, 파일 복사·삭제·이동을 할 수 있습니다.
- 파일 권한(permissions)을 확인하고 설정할 수 있습니다.
- 실전에서 자주 겪는 에러와 프로덕션 패턴을 알 수 있습니다. 실무 적용 경험: 이 글은 대규모 C++ 프로젝트에서 실제로 겪은 문제와 해결 과정을 바탕으로 작성되었습니다. 책이나 문서에서 다루지 않는 실전 함정과 디버깅 팁을 포함합니다.
1. 문제 시나리오
시나리오 1: 로그 디렉토리가 없어서 프로그램이 크래시
문제: logs/app.log에 로그를 쓰려고 하는데, logs 폴더가 없으면 ofstream 열기가 실패합니다.
해결: std::filesystem::create_directories()로 상위 디렉토리를 먼저 생성합니다.
#include <filesystem>
#include <fstream>
namespace fs = std::filesystem;
void ensureLogDir(const fs::path& logPath) {
fs::path dir = logPath.parent_path();
if (!dir.empty() && !fs::exists(dir)) {
fs::create_directories(dir);
}
}
int main() {
fs::path logPath = "logs/app.log";
ensureLogDir(logPath);
std::ofstream log(logPath);
log << "Application started\n";
}
시나리오 2: 플러그인 폴더의 .so/.dll 파일만 스캔해야 함
문제: plugins/ 안의 동적 라이브러리만 로드해야 하는데, .txt, .md 등 다른 파일도 섞여 있습니다.
해결: directory_iterator로 순회하면서 path.extension()으로 필터링합니다.
#include <filesystem>
#include <iostream>
#include <vector>
namespace fs = std::filesystem;
std::vector<fs::path> findPlugins(const fs::path& pluginDir) {
std::vector<fs::path> plugins;
for (const auto& entry : fs::directory_iterator(pluginDir)) {
if (entry.is_regular_file()) {
std::string ext = entry.path().extension().string();
#if defined(_WIN32)
if (ext == ".dll") plugins.push_back(entry.path());
#else
if (ext == ".so") plugins.push_back(entry.path());
#endif
}
}
return plugins;
}
시나리오 3: 사용자 업로드 파일의 권한이 너무 열려 있음
문제: 업로드된 파일이 chmod 777처럼 모든 사용자가 쓰기 가능해 보안 위험이 됩니다.
해결: std::filesystem::permissions()로 파일 생성 후 권한을 제한합니다.
#include <filesystem>
namespace fs = std::filesystem;
void restrictUploadedFile(const fs::path& file) {
// 소유자 읽기/쓰기만 허용
fs::permissions(file, fs::perms::owner_read | fs::perms::owner_write);
}
시나리오 4: 임시 파일을 정리하지 않아 디스크가 가득 참
문제: 크래시나 예외로 인해 임시 파일이 삭제되지 않고 쌓입니다. 해결: RAII로 임시 파일을 감싸서 스코프를 벗어날 때 자동 삭제합니다.
#include <filesystem>
#include <fstream>
namespace fs = std::filesystem;
class TempFile {
fs::path path_;
public:
TempFile(const fs::path& base = fs::temp_directory_path()) {
path_ = base / ("tmp_" + std::to_string(std::rand()) + ".tmp");
std::ofstream(path_) << "";
}
~TempFile() {
if (fs::exists(path_)) fs::remove(path_);
}
const fs::path& path() const { return path_; }
};
2. std::filesystem 개요
아키텍처
flowchart TB
subgraph path["path: 경로 표현"]
P1[문자열/경로 조합]
P2[operator/]
P3[extension, stem, filename]
P1 --> P2 --> P3
end
subgraph ops[파일/디렉토리 연산]
O1[exists, is_regular_file]
O2[create_directories, remove]
O3[copy, rename]
O1 --> O2 --> O3
end
subgraph iter[순회]
I1[directory_iterator]
I2[recursive_directory_iterator]
I1 --> I2
end
path --> ops
path --> iter
위 다이어그램 설명: path는 경로를 표현하고 조합하며, exists·create_directories·copy 등으로 파일/디렉토리를 조작합니다. directory_iterator는 한 단계, recursive_directory_iterator는 하위까지 순회합니다.
컴파일 옵션
# C++17 필요
g++ -std=c++17 -o fs_demo fs_demo.cpp
# Windows (MSVC)
# /std:c++17
헤더와 네임스페이스
#include <filesystem> 후 namespace fs = std::filesystem;으로 축약하는 것이 관례입니다.
3. 경로(path) 연산
path 생성과 조합
#include <filesystem>
#include <iostream>
namespace fs = std::filesystem;
int main() {
// 1) 문자열로 생성
fs::path p1("/home/user/config");
fs::path p2("settings.json");
// 2) operator/ 로 경로 조합 (OS 구분자 자동 적용)
fs::path full = p1 / p2;
std::cout << full << "\n"; // Linux: /home/user/config/settings.json
// Windows: /home/user/config\settings.json
// 3) 현재 디렉토리
fs::path cwd = fs::current_path();
std::cout << "CWD: " << cwd << "\n";
// 4) 실행 파일 경로 (C++23 전에는 플랫폼별 API 필요)
// Linux: /proc/self/exe, Windows: GetModuleFileName
}
위 코드 설명: path는 문자열처럼 생성하고, operator/로 조합하면 OS에 맞는 구분자가 삽입됩니다. current_path()는 프로세스의 작업 디렉토리를 반환합니다.
path 구성 요소 추출
#include <filesystem>
#include <iostream>
namespace fs = std::filesystem;
int main() {
fs::path p = "/home/user/projects/app/src/main.cpp";
std::cout << "filename: " << p.filename() << "\n"; // main.cpp
std::cout << "stem: " << p.stem() << "\n"; // main
std::cout << "extension: " << p.extension() << "\n"; // .cpp
std::cout << "parent: " << p.parent_path() << "\n"; // .../src
std::cout << "root_name: " << p.root_name() << "\n"; // (Linux: "")
std::cout << "root_path: " << p.root_path() << "\n"; // /
// 상대 경로로 변환
fs::path base = "/home/user/projects";
fs::path rel = fs::relative(p, base);
std::cout << "relative: " << rel << "\n"; // app/src/main.cpp
}
위 코드 설명: filename()은 마지막 요소, stem()은 확장자 제외, extension()은 .cpp 같은 확장자, parent_path()는 부모 경로입니다. relative(p, base)는 base 기준 상대 경로를 만듭니다.
경로 정규화와 절대 경로
#include <filesystem>
#include <iostream>
namespace fs = std::filesystem;
int main() {
fs::path p = "config/../config/settings.json";
// 정규화: . 과 ...제거
fs::path canonical = fs::weakly_canonical(p);
std::cout << "weakly_canonical: " << canonical << "\n";
// 절대 경로 (경로가 존재해야 함)
if (fs::exists(p)) {
fs::path abs = fs::absolute(p);
std::cout << "absolute: " << abs << "\n";
fs::path can = fs::canonical(p); // 심볼릭 링크 해석, 존재 필수
std::cout << "canonical: " << can << "\n";
}
// 경로 비교
fs::path a = "/a/b/c";
fs::path b = "/a/b/c";
std::cout << "equal: " << (a == b) << "\n";
}
위 코드 설명: weakly_canonical은 ./..를 정리하고, 경로가 없어도 동작합니다. canonical은 경로가 존재해야 하며 심볼릭 링크를 해석합니다. absolute는 현재 경로 기준 절대 경로를 만듭니다.
완전한 경로 조작 예제
// 복사해 붙여넣은 뒤: g++ -std=c++17 -o path_demo path_demo.cpp && ./path_demo
#include <filesystem>
#include <iostream>
#include <string>
namespace fs = std::filesystem;
int main() {
fs::path base = fs::current_path();
fs::path configDir = base / "config";
fs::path settingsPath = configDir / "app" / "settings.json";
std::cout << "Base: " << base << "\n";
std::cout << "Config dir: " << configDir << "\n";
std::cout << "Settings: " << settingsPath << "\n";
std::cout << "Filename: " << settingsPath.filename() << "\n";
std::cout << "Stem: " << settingsPath.stem() << "\n";
std::cout << "Ext: " << settingsPath.extension() << "\n";
// 문자열로 변환
std::string strPath = settingsPath.string(); // OS 네이티브 형식
std::string u8Path = settingsPath.u8string(); // UTF-8 (C++20)
}
4. 디렉토리 순회
directory_iterator: 한 단계만
#include <filesystem>
#include <iostream>
namespace fs = std::filesystem;
int main() {
fs::path dir = ".";
for (const auto& entry : fs::directory_iterator(dir)) {
std::cout << entry.path().filename() << " - ";
if (entry.is_regular_file()) {
std::cout << "file, " << fs::file_size(entry) << " bytes\n";
} else if (entry.is_directory()) {
std::cout << "dir\n";
} else if (entry.is_symlink()) {
std::cout << "symlink -> " << fs::read_symlink(entry) << "\n";
}
}
}
위 코드 설명: directory_iterator는 지정 디렉토리의 직계 항목만 순회합니다. entry.path()로 경로, entry.is_regular_file() 등으로 타입을 확인합니다.
recursive_directory_iterator: 하위까지
#include <filesystem>
#include <iostream>
#include <string>
namespace fs = std::filesystem;
void listAll(const fs::path& dir) {
for (const auto& entry : fs::recursive_directory_iterator(dir,
fs::directory_options::skip_permission_denied)) {
// depth(): 현재 깊이 (0 = 최상위)
std::string indent(entry.depth() * 2, ' ');
std::cout << indent << entry.path().filename() << "\n";
}
}
위 코드 설명: recursive_directory_iterator는 하위 디렉토리까지 재귀적으로 순회합니다. entry.depth()로 들여쓰기 깊이를 얻고, skip_permission_denied로 권한 없는 디렉토리는 건너뜁니다.
특정 확장자만 필터링
#include <filesystem>
#include <string>
#include <vector>
namespace fs = std::filesystem;
std::vector<fs::path> findFilesByExtension(const fs::path& dir,
const std::string& ext) {
std::vector<fs::path> result;
for (const auto& entry : fs::recursive_directory_iterator(dir,
fs::directory_options::skip_permission_denied)) {
if (entry.is_regular_file() && entry.path().extension() == ext) {
result.push_back(entry.path());
}
}
return result;
}
int main() {
auto cppFiles = findFilesByExtension(".", ".cpp");
for (const auto& p : cppFiles) {
std::cout << p << "\n";
}
}
디렉토리 순회 에러 처리
#include <filesystem>
#include <iostream>
#include <system_error>
namespace fs = std::filesystem;
bool safeListDir(const fs::path& dir) {
std::error_code ec;
auto iter = fs::directory_iterator(dir, ec);
if (ec) {
std::cerr << "Cannot open " << dir << ": " << ec.message() << "\n";
return false;
}
for (const auto& entry : fs::directory_iterator(dir, ec)) {
if (ec) {
std::cerr << "Iterator error: " << ec.message() << "\n";
return false;
}
std::cout << entry.path().filename() << "\n";
}
return true;
}
위 코드 설명: directory_iterator 생성자와 증가 시 std::error_code를 넘기면 예외 대신 에러 코드로 실패를 처리할 수 있습니다.
5. 파일 연산
파일 존재 및 타입 확인
#include <filesystem>
#include <iostream>
namespace fs = std::filesystem;
void checkPath(const fs::path& p) {
if (!fs::exists(p)) {
std::cout << "Does not exist\n";
return;
}
if (fs::is_regular_file(p)) {
std::cout << "Regular file, size: " << fs::file_size(p) << "\n";
} else if (fs::is_directory(p)) {
std::cout << "Directory\n";
} else if (fs::is_symlink(p)) {
std::cout << "Symlink -> " << fs::read_symlink(p) << "\n";
} else if (fs::is_block_file(p)) {
std::cout << "Block device\n";
} else if (fs::is_character_file(p)) {
std::cout << "Character device\n";
} else if (fs::is_fifo(p)) {
std::cout << "FIFO\n";
} else if (fs::is_socket(p)) {
std::cout << "Socket\n";
}
}
위 코드 설명: exists()로 존재 여부, is_regular_file() 등으로 타입을 구분합니다. file_size()는 일반 파일에만 사용 가능합니다.
디렉토리 생성
#include <filesystem>
#include <iostream>
namespace fs = std::filesystem;
int main() {
fs::path dir = "a/b/c/d";
// create_directories: 중간 경로까지 모두 생성
bool created = fs::create_directories(dir);
std::cout << "Created: " << created << "\n";
// create_directory: 한 단계만 (부모가 있어야 함)
fs::create_directory("single_dir");
// 이미 있으면 false, 없으면 생성 후 true
if (fs::create_directories("logs/2026/03")) {
std::cout << "Log dir created\n";
}
}
위 코드 설명: create_directories는 mkdir -p처럼 중간 경로를 모두 만들고, create_directory는 한 단계만 만듭니다.
파일 복사
#include <filesystem>
#include <iostream>
namespace fs = std::filesystem;
int main() {
fs::path src = "source.txt";
fs::path dst = "backup/copy.txt";
// 옵션: overwrite_existing, recursive(디렉토리용), copy_symlinks
fs::copy_options opts = fs::copy_options::overwrite_existing;
fs::copy_file(src, dst, opts); // 파일만 복사
// 디렉토리 전체 복사
fs::copy("src_dir", "dst_dir",
fs::copy_options::recursive | fs::copy_options::overwrite_existing);
}
위 코드 설명: copy_file은 단일 파일, copy는 디렉토리도 recursive로 복사할 수 있습니다. overwrite_existing이 없으면 기존 파일이 있을 때 에러가 납니다.
파일 이동/이름 변경
#include <filesystem>
namespace fs = std::filesystem;
int main() {
fs::path oldName = "temp.txt";
fs::path newName = "final.txt";
fs::rename(oldName, newName); // 같은 파일시스템: 원자적 이동
// 다른 파일시스템: 복사 후 삭제
fs::path otherFs = "/mnt/other/disk/file.txt";
fs::rename(oldName, otherFs); // 구현에 따라 copy+remove로 동작
}
위 코드 설명: rename은 같은 파일시스템 내에서는 원자적으로 동작하며, 다른 파일시스템으로 옮길 때는 복사 후 삭제로 처리될 수 있습니다.
파일 삭제
#include <filesystem>
#include <iostream>
namespace fs = std::filesystem;
int main() {
fs::path file = "junk.txt";
fs::path dir = "empty_dir";
// 파일 삭제
if (fs::remove(file)) {
std::cout << "Removed " << file << "\n";
}
// 빈 디렉토리 삭제
fs::remove(dir);
// 디렉토리 전체 삭제 (하위 포함)
fs::path tree = "to_delete";
std::uintmax_t n = fs::remove_all(tree);
std::cout << "Removed " << n << " items\n";
}
위 코드 설명: remove는 파일 또는 빈 디렉토리만 삭제하고, remove_all은 하위를 포함해 모두 삭제하며 삭제된 항목 수를 반환합니다.
심볼릭 링크
#include <filesystem>
#include <iostream>
namespace fs = std::filesystem;
int main() {
fs::path target = "real_file.txt";
fs::path link = "symlink_to_file";
fs::create_symlink(target, link); // 상대 경로 링크
// fs::create_directory_symlink(target, "symlink_to_dir"); // 디렉토리용
if (fs::is_symlink(link)) {
fs::path resolved = fs::read_symlink(link);
std::cout << "Link points to: " << resolved << "\n";
// 실제 파일 경로 (절대)
fs::path canonical = fs::canonical(link);
std::cout << "Canonical: " << canonical << "\n";
}
}
위 코드 설명: create_symlink로 심볼릭 링크를 만들고, read_symlink로 대상 경로를 읽습니다. canonical은 링크를 따라가 최종 경로를 반환합니다.
완전한 파일 복사 유틸리티
// 복사해 붙여넣은 뒤: g++ -std=c++17 -o fs_copy fs_copy.cpp && ./fs_copy src dst
#include <filesystem>
#include <iostream>
#include <system_error>
namespace fs = std::filesystem;
bool copyRecursive(const fs::path& src, const fs::path& dst) {
std::error_code ec;
if (!fs::exists(src, ec)) {
std::cerr << "Source does not exist: " << src << "\n";
return false;
}
if (fs::is_regular_file(src)) {
fs::create_directories(dst.parent_path(), ec);
if (ec) {
std::cerr << "Cannot create parent: " << ec.message() << "\n";
return false;
}
fs::copy_file(src, dst, fs::copy_options::overwrite_existing, ec);
if (ec) {
std::cerr << "Copy failed: " << ec.message() << "\n";
return false;
}
return true;
}
if (fs::is_directory(src)) {
fs::create_directories(dst, ec);
for (const auto& entry : fs::directory_iterator(src)) {
fs::path newDst = dst / entry.path().filename();
if (!copyRecursive(entry.path(), newDst)) {
return false;
}
}
return true;
}
std::cerr << "Unsupported type: " << src << "\n";
return false;
}
int main(int argc, char* argv[]) {
if (argc != 3) {
std::cerr << "Usage: " << argv[0] << " <source> <destination>\n";
return 1;
}
return copyRecursive(argv[1], argv[2]) ? 0 : 1;
}
6. 권한(permissions)
권한 확인
#include <filesystem>
#include <iostream>
namespace fs = std::filesystem;
void showPermissions(const fs::path& p) {
if (!fs::exists(p)) return;
auto perms = fs::status(p).permissions();
auto has = [perms](fs::perms p) { return (perms & p) != fs::perms::none; };
std::cout << "Owner read: " << has(fs::perms::owner_read) << "\n";
std::cout << "Owner write: " << has(fs::perms::owner_write) << "\n";
std::cout << "Owner exec: " << has(fs::perms::owner_exec) << "\n";
std::cout << "Group read: " << has(fs::perms::group_read) << "\n";
std::cout << "Others read: " << has(fs::perms::others_read) << "\n";
}
위 코드 설명: fs::status(p).permissions()로 권한 비트를 얻고, owner_read 등과 AND 연산으로 확인합니다.
권한 설정
#include <filesystem>
#include <iostream>
namespace fs = std::filesystem;
int main() {
fs::path file = "secret.txt";
// 소유자만 읽기/쓰기 (600)
fs::permissions(file,
fs::perms::owner_read | fs::perms::owner_write,
fs::perm_options::replace);
// 실행 권한 추가 (기존 유지)
fs::permissions(file,
fs::perms::owner_exec,
fs::perm_options::add);
// 권한 제거
fs::permissions(file,
fs::perms::others_read | fs::perms::others_write,
fs::perm_options::remove);
}
위 코드 설명: replace는 지정한 권한으로 교체, add는 추가, remove는 제거합니다. owner_read | owner_write는 chmod 600에 해당합니다.
권한과 함께 파일 생성
#include <filesystem>
#include <fstream>
namespace fs = std::filesystem;
void createRestrictedFile(const fs::path& path, const std::string& content) {
std::ofstream out(path);
if (!out) return;
out << content;
out.close();
// 생성 직후 권한 제한 (소유자만 읽기/쓰기)
fs::permissions(path,
fs::perms::owner_read | fs::perms::owner_write,
fs::perm_options::replace);
}
위 코드 설명: 파일을 쓴 뒤 permissions로 권한을 제한하면, 업로드·설정 파일 등에 적용할 수 있습니다.
7. 자주 발생하는 에러와 해결법
문제 1: filesystem_error 예외로 프로그램이 종료됨
증상: fs::copy_file이나 fs::remove 호출 시 filesystem_error가 발생합니다.
원인: 기본적으로 std::filesystem 함수는 실패 시 예외를 던집니다. 파일이 없거나 권한이 없으면 예외가 발생합니다.
해결:
// ❌ 예외 발생
fs::remove("nonexistent.txt"); // throws filesystem_error
// ✅ error_code 사용
std::error_code ec;
fs::remove("nonexistent.txt", ec);
if (ec) {
std::cerr << "Remove failed: " << ec.message() << "\n";
}
문제 2: file_size가 디렉토리에 호출되어 에러
증상: fs::file_size(dir) 호출 시 예외 또는 잘못된 결과.
원인: file_size는 일반 파일에만 사용 가능합니다.
해결:
// ❌ 잘못된 사용
auto size = fs::file_size(somePath); // 디렉토리면 에러
// ✅ 타입 확인 후 호출
if (fs::is_regular_file(path)) {
auto size = fs::file_size(path);
}
문제 3: recursive_directory_iterator가 권한 거부로 중단
증상: /나 시스템 디렉토리를 순회하다가 예외가 발생합니다.
원인: 권한이 없는 디렉토리에 접근하면 기본적으로 예외가 납니다.
해결:
// ✅ skip_permission_denied 옵션
for (const auto& entry : fs::recursive_directory_iterator("/",
fs::directory_options::skip_permission_denied)) {
// 권한 없는 디렉토리는 건너뜀
}
문제 4: Windows에서 경로가 깨져 보임
증상: path << u8"한글" 출력 시 깨집니다.
원인: Windows 콘솔 기본 인코딩이 UTF-8이 아닐 수 있습니다.
해결:
// Windows: 콘솔 UTF-8 설정
#ifdef _WIN32
#include <windows.h>
SetConsoleOutputCP(65001);
#endif
// 또는 u8string()으로 UTF-8 문자열 사용
std::string u8 = path.u8string();
문제 5: copy_file이 기존 파일이 있어도 덮어쓰지 않음
증상: 대상 파일이 이미 있는데 복사가 실패합니다. 원인: 기본 동작은 기존 파일을 덮어쓰지 않습니다. 해결:
// ✅ overwrite_existing 옵션
fs::copy_file(src, dst, fs::copy_options::overwrite_existing);
문제 6: create_directories가 실패함
증상: create_directories("a/b/c")가 false를 반환합니다.
원인: a가 이미 일반 파일로 존재하면, 그 아래에 디렉토리를 만들 수 없습니다. 또는 권한 부족.
해결:
std::error_code ec;
bool ok = fs::create_directories(path, ec);
if (!ok && ec) {
std::cerr << "Create failed: " << ec.message()
<< " (path: " << path << ")\n";
}
// a가 파일인지 확인
if (fs::exists("a") && fs::is_regular_file("a")) {
std::cerr << "Cannot create: 'a' is a file\n";
}
8. 모범 사례
1. error_code로 예외 비활성화
프로덕션에서는 예외 대신 std::error_code를 사용해 파일 시스템 오류를 처리하는 것이 안정적입니다.
std::error_code ec;
if (!fs::exists(path, ec) || ec) {
logError("Path check failed", ec);
return false;
}
2. 경로는 항상 path로 조합
문자열 연결 대신 path와 operator/를 사용합니다.
// ❌
std::string p = base + "/" + sub + "/" + file;
// ✅
fs::path p = fs::path(base) / sub / file;
3. exists + is_regular_file로 이중 확인
파일 연산 전에 타입을 확인합니다.
if (fs::exists(p) && fs::is_regular_file(p)) {
auto size = fs::file_size(p);
}
4. TOCTOU 주의
exists 확인과 실제 사용 사이에 파일이 삭제되거나 바뀔 수 있습니다. 최종적으로는 ifstream::is_open() 등 실제 연산 결과를 검사해야 합니다.
if (fs::exists(p)) {
std::ifstream f(p);
if (!f) { /* 열기 실패 - exists 후 삭제됐을 수 있음 */ }
}
5. recursive 순회 시 skip_permission_denied
시스템 전체를 스캔할 때는 skip_permission_denied를 사용합니다.
9. 프로덕션 패턴
패턴 1: 안전한 디렉토리 생성
#include <filesystem>
#include <system_error>
namespace fs = std::filesystem;
bool ensureDirectory(const fs::path& dir) {
std::error_code ec;
if (fs::exists(dir, ec)) {
if (ec) return false;
return fs::is_directory(dir, ec);
}
return fs::create_directories(dir, ec) && !ec;
}
패턴 2: 원자적 파일 쓰기 (임시 + rename)
#include <filesystem>
#include <fstream>
#include <system_error>
namespace fs = std::filesystem;
bool atomicWrite(const fs::path& target, const std::string& content) {
std::error_code ec;
fs::path tmp = target.parent_path() / (target.filename().string() + ".tmp");
std::ofstream out(tmp, std::ios::binary);
if (!out) return false;
out << content;
out.close();
if (!out) {
fs::remove(tmp, ec);
return false;
}
fs::rename(tmp, target, ec);
if (ec) {
fs::remove(tmp, ec);
return false;
}
return true;
}
패턴 3: 디렉토리 크기 계산
#include <filesystem>
#include <cstdint>
namespace fs = std::filesystem;
std::uintmax_t directorySize(const fs::path& dir) {
std::uintmax_t total = 0;
std::error_code ec;
for (const auto& entry : fs::recursive_directory_iterator(dir,
fs::directory_options::skip_permission_denied, ec)) {
if (ec) return 0;
if (entry.is_regular_file(ec) && !ec) {
total += fs::file_size(entry, ec);
}
}
return total;
}
패턴 4: 임시 디렉토리 RAII
#include <filesystem>
#include <random>
#include <sstream>
namespace fs = std::filesystem;
class TempDirectory {
fs::path path_;
public:
TempDirectory() {
std::ostringstream oss;
oss << fs::temp_directory_path() / "tmp_" << std::rand();
path_ = oss.str();
fs::create_directories(path_);
}
~TempDirectory() {
std::error_code ec;
fs::remove_all(path_, ec);
}
const fs::path& path() const { return path_; }
};
패턴 5: 설정 파일 경로 해석
#include <filesystem>
#include <string>
namespace fs = std::filesystem;
fs::path resolveConfigPath(const std::string& filename) {
// 1) 환경 변수
const char* configHome = std::getenv("XDG_CONFIG_HOME");
if (configHome) {
fs::path p = fs::path(configHome) / "myapp" / filename;
if (fs::exists(p)) return fs::canonical(p);
}
// 2) 홈 디렉토리
const char* home = std::getenv("HOME");
if (home) {
fs::path p = fs::path(home) / ".config" / "myapp" / filename;
if (fs::exists(p)) return fs::canonical(p);
}
// 3) 현재 디렉토리
if (fs::exists(filename)) return fs::canonical(filename);
return {};
}
10. 체크리스트
구현 시 확인할 항목:
-
-std=c++17이상으로 컴파일 - 경로 조합에
path와operator/사용 - 프로덕션에서는
std::error_code오버로드 사용 -
file_size전에is_regular_file확인 -
recursive_directory_iterator에skip_permission_denied적용 -
copy_file시overwrite_existing필요 여부 확인 - 중요 데이터 쓰기는 원자적 쓰기(임시 + rename) 고려
- 임시 파일/디렉토리는 RAII로 정리
11. 정리
| 항목 | 내용 |
|---|---|
| path | 경로 표현, operator/로 조합 |
| exists | 존재 여부 |
| is_regular_file | 일반 파일 여부 |
| create_directories | 중간 경로까지 생성 |
| directory_iterator | 한 단계 순회 |
| recursive_directory_iterator | 하위까지 순회 |
| copy_file / copy | 파일/디렉토리 복사 |
| remove / remove_all | 삭제 |
| permissions | 권한 확인/설정 |
| 핵심 원칙: |
path로 크로스 플랫폼 경로 처리error_code로 예외 없이 에러 처리- 파일 연산 전 타입 확인
- 원자적 쓰기로 데이터 무결성 유지
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ 파일 입출력 | ifstream·ofstream으로 “파일 열기 실패” 에러 처리까지
- C++ 남들보다 먼저 써보는 C++23 핵심 기능 [#37-1]
- C++ 예외 처리 | try-catch-throw와 예외 vs 에러 코드, 언제 뭘 쓸지
실전 체크리스트
실무에서 이 개념을 적용할 때 확인해야 할 사항입니다.
코드 작성 전
- 이 기법이 현재 문제를 해결하는 최선의 방법인가?
- 팀원들이 이 코드를 이해하고 유지보수할 수 있는가?
- 성능 요구사항을 만족하는가?
코드 작성 중
- 컴파일러 경고를 모두 해결했는가?
- 엣지 케이스를 고려했는가?
- 에러 처리가 적절한가?
코드 리뷰 시
- 코드의 의도가 명확한가?
- 테스트 케이스가 충분한가?
- 문서화가 되어 있는가? 이 체크리스트를 활용하여 실수를 줄이고 코드 품질을 높이세요.
이 글에서 다루는 키워드 (관련 검색어)
C++ filesystem, std::filesystem, 경로 조작, 디렉토리 순회, 파일 복사, 파일 권한 등으로 검색하시면 이 글이 도움이 됩니다.
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. 설정 파일 경로, 로그 디렉토리 생성, 백업/복원, 플러그인 스캔, 임시 파일 정리 등 파일 시스템을 다루는 모든 C++ 프로젝트에서 std::filesystem이 표준입니다.
Q. 선행으로 읽으면 좋은 글은?
A. 파일 I/O 기초(#11-1)와 예외 처리(#8-1)를 먼저 읽으면 이해가 쉽습니다.
Q. 더 깊이 공부하려면?
A. cppreference std::filesystem 문서와 각 OS의 stat/chmod API를 참고하세요.
한 줄 요약 std::filesystem::path로 경로를 조합하고, create_directories·copy_file·directory_iterator로 파일 시스템을 안전하게 다룹니다. 다음으로 파일 I/O 기초(#11-1)를 읽어보면 좋습니다.
이전 글: C++23 핵심 기능(#37-1)
다음 글: C++ 실전 가이드 #38-1: 클린 코드 기초
관련 글
심화 부록: 구현·운영 관점
이 부록은 앞선 본문에서 다룬 주제(「C++ std::filesystem 완벽 가이드 | 경로·디렉토리·파일·권한 한 번에 정리」)를 구현·런타임·운영 관점에서 다시 압축합니다. 도메인별 세부 구현은 글마다 다르지만, 입력 검증 → 핵심 연산 → 부작용(I/O·네트워크·동시성) → 관측의 흐름으로 장애를 나누면 원인 추적이 빨라집니다.
내부 동작과 핵심 메커니즘
flowchart TD A[입력·요청·이벤트] --> B[파싱·검증·디코딩] B --> C[핵심 연산·상태 전이] C --> D[부작용: I/O·네트워크·동시성] D --> E[결과·관측·저장]
sequenceDiagram participant C as 클라이언트/호출자 participant B as 경계(런타임·게이트웨이·프로세스) participant D as 의존성(API·DB·큐·파일) C->>B: 요청/이벤트 B->>D: 조회·쓰기·RPC D-->>B: 지연·부분 실패·재시도 가능 B-->>C: 응답 또는 오류(코드·상관 ID)
- 불변 조건(Invariant): 버퍼 경계, 프로토콜 상태, 트랜잭션 격리, FD 상한 등 단계별로 문장으로 적어 두면 디버깅 비용이 줄어듭니다.
- 결정성: 순수 층과 시간·네트워크·스케줄에 의존하는 층을 분리해야 테스트와 장애 분석이 쉬워집니다.
- 경계 비용: 직렬화, 인코딩, syscall 횟수, 락 경합, 할당·GC, 캐시 미스를 의심 목록에 둡니다.
- 백프레셔: 생산자가 소비자보다 빠를 때 버퍼·큐·스트림에서 속도를 줄이는 신호를 어디에 둘지 정의합니다.
프로덕션 운영 패턴
| 영역 | 운영 관점 질문 |
|---|---|
| 관측성 | 요청 단위 상관 ID, 에러율·지연 p95/p99, 의존성 타임아웃·재시도가 대시보드에 보이는가 |
| 안전성 | 입력 검증·권한·비밀·감사 로그가 코드 경로마다 일관적인가 |
| 신뢰성 | 재시도는 멱등 연산에만 적용되는가, 서킷 브레이커·백오프·DLQ가 있는가 |
| 성능 | 캐시·배치 크기·커넥션 풀·인덱스·백프레셔가 데이터 규모에 맞는가 |
| 배포 | 롤백 룬북, 카나리/블루그린, 마이그레이션·피처 플래그가 문서화되어 있는가 |
| 용량 | 피크 트래픽·디스크·FD·스레드 풀 상한을 주기적으로 검증하는가 |
스테이징은 데이터 양·네트워크 RTT·동시성을 프로덕션에 가깝게 맞출수록 재현율이 올라갑니다.
확장 예시: 엔드투엔드 미니 시나리오
앞선 본문 주제(「C++ std::filesystem 완벽 가이드 | 경로·디렉토리·파일·권한 한 번에 정리」)를 배포·운영 흐름에 맞춰 옮긴 체크리스트입니다. 도메인에 맞게 단계 이름만 바꿔 적용할 수 있습니다.
- 입력 계약 고정: 스키마·버전·최대 페이로드·타임아웃·에러 코드를 경계에 둔다.
- 핵심 경로 계측: 요청 ID, 단계별 지연, 외부 호출 결과 코드를 로그·메트릭·트레이스에서 한 흐름으로 본다.
- 실패 주입: 의존성 타임아웃·5xx·부분 데이터·락 대기를 스테이징에서 재현한다.
- 호환·롤백: 설정/마이그레이션/클라이언트 버전을 되돌릴 수 있는지 확인한다.
- 부하 후 검증: 피크 대비 p95/p99, 에러율, 리소스 상한, 알림 임계값을 점검한다.
handle(request):
ctx = newCorrelationId()
validated = validateSchema(request)
authorize(validated, ctx)
result = domainCore(validated)
persistOrEmit(result, idempotentKey)
recordMetrics(ctx, latency, outcome)
return result
문제 해결(Troubleshooting)
| 증상 | 가능 원인 | 조치 |
|---|---|---|
| 간헐적 실패 | 레이스, 타임아웃, 외부 의존성, DNS | 최소 재현 스크립트, 분산 트레이스·로그 상관관계, 재시도·서킷 설정 점검 |
| 성능 저하 | N+1, 동기 I/O, 락 경합, 과도한 직렬화, 캐시 미스 | 프로파일러·APM으로 핫스팟 확인 후 한 가지씩 제거 |
| 메모리 증가 | 캐시 무제한, 구독/리스너 누수, 대용량 버퍼, 커넥션 미반납 | 상한·TTL·힙/FD 스냅샷 비교 |
| 빌드·배포만 실패 | 환경 변수, 권한, 플랫폼 차이, lockfile | CI 로그와 로컬 diff, 런타임·이미지 버전 핀 |
| 설정 불일치 | 프로필·시크릿·기본값, 리전 | 스키마 검증된 설정 단일 소스와 배포 매트릭스 표준화 |
| 데이터 불일치 | 비멱등 재시도, 부분 쓰기, 캐시 무효화 누락 | 멱등 키·아웃박스·트랜잭션 경계 재검토 |
권장 순서: (1) 최소 재현 (2) 최근 변경 범위 축소 (3) 환경·의존성 차이 (4) 관측으로 가설 검증 (5) 수정 후 회귀·부하 테스트.
배포 전에는 git add → git commit → git push 후 npm run deploy 순서를 권장합니다.