C++ filesystem::status로 파일 종류·권한·수정 시간 확인하기

이 글의 핵심

exists()로 확인한 직후 파일을 열었는데 실패하거나, 심볼릭 링크를 따라가느냐에 따라 결과가 달라지는 문제는 파일 상태 API의 동작을 정확히 알아야 피할 수 있습니다. status와 symlink_status의 차이, POSIX 권한 비트가 Windows에서 어떻게 표현되는지, 권한 에러를 error_code로 처리하는 방법을 짚고 백업과 로그 정리 예제로 실제 사용법을 보여줍니다.

파일 상태란?

C++17 filesystem에서 파일의 존재 여부뿐 아니라 일반 파일·디렉터리·심볼릭 링크 여부와 권한을 구분하려면 file_status를 이해하는 것이 좋습니다. file_status는 운영체제의 stat(POSIX) 또는 GetFileAttributesEx(Windows) 호출 결과 중 파일 종류와 권한만 추려 담은 작은 값 객체입니다. 중요한 점은 이것이 조회한 순간의 스냅샷이라는 것입니다. file_status를 받아 둔 뒤 다른 프로세스가 파일을 지우거나 권한을 바꿔도 이 값은 갱신되지 않으므로, 오래 보관해 두고 판단 근거로 쓰면 안 됩니다.

fs::is_regular_file(p)처럼 경로를 받는 편의 함수는 호출할 때마다 시스템 호출을 새로 합니다. 같은 경로에 대해 종류를 여러 번 확인한다면 아래처럼 fs::status(p)를 한 번 호출해 받은 file_status를 is_regular_file(status), is_directory(status)에 넘기는 편이 시스템 호출 수를 줄이고, 검사 도중 파일 상태가 바뀌어 앞뒤 결과가 모순되는 일도 막아 줍니다.

#include <filesystem>

namespace fs = std::filesystem;

fs::path p = "file.txt";
auto status = fs::status(p);

if (status.type() == fs::file_type::regular) {
    std::cout << "일반 파일" << std::endl;
}

파일 타입

fs::file_type type = fs::status(p).type();

// 타입 확인
fs::is_regular_file(p);
fs::is_directory(p);
fs::is_symlink(p);
fs::is_block_file(p);
fs::is_character_file(p);
fs::is_fifo(p);
fs::is_socket(p);

실전 예시

예시 1: 파일 정보

void printFileInfo(const fs::path& p) {
    if (!fs::exists(p)) {
        std::cout << "파일 없음" << std::endl;
        return;
    }
    
    auto status = fs::status(p);
    
    std::cout << "경로: " << p << std::endl;
    std::cout << "타입: ";
    
    if (fs::is_regular_file(status)) {
        std::cout << "일반 파일" << std::endl;
        std::cout << "크기: " << fs::file_size(p) << " bytes" << std::endl;
    } else if (fs::is_directory(status)) {
        std::cout << "디렉토리" << std::endl;
    } else if (fs::is_symlink(status)) {  // ⚠️ status()는 링크를 따라가므로 여기엔 도달하지 않음
        std::cout << "심볼릭 링크" << std::endl;
    }
}

이 예제의 마지막 분기는 실제로는 실행되지 않습니다. fs::status는 심볼릭 링크를 따라간 대상의 상태를 돌려주므로, 링크가 일반 파일을 가리키면 “일반 파일”로, 디렉터리를 가리키면 “디렉터리”로 보이고, 링크 자체가 symlink 타입으로 보고되는 일은 없습니다. 링크 여부를 표시하려면 fs::symlink_status(p)를 따로 조회해야 합니다. 또 첫 줄의 fs::exists(p)도 링크를 따라가므로, 대상이 사라진 깨진 링크는 링크 파일이 멀쩡히 있는데도 “파일 없음”으로 판정됩니다. exists로 확인한 뒤 file_size를 호출하는 사이에 파일이 지워지면 filesystem_error 예외가 날 수 있다는 점(TOCTOU)도 기억해 둘 만합니다.

예시 2: 수정 시간

void printModifiedTime(const fs::path& p) {
    auto ftime = fs::last_write_time(p);
    
    // C++17 근사 변환: 두 시계의 "지금" 차이를 이용 (수 마이크로초 오차 가능)
    auto sctp = std::chrono::time_point_cast<std::chrono::system_clock::duration>(
        ftime - fs::file_time_type::clock::now() + 
        std::chrono::system_clock::now()
    );
    
    std::time_t cftime = std::chrono::system_clock::to_time_t(sctp);
    std::cout << "수정: " << std::ctime(&cftime);
}

