C++17 directory_iterator로 폴더 순회하기: 재귀 탐색, 필터링, 심볼릭 링크와 오류 처리
이 글의 핵심
디렉터리 순회는 권한이 없는 폴더 하나 때문에 예외가 터져 전체 작업이 중단되거나, 심볼릭 링크 순환으로 끝나지 않는 문제가 자주 생깁니다. 예외 대신 error_code를 쓰는 방법, skip_permission_denied 같은 순회 옵션, 순회 중 파일을 수정할 때의 주의점과 성능 고려사항을 함께 다룹니다.
directory_iterator란?
std::filesystem::directory_iterator는 C++17에서 표준에 들어온 디렉터리 순회 도구입니다. 그 전에는 POSIX의 opendir/readdir와 Windows의 FindFirstFile/FindNextFile을 플랫폼별로 따로 써야 했고, Boost.Filesystem이 사실상의 표준 역할을 했습니다. 표준 버전은 Boost.Filesystem을 바탕으로 만들어져 사용법이 거의 같고, 이터레이터가 돌려주는 directory_entry에는 경로와 함께 운영체제가 순회 중에 알려 준 파일 종류 같은 정보가 담깁니다.
순회 순서는 보장되지 않는다는 점을 먼저 기억해야 합니다. 이터레이터는 운영체제가 돌려주는 순서를 그대로 따르므로, NTFS에서는 대체로 이름순처럼 보이지만 Linux의 ext4에서는 해시 순서라 뒤섞여 나옵니다. 출력 결과를 테스트에서 비교하거나 사용자에게 정렬된 목록을 보여 줘야 한다면 경로를 std::vector에 모은 뒤 직접 정렬해야 합니다. 또 .과 .. 항목은 건너뛰고 돌려주므로 따로 걸러낼 필요가 없습니다.
#include <filesystem>
namespace fs = std::filesystem;
for (const auto& entry : fs::directory_iterator(".")) {
std::cout << entry.path() << std::endl;
}
재귀 순회
// 하위 디렉토리 포함
for (const auto& entry : fs::recursive_directory_iterator(".")) {
std::cout << entry.path() << std::endl;
}
directory_iterator vs recursive_directory_iterator
| 구분 | directory_iterator | recursive_directory_iterator |
|---|---|---|
| 범위 | 해당 경로의 직계 항목만 | 하위 디렉터리까지 재귀 순회 |
| 기본 동작 | 하위 폴더 안으로 들어가지 않음 | 자동으로 하위 트리를 펼침 |
| 옵션 | directory_options (동일 계열) | 동일 + disable_recursion_pending 등 |
| 전형적 용도 | 한 단계 목록·즉시 자식만 스캔 | 전체 검색·트리 합산·백업 스캔 |
recursive_directory_iterator에는 표준에 최대 깊이 옵션이 없습니다. 깊이만 제한하려면 directory_iterator를 직접 재귀·큐로 돌리거나, 특정 디렉터리에서 disable_recursion_pending()으로 그 가지만 건너뛰는 방식을 씁니다.
// 이름이 "build"인 디렉터리는 들어가지 않음 (예시)
for (fs::recursive_directory_iterator it(root), end; it != end; ++it) {
if (it->is_directory() && it->path().filename() == "build") {
it.disable_recursion_pending();
}
// ...
}
실전 예시
예시 1: 파일 목록
void listFiles(const fs::path& dir) {
for (const auto& entry : fs::directory_iterator(dir)) {
if (entry.is_regular_file()) {
std::cout << entry.path().filename() << std::endl;
}
}
}
예시 2: 특정 확장자 찾기
std::vector<fs::path> findFiles(const fs::path& dir,
const std::string& ext) {
std::vector<fs::path> result;
for (const auto& entry : fs::recursive_directory_iterator(dir)) {
if (entry.is_regular_file() &&
entry.path().extension() == ext) {
result.push_back(entry.path());
}
}
return result;
}
// 사용
auto cppFiles = findFiles(".", ".cpp");
예시 3: 디렉토리 크기
uintmax_t calculateDirSize(const fs::path& dir) {
uintmax_t size = 0;
for (const auto& entry : fs::recursive_directory_iterator(dir)) {
if (entry.is_regular_file()) {
size += entry.file_size();
}
}
return size;
}
예시 4: 파일 필터링
void filterFiles(const fs::path& dir) {
for (const auto& entry : fs::directory_iterator(dir)) {
if (entry.is_regular_file()) {
auto size = entry.file_size();
if (size > 1024 * 1024) { // 1MB 이상
std::cout << entry.path() << ": "
<< size << " bytes" << std::endl;
}
}
}
}
파일 필터링 패턴
표준에는 글로브/정규식 필터가 없습니다. 관용적으로는 다음을 조합합니다.
- 확장자:
path::extension(),path::stem()과 비교 (위 예시 2·4와 같음). - 타입:
entry.is_regular_file(),is_directory()로 디렉터리·파일만 골라내기. - 제외 디렉터리: 경로에
".git","node_modules"등이 포함되는지 검사해 조기continue. - 정규식: 필요 시
path::string()에std::regex적용 (Windows에서는 경로 인코딩 유의).
C++20이면 std::ranges와 함께 쓰면 필터 체인을 짧게 쓸 수 있습니다.
순회 옵션
// 기본
fs::directory_iterator it(dir);
// 심볼릭 링크 따라가기
fs::directory_iterator it(dir,
fs::directory_options::follow_directory_symlink);
// 권한 에러 무시
fs::directory_iterator it(dir,
fs::directory_options::skip_permission_denied);
심볼릭 링크 처리
- 디렉터리 심볼릭 링크:
recursive_directory_iterator는 기본값(directory_options::none)에서 디렉터리를 가리키는 심볼릭 링크를 항목으로 돌려주기만 하고 그 안으로 들어가지 않습니다. 그래서 기본 설정에서는 링크 순환 때문에 끝나지 않는 일이 생기지 않습니다. follow_directory_symlink: 디렉터리 링크를 따라가도록 명시할 때 사용합니다. 이 옵션을 켜면 링크가 상위 디렉터리를 가리키는 순환 구조에서 순회가 끝나지 않거나, 운영체제의 경로 길이·깊이 한도에 걸려 “Too many levels of symbolic links”나 “File name too long” 같은 에러로 멈출 수 있습니다. 표준이 순환을 자동으로 탐지해 주지 않으므로, 이 옵션을 쓸 때는fs::canonical로 얻은 실제 경로를 방문 집합에 기록해 이미 본 디렉터리는disable_recursion_pending()으로 건너뛰는 것이 안전합니다.- 링크 자체 vs 대상:
entry.is_symlink(),fs::symlink_status(entry.path())는 링크 자체의 타입을,fs::status는 최종 타겟을 봅니다. - 하드 링크: 동일 inode가 여러 경로에 나타나면 용량 합산 시 중복될 수 있습니다. 중복 제거가 필요하면
fs::equivalent나 플랫폼별 inode 조회를 고려합니다.
실전: 파일 검색 (심화)
이름 부분 문자열로 트리를 찾는 예입니다. 대규모 트리에서는 제외 디렉터리를 먼저 적용해 I/O를 줄입니다.
#include <filesystem>
#include <string>
#include <vector>
namespace fs = std::filesystem;
std::vector<fs::path> find_by_name(const fs::path& root, std::string_view key) {
std::vector<fs::path> out;
auto opts = fs::directory_options::skip_permission_denied;
std::error_code ec;
for (const auto& e : fs::recursive_directory_iterator(root, opts, ec)) {
if (ec) break;
if (!e.is_regular_file()) continue;
auto fn = e.path().filename().string();
if (fn.find(key) != std::string::npos)
out.push_back(e.path());
}
return out;
}
이 코드에는 주의할 점이 하나 있습니다. 생성자에 넘긴 ec는 루트 디렉터리를 여는 단계의 오류만 받습니다. 범위 기반 for는 내부적으로 operator++를 호출하는데, 이 연산자는 예외를 던지는 버전이라 순회 중간에 하위 디렉터리를 열지 못하면 ec가 아니라 filesystem_error 예외가 발생합니다. skip_permission_denied가 권한 오류는 건너뛰게 해 주지만, 순회 도중 다른 프로세스가 디렉터리를 지우는 경우 같은 다른 오류까지 예외 없이 처리하려면 아래처럼 반복자를 직접 다루며 increment(ec)를 써야 합니다.
std::error_code ec;
fs::recursive_directory_iterator it(root, opts, ec), end;
while (!ec && it != end) {
// it->path() 처리
it.increment(ec); // 예외 대신 ec로 오류 전달
}
제가 이 API를 쓰면서 가장 먼저 부딪힌 문제도 이것이었습니다. ec를 넘겼으니 예외가 나지 않을 거라 생각하고 try 없이 배포했는데, 로그 디렉터리처럼 순회 중에 파일이 계속 생기고 지워지는 경로에서 가끔 “filesystem error: directory iterator cannot advance” 같은 예외로 프로그램이 종료되었습니다. 파일이 자주 바뀌는 경로를 순회한다면 increment(ec) 방식이나 try/catch 중 하나는 반드시 필요합니다.
실전: 디스크 사용량 계산 (심화)
트리 합계는 일반 파일 크기를 더합니다. 심볼릭 링크는 file_size 동작이 정책에 따라 달라질 수 있으니, 링크는 건너뛰거나 별도 규칙을 두세요.
std::uintmax_t tree_bytes(const fs::path& p, std::error_code& ec) {
std::uintmax_t total = 0;
auto opts = fs::directory_options::skip_permission_denied;
for (const auto& e : fs::recursive_directory_iterator(p, opts, ec)) {
if (ec) break;
if (!e.is_regular_file()) continue;
std::error_code fe;
auto sz = fs::file_size(e.path(), fe);
if (!fe) total += sz;
}
return total;
}
에러 처리 (std::error_code)
예외를 쓰지 않을 때는 이터레이터 생성·file_size 등에 error_code 오버로드를 넘깁니다.
std::error_code ec;
for (const auto& entry : fs::directory_iterator("maybe_missing", ec)) {
if (ec) {
std::cerr << ec.message() << '\n';
break;
}
std::cout << entry.path() << '\n';
}
recursive_directory_iterator의 생성자에 ec를 넘기면 루트 열기 실패를 잡을 수 있으며, skip_permission_denied와 함께 쓰면 권한 없는 항목 때문에 전체가 끊기기 어렵습니다. 순회 중 일부 항목만 실패할 수 있으므로, 중요한 경로에서는 fs::file_size·fs::status마다 ec를 확인하는 편이 안전합니다.
성능 고려사항
- 불필요한 재귀 줄이기: 필요한 깊이만
directory_iterator로 직접 제어하면 거대한 하위 트리를 피할 수 있습니다. path복사: 루프에서entry.path()를 여러 번 호출하지 말고 지역fs::path에 담습니다.- 시스템 호출:
directory_entry::file_size()등이 캐시되는 경우가 많지만 보장은 아닙니다. 핫 루프에서는 불필요한 메타데이터 조회를 줄입니다. - 병렬화: 표준 이터레이터만으로는 병렬 순회가 없습니다. 서브트리 단위로 경로 목록을 나눈 뒤 스레드 풀에 넣는 방식이 일반적입니다.
- 네트워크 드라이브: 지연이 크므로 배치·캐시 정책을 애플리케이션 요구에 맞게 조정합니다.
자주 발생하는 문제
문제 1: 예외 처리
// ❌ 권한 에러
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;
}
문제 2: 심볼릭 링크와 순환
// 심볼릭 링크가 디렉터리를 가리키면 재귀 순회가 그쪽으로 들어갈 수 있음
// (표준이 자동으로 “순환 탐지”를 해주지는 않음)
// ✅ 권한 문제만 완화
for (const auto& entry : fs::recursive_directory_iterator(dir,
fs::directory_options::skip_permission_denied)) {
std::cout << entry.path() << std::endl;
}
// 순환·중복 방문을 막으려면 방문한 경로 집합·깊이 제한 등을 애플리케이션에서 둠
문제 3: 성능
// ❌ 매번 file_size 호출
for (const auto& entry : fs::directory_iterator(dir)) {
auto size = fs::file_size(entry.path()); // 시스템 콜
}
// ✅ entry 캐시 사용
for (const auto& entry : fs::directory_iterator(dir)) {
auto size = entry.file_size(); // 캐시될 수 있음 (플랫폼에 따라 다름)
}
directory_entry가 무엇을 캐시하는지는 구현과 플랫폼에 따라 다릅니다. Windows에서는 디렉터리를 읽는 API(FindNextFile)가 파일 크기와 수정 시간까지 한 번에 돌려주므로 entry.file_size()가 추가 시스템 호출 없이 끝나는 경우가 많습니다. 반면 Linux의 readdir는 이름과 파일 종류(d_type) 정도만 알려 주므로, libstdc++에서 entry.is_regular_file()은 캐시로 처리되더라도 entry.file_size()는 결국 stat을 호출합니다. 그래서 POSIX 환경에서는 두 코드의 성능 차이가 거의 없을 수 있고, 차이가 나는 것은 주로 is_regular_file() 같은 타입 검사입니다. 순회 중에 파일이 바뀌었다면 캐시된 값이 오래된 정보일 수 있으므로, 최신 값이 필요하면 entry.refresh()를 호출합니다.
문제 4: 수정 중 순회
// ❌ 순회 중 수정
for (const auto& entry : fs::directory_iterator(dir)) {
fs::remove(entry.path()); // 이후 순회 결과가 명시되지 않음
}
// ✅ 경로 수집 후 삭제
std::vector<fs::path> toDelete;
for (const auto& entry : fs::directory_iterator(dir)) {
toDelete.push_back(entry.path());
}
for (const auto& p : toDelete) {
fs::remove(p);
}
표준은 순회 도중 디렉터리에 파일이 추가되거나 삭제되면 그 변화가 이터레이터에 반영될지 명시하지 않는다(unspecified)고만 정합니다. 크래시가 나는 정의되지 않은 동작은 아니지만, 새로 만든 파일이 순회에 다시 나타나 같은 작업을 반복하거나, 파일 이름을 바꾸면 바뀐 이름으로 한 번 더 방문하는 식의 예측하기 어려운 결과가 생깁니다. 특히 recursive_directory_iterator로 순회하면서 디렉터리를 통째로 지우면, 이터레이터가 방금 지운 디렉터리로 들어가려다 오류를 냅니다. 경로를 먼저 모으고 나서 수정하는 방식이 가장 안전하며, 하위 트리 전체를 지우는 것이 목적이라면 직접 순회하지 말고 fs::remove_all을 쓰는 편이 간단합니다.
활용 패턴
// 1. 파일 검색
auto files = findFiles(".", ".cpp");
// 2. 디렉토리 크기
auto size = calculateDirSize(".");
// 3. 파일 백업
backupFiles("src", "backup");
// 4. 정리
cleanupOldFiles("temp", 7); // 7일 이상
FAQ
Q1: directory_iterator를 쓰려면 무엇이 필요한가요?
A: C++17 이상과 <filesystem> 헤더가 필요합니다. GCC 8에서는 -lstdc++fs, Clang 7~8의 libc++에서는 -lc++fs를 따로 링크해야 해서 “undefined reference to std::filesystem::...” 에러가 나는 경우가 있었지만, GCC 9 이상과 최신 Clang에서는 별도 링크가 필요 없습니다.
Q2: 재귀 순회에서 현재 깊이는 어떻게 알 수 있나요?
A: recursive_directory_iterator의 depth() 멤버가 시작 디렉터리 기준 깊이(0부터)를 돌려줍니다. 최대 깊이 옵션은 없으므로, depth()가 기준을 넘으면 disable_recursion_pending()을 호출해 그 아래로 내려가지 않게 하는 방식으로 깊이를 제한할 수 있습니다.
Q3: 예외 처리?
A: try/catch 또는 std::error_code 오버로드로 예외 없이 처리. skip_permission_denied로 권한 오류 완화.
Q4: 성능을 올리려면?
A: fs::file_size(entry.path()) 대신 entry.file_size()처럼 directory_entry의 멤버를 쓰면 캐시된 정보를 활용할 수 있고, 제외할 디렉터리는 disable_recursion_pending()으로 아예 들어가지 않는 것이 가장 효과가 큽니다. 캐시 범위는 플랫폼마다 다르므로 실제 환경에서 측정해 보는 것이 좋습니다.
Q5: 순회 중에 파일을 지우거나 만들어도 되나요?
A: 크래시는 아니지만 변경이 순회에 반영될지는 명시되지 않아 결과를 예측할 수 없습니다. 대상 경로를 먼저 모은 뒤 수정하세요.
Q6: directory_iterator 학습 리소스는?
A:
- “C++17 The Complete Guide”
- “C++ Primer”
- cppreference.com
같이 보면 좋은 글
- C++ filesystem::status로 파일 종류·권한·수정 시간 확인하기
- C++ std::filesystem 파일 연산: copy·rename·remove와 덮어쓰기·원자성 문제
- C++17 std::filesystem 빠른 참조: 경로 정규화, exists()의 TOCTOU, remove_all 주의점
- Windows에서만 파일을 못 찾을 때: std::filesystem 경로 연산, 디렉터리 순회, 권한
- C++ path