C++ std::filesystem 완벽 가이드 | 경로·디렉토리·파일·권한 한 번에 정리

C++ std::filesystem 완벽 가이드 | 경로·디렉토리·파일·권한 한 번에 정리

이 글의 핵심

C++ std::filesystem 완벽 가이드에 대해 정리한 개발 블로그 글입니다. 설정 파일 config/settings.json을 읽는 코드를 작성했습니다. Linux와 macOS에서는 잘 동작하는데, Windows에서만 "파일을 찾을 수 없습니다" 에러가 납니다. 개념과 예제 코드를 단계적으로 다루며, 실무·학습에 참고할 수 있도록 구성했습니다. 관련 키워드: C++, std::fi…

들어가며: “경로가 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::filesystempath를 사용하면 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)을 확인하고 설정할 수 있습니다.
  • 실전에서 자주 겪는 에러와 프로덕션 패턴을 알 수 있습니다.

목차

  1. 문제 시나리오
  2. std::filesystem 개요
  3. 경로(path) 연산
  4. 디렉토리 순회
  5. 파일 연산
  6. 권한(permissions)
  7. 자주 발생하는 에러와 해결법
  8. 모범 사례
  9. 프로덕션 패턴
  10. 체크리스트
  11. 정리

개념을 잡는 비유

시간·파일·로그·JSON은 도구 상자의 자주 쓰는 렌치입니다. 표준·검증된 라이브러리로 한 가지 규칙을 정해 두면, 팀 전체가 같은 단위·같은 포맷으로 맞출 수 있습니다.


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_directoriesmkdir -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로 조합

문자열 연결 대신 pathoperator/를 사용합니다.

// ❌
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 이상으로 컴파일
  • 경로 조합에 pathoperator/ 사용
  • 프로덕션에서는 std::error_code 오버로드 사용
  • file_size 전에 is_regular_file 확인
  • recursive_directory_iteratorskip_permission_denied 적용
  • copy_fileoverwrite_existing 필요 여부 확인
  • 중요 데이터 쓰기는 원자적 쓰기(임시 + rename) 고려
  • 임시 파일/디렉토리는 RAII로 정리

11. 정리

항목내용
path경로 표현, operator/로 조합
exists존재 여부
is_regular_file일반 파일 여부
create_directories중간 경로까지 생성
directory_iterator한 단계 순회
recursive_directory_iterator하위까지 순회
copy_file / copy파일/디렉토리 복사
remove / remove_all삭제
permissions권한 확인/설정

핵심 원칙:

  1. path로 크로스 플랫폼 경로 처리
  2. error_code로 예외 없이 에러 처리
  3. 파일 연산 전 타입 확인
  4. 원자적 쓰기로 데이터 무결성 유지

같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

  • 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++ 시리즈 전체 보기
  • C++ Adapter Pattern 완벽 가이드 | 인터페이스 변환과 호환성
  • C++ ADL |
  • C++ Aggregate Initialization |