이 복잡한 변환이 필요한 이유는 C++17의 file_time_type이 구현이 정한 시계(file_time_type::clock)를 쓰고, 그 시계의 기준점(epoch)이 system_clock과 같다는 보장이 없기 때문입니다. 실제로 libstdc++는 기준점을 2174년 근처로 잡고, MSVC는 Windows의 파일 시간 기준인 1601년을 씁니다. 그래서 time_since_epoch() 값을 그대로 time_t로 바꾸면 수백 년 어긋난 날짜가 찍힙니다. 위 코드는 “두 시계의 현재 시각 차이”만큼 옮겨서 근사하는 C++17용 관용구이고, 두 now() 호출 사이의 시간만큼 오차가 생깁니다. C++20부터는 std::chrono::clock_cast<std::chrono::system_clock>(ftime)이나 std::chrono::file_clock::to_sys(ftime)으로 정확하게 변환할 수 있으므로, 컴파일러가 지원한다면 그쪽을 쓰는 것이 좋습니다.

예시 3: 권한 확인

void checkPermissions(const fs::path& p) {
    auto perms = fs::status(p).permissions();
    
    std::cout << "권한: ";
    
    if ((perms & fs::perms::owner_read) != fs::perms::none) {
        std::cout << "r";
    }
    if ((perms & fs::perms::owner_write) != fs::perms::none) {
        std::cout << "w";
    }
    if ((perms & fs::perms::owner_exec) != fs::perms::none) {
        std::cout << "x";
    }
    
    std::cout << std::endl;
}

예시 4: 공간 정보

void printSpaceInfo(const fs::path& p) {
    auto space = fs::space(p);
    
    std::cout << "용량: " << space.capacity << " bytes" << std::endl;
    std::cout << "여유: " << space.free << " bytes" << std::endl;
    std::cout << "사용 가능: " << space.available << " bytes" << std::endl;
}

권한 설정

fs::path p = "file.txt";

// 권한 추가
fs::permissions(p, fs::perms::owner_write, 
                fs::perm_options::add);

// 권한 제거
fs::permissions(p, fs::perms::owner_write,
                fs::perm_options::remove);

// 권한 설정 (기본값 replace: 기존 권한을 모두 이 값으로 교체)
fs::permissions(p, fs::perms::owner_all);

fs::permissions의 세 번째 인자를 생략하면 perm_options::replace가 적용되어, 지정한 비트 외의 권한은 모두 꺼집니다. 마지막 줄은 “소유자에게 rwx를 추가”가 아니라 “소유자 rwx만 남기고 그룹·기타 사용자의 권한을 모두 제거”한다는 뜻이라, 웹 서버처럼 다른 사용자로 실행되는 프로세스가 갑자기 파일을 읽지 못하게 되는 원인이 되곤 합니다. 기존 권한에 더하거나 빼려면 예제의 앞 두 줄처럼 add나 remove를 명시해야 합니다. 심볼릭 링크에 이 함수를 호출하면 기본적으로 링크가 가리키는 대상의 권한이 바뀌며, 링크 자체를 대상으로 하려면 perm_options::nofollow를 함께 줍니다.

file_status와 perms

std::filesystem::file_status는 한 번의 조회 결과를 담는 값입니다. 주로 다음 두 가지를 묶습니다.

  • type() → file_type: regular, directory, symlink, not_found 등. 심볼릭 링크를 따라가지 않은 상태는 symlink_status로, 따라간 결과는 status로 얻는 패턴이 자주 사용됩니다.
  • permissions() → perms: 소유자·그룹·기타에 대한 읽기/쓰기/실행 비트. none과 비트 AND로 “설정 여부”를 확인합니다.
fs::file_status st = fs::status(p, ec);
if (!ec && st.type() != fs::file_type::not_found) {
    auto pm = st.permissions();
    bool owner_read = (pm & fs::perms::owner_read) != fs::perms::none;
}

status(p)는 심볼릭 링크의 최종 타겟을 조회하며, 링크 자체의 타입이 필요하면 symlink_status(p)를 씁니다. 깨진 링크는 status가 not_found가 될 수 있어, 먼저 symlink_status로 링크인지 구분하는 편이 디버깅에 유리합니다.

파일 타입 확인 (정리)

file_type은 unknown, none, not_found 같은 “정보 부족” 상태도 구분합니다. 실무에서는 보통 편의 함수를 함께 씁니다.

목적권장 API
일반 파일 여부fs::is_regular_file(st) 또는 is_regular_file(p)
디렉터리 여부fs::is_directory(st)
심볼릭 링크 여부fs::is_symlink(st) — symlink_status 기반
블록/문자 장치 등is_block_file, is_character_file, is_fifo, is_socket
auto st = fs::status(path);
if (fs::is_regular_file(st)) { /* … */ }
else if (fs::is_directory(st)) { /* … */ }

auto lst = fs::symlink_status(path);
if (fs::is_symlink(lst)) {
    auto target_st = fs::status(path); // 링크를 따라간 대상
}

주의: 경로가 존재하지 않으면 type()이 not_found이고, exists(path)는 false입니다. is_regular_file은 존재하지 않으면 false를 돌려주므로, “없는 파일”과 “있지만 디렉터리”를 구분하려면 status와 file_type을 함께 보는 것이 안전합니다.

권한 검사

perms는 POSIX 스타일 비트 마스크를 가능한 한 추상화한 것입니다. 실행 권한이 없는 일반 파일, 디렉터리의 실행 비트(탐색 허용) 등 OS 의미는 문서와 실제 환경에서 확인하는 것이 좋습니다.

bool can_owner_write(const fs::path& p, std::error_code& ec) {
    auto st = fs::status(p, ec);
    if (ec) return false;
    auto pm = st.permissions();
    return (pm & fs::perms::owner_write) != fs::perms::none;
}

// 소유자 rwx를 한 줄로 출력하는 예 (POSIX 스타일)
void print_owner_rwx(fs::perms pm) {
    char r = (pm & fs::perms::owner_read) != fs::perms::none ? 'r' : '-';
    char w = (pm & fs::perms::owner_write) != fs::perms::none ? 'w' : '-';
    char x = (pm & fs::perms::owner_exec) != fs::perms::none ? 'x' : '-';
    std::cout << r << w << x;
}

쓰기 전 검사 예:

std::error_code ec;
if (can_owner_write(path, ec)) {
    // 덮어쓰기 등
}

Windows: 읽기 전용 속성·ACL은 filesystem의 perms와 1:1로 대응하지 않습니다. MSVC 구현은 파일의 읽기 전용 속성 하나만 쓰기 비트에 반영하고, 나머지는 모든 사용자에게 읽기·실행이 허용된 것처럼 보고합니다(읽기 전용이 아니면 0777, 읽기 전용이면 0555에 해당하는 값). 그래서 ACL로 쓰기가 막힌 파일도 can_owner_write는 true를 돌려주고, 실제로 열 때 “Access is denied”로 실패합니다. 반대로 permissions로 owner_write를 지우면 읽기 전용 속성이 켜지는 정도의 효과만 있습니다. 크로스 플랫폼 도구라면 “실패 시 permissions 재시도”보다 실제 open/ofstream 실패를 처리하는 방법이 신뢰도가 높은 경우가 많습니다.

POSIX에서도 권한 비트만으로 “쓸 수 있는가”를 판단하는 것은 불완전합니다. owner_write는 파일 소유자의 권한일 뿐이라, 현재 프로세스가 소유자가 아니면 그룹이나 기타 비트가 적용되고, root는 비트와 상관없이 쓸 수 있으며, 읽기 전용으로 마운트된 파일 시스템에서는 비트가 켜져 있어도 쓰기가 실패합니다. 파일을 여는 것이 목적이라면 미리 검사하기보다 바로 열어 보고 실패 이유(EACCES, EROFS 등)를 처리하는 편이 정확합니다.

실전: 백업 스크립트 (개념)

“최근 N일 수정된 파일만 복사” 같은 백업은 last_write_time과 file_status로 대상을 고릅니다. 대용량 트리는 recursive_directory_iterator와 조합합니다.

namespace fs = std::filesystem;
using clock = fs::file_time_type::clock;

bool needs_backup(const fs::path& p,
                  fs::file_time_type cutoff,
                  std::error_code& ec) {
    auto st = fs::status(p, ec);
    if (ec || !fs::is_regular_file(st)) return false;
    auto mtime = fs::last_write_time(p, ec);
    if (ec) return false;
    return mtime >= cutoff;
}

cutoff는 “지금 − 7일” 등을 file_time_type으로 변환한 값입니다. C++20에서는 clock::now()와의 차로 비교하기 쉬워집니다. 복사 시에는 equivalent로 동일 inode(하드 링크) 여부를 참고해 중복 복사를 줄일 수 있습니다.

실전: 로그 관리 (로테이션·정리)

오래된 로그 삭제는 일반 파일 + 수정 시각으로 결정합니다. C++17에서는 file_time_type과 시계 변환이 구현체마다 달라서, 기준 시각 하나를 file_time_type으로 만들어 두고 항목마다 last_write_time과 비교하는 방식이 안전합니다.

void prune_old_logs(const fs::path& log_dir,
                    fs::file_time_type cutoff,
                    std::error_code& ec) {
    for (const auto& e : fs::directory_iterator(log_dir, ec)) {
        if (ec) break;
        if (!e.is_regular_file()) continue;
        auto mt = fs::last_write_time(e.path(), ec);
        if (ec) continue;
        if (mt < cutoff) {
            fs::remove(e.path(), ec);
        }
    }
}
// cutoff는 "지금 기준 7일 전" 등을 동일한 clock으로 맞춰 만든 값이어야 합니다.
// C++20 `std::chrono::file_clock`이 있으면 변환·비교가 더 명확해집니다.

디스크 임계치는 앞서 본 fs::space로 볼륨 여유를 확인한 뒤, 가장 오래된 로그부터 지우는 식으로 정책을 이중화하는 것이 안전합니다.

플랫폼별 차이

주제POSIX (Linux, macOS)Windows
권한 비트전통적 rwx, chmod와 유사한 모델perms는 단순화·에뮬; ACL·속성은 별도
경로 구분자/\와 / 모두 허용되나 표시는 \인 경우 많음
심볼릭 링크널리 사용관리자 권한·개발자 모드 등 환경 의존
대소문자대개 구분기본적으로 경로 비교 시 대소문자 무시에 가까움

실무 권장: 크로스 플랫폼 코드는 fs::path 연산을 쓰며, 권한은 “perms로 힌트를 얻되, 최종은 I/O 실패 처리”로 보완합니다. 배포 스크립트는 OS별로 permissions 호출을 분기하거나 문서에 한정할 수 있습니다.

자주 발생하는 문제

문제 1: 존재 확인

// ❌ 존재 확인 없이
auto size = fs::file_size("file.txt");  // 예외

// ✅ 존재 확인
if (fs::exists("file.txt")) {
    auto size = fs::file_size("file.txt");
}

문제 2: 디렉토리 크기

// ❌ 디렉토리에 file_size
// auto size = fs::file_size("dir");  // 예외

// ✅ 재귀 계산
uintmax_t size = 0;
for (const auto& entry : fs::recursive_directory_iterator("dir")) {
    if (entry.is_regular_file()) {
        size += entry.file_size();
    }
}

문제 3: 심볼릭 링크

fs::path link = "symlink";

// 링크 자체 상태
auto linkStatus = fs::symlink_status(link);

// 링크 대상 상태
auto targetStatus = fs::status(link);

문제 4: 권한 에러

// ❌ 권한 없으면 예외
for (const auto& entry : fs::recursive_directory_iterator("/")) {
    // 권한 에러
}

// ✅ 에러 무시
for (const auto& entry : fs::recursive_directory_iterator("/",
    fs::directory_options::skip_permission_denied)) {
    std::cout << entry.path() << std::endl;
}

파일 비교

fs::path p1 = "file1.txt";
fs::path p2 = "file2.txt";

// 같은 파일?
if (fs::equivalent(p1, p2)) {
    std::cout << "같은 파일" << std::endl;
}

// 수정 시간 비교
if (fs::last_write_time(p1) > fs::last_write_time(p2)) {
    std::cout << "p1이 더 최신" << std::endl;
}

FAQ

Q1: exists()와 status()는 무엇이 다른가요?

A: fs::exists(p)는 내부적으로 status(p)를 호출해 타입이 not_found가 아닌지만 확인하는 편의 함수입니다. 존재 여부 외에 파일 종류나 권한까지 필요하다면 status를 한 번 호출해 결과를 재사용하는 편이 시스템 호출을 줄일 수 있습니다.

Q2: 권한 부족으로 status 조회 자체가 실패하면 어떻게 되나요?

A: 예외 버전은 filesystem_error를 던지고, error_code 버전은 ec를 설정한 뒤 타입이 none인 file_status를 돌려줍니다. “없음”(not_found)과 “확인할 수 없음”(none, unknown)을 구분해 처리해야, 권한 문제를 파일이 없는 것으로 오판하지 않습니다.

Q3: 파일 크기는 file_status로 알 수 없나요?

A: file_status에는 크기와 시간이 들어 있지 않습니다. 크기는 fs::file_size, 수정 시간은 fs::last_write_time으로 따로 조회하며, 디렉터리 순회 중이라면 directory_entry의 같은 이름 멤버 함수가 캐시된 값을 쓸 수 있습니다.

Q4: 참고할 자료는?

A:

  • cppreference.com의 std::filesystem::status, file_status, perms 항목
  • “C++17 - The Complete Guide” (Nicolai Josuttis)의 filesystem 장

같이 보면 좋은 글