콘텐츠 대표 이미지 - C언어 파일 시스템과 디렉터리 조작을 통한실전 파일 관리 도구 완전 정복
📁 C언어 · 파일 시스템

C언어 파일 시스템과 디렉터리 조작을 통한
실전 파일 관리 도구 완전 정복

파일 탐색기를 직접 만들어보는 C언어 실전 여정 🗂️✨

C언어로 만드는 파일 관리 도구 구조 📁 루트 디렉터리 opendir() / readdir() 📁 서브 디렉터리 stat() / mkdir() 📄 파일 목록 fopen() / fread() / fwrite() 🔧 파일 조작 rename() / remove() 재귀 탐색 구현 파일 복사 / 이동 권한 / 메타데이터 POSIX API + 표준 C 라이브러리로 완성하는 파일 관리 시스템
🚀 들어가며 — 파일 관리 도구, 왜 C로 만들어?

솔직히 말하면 요즘 Python이나 Node.js로도 파일 관리 도구 뚝딱 만들 수 있는데, 굳이 C언어로?? 라고 생각할 수 있음 ㅋㅋㅋ
근데 진짜 시스템 레벨에서 파일이 어떻게 돌아가는지 이해하려면 C언어가 답임. 운영체제 커널이랑 가장 가까운 언어가 C거든.

파일 탐색기, 백업 도구, 로그 정리 스크립트, 디렉터리 동기화 프로그램... 이런 거 다 C로 만들 수 있고, 실제로 리눅스 유틸리티들(ls, cp, mv, rm 등)이 전부 C로 짜여 있음.

이 글에서는 C언어의 파일 시스템 API를 활용해서 실전에서 쓸 수 있는 파일 관리 도구를 만드는 방법을 처음부터 끝까지 파헤쳐볼 거야. 🔥

💡 이 글의 대상: C언어 기초 문법은 알고 있고, 파일 I/O나 시스템 프로그래밍으로 한 단계 업그레이드하고 싶은 분들에게 딱임!

📂 1. 파일 시스템의 기본 개념 — 운영체제가 파일을 보는 방식

C언어로 파일 관리 도구를 만들기 전에, 운영체제가 파일을 어떻게 관리하는지 알아야 함. 이거 모르면 나중에 버그 만났을 때 멘붕 옴 ㅋㅋ

파일 시스템은 크게 두 가지 관점으로 볼 수 있어:

🗃️ 1-1. 파일(File)이란?

운영체제에서 파일은 단순히 바이트의 연속된 시퀀스야. 텍스트 파일이든, 이미지 파일이든, 실행 파일이든 OS 입장에서는 그냥 바이트 덩어리임.
파일에는 두 가지 핵심 요소가 있어:

① 데이터(Data) — 파일의 실제 내용 (바이트 스트림)
② 메타데이터(Metadata) — 파일 이름, 크기, 생성일, 수정일, 권한, 소유자 등

리눅스/유닉스 계열에서는 이 메타데이터를 inode(아이노드)라는 자료구조에 저장함. inode에는 파일 이름 빼고 거의 모든 메타데이터가 들어있어. (파일 이름은 디렉터리 엔트리에 저장됨)

📁 1-2. 디렉터리(Directory)란?

디렉터리도 사실 파일임! 특수한 파일이지. 디렉터리 파일 안에는 파일 이름 → inode 번호의 매핑 테이블이 들어있어.
그래서 디렉터리를 열면 그 안에 있는 파일/서브디렉터리 목록을 얻을 수 있는 거임.

🔗 1-3. 경로(Path)의 두 가지 종류
종류설명예시
절대 경로루트(/)부터 시작하는 전체 경로/home/user/docs/file.txt
상대 경로현재 작업 디렉터리 기준 경로./docs/file.txt

C언어에서 파일 경로를 다룰 때 이 두 가지를 명확히 구분해야 함. 특히 재귀적으로 디렉터리를 탐색할 때 경로 조합을 잘못하면 버그 터짐 ㅠㅠ


📖 2. C언어 파일 I/O 핵심 API — 이것만 알면 80%는 됨

C언어에서 파일을 다루는 API는 크게 두 계층으로 나뉨:

① 표준 C 라이브러리 (stdio.h) — fopen, fclose, fread, fwrite, fprintf 등 → 버퍼링 지원, 이식성 좋음
② POSIX/시스템 콜 (unistd.h, fcntl.h) — open, close, read, write, stat 등 → 저수준, 리눅스/유닉스 전용
📝 2-1. 표준 파일 I/O (stdio.h)

가장 기본적인 파일 열기/읽기/쓰기/닫기 패턴:

C
#include <stdio.h>
#include <stdlib.h>

int main() {
    FILE *fp;
    char buffer[256];

    // 파일 열기 (읽기 모드)
    fp = fopen("example.txt", "r");
    if (fp == NULL) {
        perror("파일 열기 실패");
        return EXIT_FAILURE;
    }

    // 한 줄씩 읽기
    while (fgets(buffer, sizeof(buffer), fp) != NULL) {
        printf("%s", buffer);
    }

    // 파일 닫기 (반드시!)
    fclose(fp);
    return EXIT_SUCCESS;
}
⚠️ fopen() 후 반드시 fclose() 해야 함! 파일 디스크립터는 시스템 자원이라 안 닫으면 리소스 누수(resource leak) 발생. 특히 루프 안에서 파일 열 때 진짜 조심해야 함 ㅋㅋ
📋 2-2. fopen() 모드 완전 정리
모드의미파일 없을 때기존 내용
"r"읽기 전용에러 반환유지
"w"쓰기 전용새로 생성삭제됨 ⚠️
"a"추가 쓰기새로 생성유지, 끝에 추가
"r+"읽기+쓰기에러 반환유지
"w+"읽기+쓰기새로 생성삭제됨 ⚠️
"a+"읽기+추가새로 생성유지, 끝에 추가
"rb"바이너리 읽기에러 반환유지
"wb"바이너리 쓰기새로 생성삭제됨 ⚠️

바이너리 파일(이미지, 실행파일 등) 복사할 때는 반드시 "rb", "wb" 모드 써야 함. 텍스트 모드로 바이너리 읽으면 Windows에서 줄바꿈 변환 때문에 파일 깨짐 😱

🔢 2-3. fread / fwrite — 바이너리 파일 처리의 핵심
C
// 파일 복사 함수 (바이너리 방식)
int copy_file(const char *src, const char *dst) {
    FILE *fsrc = fopen(src, "rb");
    FILE *fdst = fopen(dst, "wb");
    
    if (!fsrc || !fdst) {
        if (fsrc) fclose(fsrc);
        if (fdst) fclose(fdst);
        return -1;
    }
    
    char buf[4096];  // 4KB 버퍼
    size_t n;
    
    while ((n = fread(buf, 1, sizeof(buf), fsrc)) > 0) {
        if (fwrite(buf, 1, n, fdst) != n) {
            fclose(fsrc);
            fclose(fdst);
            return -1;  // 쓰기 실패
        }
    }
    
    fclose(fsrc);
    fclose(fdst);
    return 0;  // 성공
}
💡 버퍼 크기는 보통 4096(4KB) 또는 8192(8KB)를 많이 씀. 이게 대부분의 OS에서 디스크 블록 크기와 맞아서 I/O 효율이 좋음!

🗂️ 3. 디렉터리 조작 API — 폴더를 C로 다루는 법

파일 관리 도구의 핵심은 디렉터리 탐색임. C언어에서 디렉터리를 다루려면 dirent.h 헤더가 필요해. 이게 POSIX 표준이라 리눅스/macOS에서 잘 돌아감.

🔍 3-1. 디렉터리 열기/읽기/닫기
C
#include <stdio.h>
#include <dirent.h>

void list_directory(const char *path) {
    DIR *dir;
    struct dirent *entry;
    
    // 디렉터리 열기
    dir = opendir(path);
    if (dir == NULL) {
        perror("디렉터리 열기 실패");
        return;
    }
    
    printf("📁 [%s] 내용:\n", path);
    
    // 엔트리 하나씩 읽기
    while ((entry = readdir(dir)) != NULL) {
        // "."과 ".."은 건너뜀
        if (strcmp(entry->d_name, ".") == 0 || 
            strcmp(entry->d_name, "..") == 0) {
            continue;
        }
        
        // 파일 타입 구분
        if (entry->d_type == DT_DIR) {
            printf("  📁 %s/\n", entry->d_name);
        } else {
            printf("  📄 %s\n", entry->d_name);
        }
    }
    
    // 디렉터리 닫기
    closedir(dir);
}
📊 3-2. struct dirent 구조체 핵심 필드
필드타입설명
d_namechar[]파일/디렉터리 이름
d_typeunsigned char파일 타입 (DT_REG, DT_DIR 등)
d_inoino_tinode 번호
🏷️ 3-3. d_type 상수값 정리
상수의미
DT_REG일반 파일
DT_DIR디렉터리
DT_LNK심볼릭 링크
DT_FIFO파이프
DT_SOCK소켓
DT_CHR문자 장치
DT_BLK블록 장치
DT_UNKNOWN알 수 없음
⚠️ d_type이 DT_UNKNOWN을 반환하는 파일시스템도 있음 (일부 NFS, FAT32 등). 이럴 때는 stat()으로 타입을 확인해야 함!

📊 4. stat() 함수 — 파일 메타데이터의 모든 것

파일 관리 도구에서 파일 크기, 수정 시간, 권한 등을 알려면 stat() 함수가 필수임. 이거 진짜 자주 씀 ㅋㅋ

🔬 4-1. stat() 기본 사용법
C
#include <stdio.h>
#include <sys/stat.h>
#include <time.h>

void print_file_info(const char *path) {
    struct stat st;
    
    if (stat(path, &st) == -1) {
        perror("stat 실패");
        return;
    }
    
    printf("파일명  : %s\n", path);
    printf("크기    : %lld bytes\n", (long long)st.st_size);
    printf("권한    : %o\n", st.st_mode & 0777);
    printf("수정시간: %s", ctime(&st.st_mtime));
    
    // 파일 타입 확인
    if (S_ISREG(st.st_mode))  printf("타입    : 일반 파일\n");
    if (S_ISDIR(st.st_mode))  printf("타입    : 디렉터리\n");
    if (S_ISLNK(st.st_mode))  printf("타입    : 심볼릭 링크\n");
}
📋 4-2. struct stat 주요 필드
필드타입설명
st_sizeoff_t파일 크기 (바이트)
st_modemode_t파일 타입 + 권한 비트
st_mtimetime_t마지막 수정 시간
st_atimetime_t마지막 접근 시간
st_ctimetime_t마지막 상태 변경 시간
st_nlinknlink_t하드 링크 수
st_uiduid_t소유자 UID
st_gidgid_t그룹 GID
st_inoino_tinode 번호
🔐 4-3. 파일 권한 비트 해석

st_mode에서 권한 비트를 읽는 방법:

C
void print_permissions(mode_t mode) {
    printf("권한: ");
    printf((S_ISDIR(mode))  ? "d" : "-");
    printf((mode & S_IRUSR) ? "r" : "-");
    printf((mode & S_IWUSR) ? "w" : "-");
    printf((mode & S_IXUSR) ? "x" : "-");
    printf((mode & S_IRGRP) ? "r" : "-");
    printf((mode & S_IWGRP) ? "w" : "-");
    printf((mode & S_IXGRP) ? "x" : "-");
    printf((mode & S_IROTH) ? "r" : "-");
    printf((mode & S_IWOTH) ? "w" : "-");
    printf((mode & S_IXOTH) ? "x" : "-");
    printf("\n");
    // 예: drwxr-xr-x
}
💡 이게 바로 ls -l 명령어가 보여주는 권한 표시 방식임! C로 직접 구현하면 ls 명령어가 어떻게 동작하는지 이해됨 ㅋㅋ
파일 관리 도구 핵심 기능 구조도 파일 관리 도구 코어 📁 디렉터리 탐색 opendir/readdir 📄 파일 복사 fread/fwrite 🔧 파일 이동/삭제 rename/remove 📊 파일 정보 stat/lstat 📝 파일 검색 재귀 탐색 🔐 권한 관리 chmod/stat 모든 기능이 유기적으로 연결된 완성형 파일 관리 시스템

🔄 5. 재귀적 디렉터리 탐색 — ls -R 직접 구현

파일 관리 도구의 꽃은 재귀 탐색임! 디렉터리 안의 디렉터리를 계속 파고들어가는 거. 이거 구현하면 진짜 뿌듯함 ㅋㅋㅋ

🌳 5-1. 재귀 탐색 전체 구현
C
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dirent.h>
#include <sys/stat.h>

#define MAX_PATH 4096

// 재귀적으로 디렉터리 탐색
void traverse_directory(const char *path, int depth) {
    DIR *dir;
    struct dirent *entry;
    struct stat st;
    char full_path[MAX_PATH];
    
    dir = opendir(path);
    if (dir == NULL) {
        perror("opendir 실패");
        return;
    }
    
    while ((entry = readdir(dir)) != NULL) {
        // "."과 ".." 건너뜀
        if (strcmp(entry->d_name, ".") == 0 ||
            strcmp(entry->d_name, "..") == 0) {
            continue;
        }
        
        // 전체 경로 조합
        snprintf(full_path, sizeof(full_path), 
                 "%s/%s", path, entry->d_name);
        
        // 들여쓰기로 깊이 표현
        for (int i = 0; i < depth; i++) {
            printf("  ");
        }
        
        // stat으로 파일 정보 가져오기
        if (lstat(full_path, &st) == -1) {
            perror("lstat 실패");
            continue;
        }
        
        if (S_ISDIR(st.st_mode)) {
            printf("📁 %s/\n", entry->d_name);
            // 재귀 호출!
            traverse_directory(full_path, depth + 1);
        } else if (S_ISLNK(st.st_mode)) {
            printf("🔗 %s -> (링크)\n", entry->d_name);
        } else {
            printf("📄 %s (%lld bytes)\n", 
                   entry->d_name, (long long)st.st_size);
        }
    }
    
    closedir(dir);
}

int main(int argc, char *argv[]) {
    const char *start_path = (argc > 1) ? argv[1] : ".";
    printf("=== 디렉터리 트리: %s ===\n", start_path);
    traverse_directory(start_path, 0);
    return 0;
}
⚠️ 심볼릭 링크 주의! stat() 대신 lstat()을 써야 심볼릭 링크를 따라가지 않음. stat()은 링크가 가리키는 파일의 정보를 반환하지만, lstat()은 링크 자체의 정보를 반환함. 재귀 탐색 시 심볼릭 링크를 stat()으로 처리하면 무한 루프 빠질 수 있음 😱
📏 5-2. 디렉터리 전체 크기 계산
C
// 디렉터리 전체 크기 재귀 계산 (du 명령어 원리)
long long get_dir_size(const char *path) {
    DIR *dir;
    struct dirent *entry;
    struct stat st;
    char full_path[MAX_PATH];
    long long total = 0;
    
    dir = opendir(path);
    if (!dir) return 0;
    
    while ((entry = readdir(dir)) != NULL) {
        if (strcmp(entry->d_name, ".") == 0 ||
            strcmp(entry->d_name, "..") == 0) continue;
        
        snprintf(full_path, sizeof(full_path),
                 "%s/%s", path, entry->d_name);
        
        if (lstat(full_path, &st) == -1) continue;
        
        if (S_ISDIR(st.st_mode)) {
            total += get_dir_size(full_path);  // 재귀
        } else {
            total += st.st_size;
        }
    }
    
    closedir(dir);
    return total;
}

// 크기를 사람이 읽기 좋은 형태로 변환
void print_human_size(long long bytes) {
    if (bytes < 1024)
        printf("%lld B", bytes);
    else if (bytes < 1024 * 1024)
        printf("%.1f KB", bytes / 1024.0);
    else if (bytes < 1024 * 1024 * 1024)
        printf("%.1f MB", bytes / (1024.0 * 1024));
    else
        printf("%.1f GB", bytes / (1024.0 * 1024 * 1024));
}

📋 6. 디렉터리 생성/삭제 — mkdir, rmdir, remove
🏗️ 6-1. 디렉터리 생성 (mkdir)
C
#include <sys/stat.h>
#include <sys/types.h>
#include <errno.h>

// 단일 디렉터리 생성
int create_dir(const char *path) {
    // 0755 = rwxr-xr-x 권한
    if (mkdir(path, 0755) == -1) {
        if (errno == EEXIST) {
            printf("이미 존재하는 디렉터리: %s\n", path);
            return 0;  // 이미 있으면 OK로 처리
        }
        perror("mkdir 실패");
        return -1;
    }
    printf("✅ 디렉터리 생성: %s\n", path);
    return 0;
}

// 중첩 디렉터리 생성 (mkdir -p 원리)
// 예: /a/b/c 경로에서 /a, /a/b, /a/b/c 순서로 생성
int create_dir_recursive(const char *path) {
    char tmp[MAX_PATH];
    char *p = NULL;
    
    snprintf(tmp, sizeof(tmp), "%s", path);
    
    for (p = tmp + 1; *p; p++) {
        if (*p == '/') {
            *p = '\0';
            mkdir(tmp, 0755);  // 에러 무시 (이미 있을 수 있음)
            *p = '/';
        }
    }
    return mkdir(tmp, 0755);
}
🗑️ 6-2. 파일/디렉터리 삭제
C
#include <stdio.h>
#include <dirent.h>
#include <sys/stat.h>
#include <string.h>

// 재귀적 디렉터리 삭제 (rm -rf 원리)
int remove_recursive(const char *path) {
    struct stat st;
    
    if (lstat(path, &st) == -1) return -1;
    
    // 일반 파일이나 심볼릭 링크면 바로 삭제
    if (!S_ISDIR(st.st_mode)) {
        return remove(path);
    }
    
    // 디렉터리면 내용물 먼저 삭제
    DIR *dir = opendir(path);
    if (!dir) return -1;
    
    struct dirent *entry;
    char full_path[MAX_PATH];
    
    while ((entry = readdir(dir)) != NULL) {
        if (strcmp(entry->d_name, ".") == 0 ||
            strcmp(entry->d_name, "..") == 0) continue;
        
        snprintf(full_path, sizeof(full_path),
                 "%s/%s", path, entry->d_name);
        remove_recursive(full_path);  // 재귀 삭제
    }
    
    closedir(dir);
    return rmdir(path);  // 빈 디렉터리 삭제
}
⚠️ rm -rf는 진짜 조심해야 함! 재귀 삭제 함수는 복구 불가능한 삭제를 수행함. 실제 도구에서는 삭제 전 확인 프롬프트나 휴지통 기능을 꼭 넣어야 함. 경로 조합 버그로 잘못된 경로 삭제하면 진짜 큰일남 😰

🔍 7. 파일 검색 기능 구현 — find 명령어 만들기

리눅스의 find 명령어처럼 특정 조건에 맞는 파일을 찾는 기능을 구현해보자. 이거 만들면 진짜 뿌듯함 ㅋㅋㅋ

🔎 7-1. 이름으로 파일 찾기
C
#include <stdio.h>
#include <string.h>
#include <dirent.h>
#include <sys/stat.h>

// 간단한 와일드카드 매칭 (끝 확장자 비교)
int match_extension(const char *filename, const char *ext) {
    const char *dot = strrchr(filename, '.');
    if (!dot) return 0;
    return strcmp(dot + 1, ext) == 0;
}

// 파일명 패턴으로 검색
void find_files(const char *dir_path, const char *pattern, 
                int *count) {
    DIR *dir;
    struct dirent *entry;
    struct stat st;
    char full_path[MAX_PATH];
    
    dir = opendir(dir_path);
    if (!dir) return;
    
    while ((entry = readdir(dir)) != NULL) {
        if (strcmp(entry->d_name, ".") == 0 ||
            strcmp(entry->d_name, "..") == 0) continue;
        
        snprintf(full_path, sizeof(full_path),
                 "%s/%s", dir_path, entry->d_name);
        
        if (lstat(full_path, &st) == -1) continue;
        
        if (S_ISDIR(st.st_mode)) {
            // 서브디렉터리 재귀 탐색
            find_files(full_path, pattern, count);
        } else {
            // 파일명에 패턴이 포함되는지 확인
            if (strstr(entry->d_name, pattern) != NULL) {
                printf("🔍 발견: %s (%lld bytes)\n",
                       full_path, (long long)st.st_size);
                (*count)++;
            }
        }
    }
    
    closedir(dir);
}

int main() {
    int count = 0;
    printf("=== .c 파일 검색 ===\n");
    find_files(".", ".c", &count);
    printf("총 %d개 파일 발견\n", count);
    return 0;
}
📅 7-2. 날짜/크기 조건으로 파일 찾기
C
#include <time.h>

// 특정 크기 이상의 파일 찾기
void find_large_files(const char *dir_path, 
                      long long min_size) {
    DIR *dir;
    struct dirent *entry;
    struct stat st;
    char full_path[MAX_PATH];
    
    dir = opendir(dir_path);
    if (!dir) return;
    
    while ((entry = readdir(dir)) != NULL) {
        if (strcmp(entry->d_name, ".") == 0 ||
            strcmp(entry->d_name, "..") == 0) continue;
        
        snprintf(full_path, sizeof(full_path),
                 "%s/%s", dir_path, entry->d_name);
        
        if (lstat(full_path, &st) == -1) continue;
        
        if (S_ISDIR(st.st_mode)) {
            find_large_files(full_path, min_size);
        } else if (st.st_size >= min_size) {
            printf("📦 %s : ", full_path);
            print_human_size(st.st_size);
            printf("\n");
        }
    }
    closedir(dir);
}

// 최근 N일 이내 수정된 파일 찾기
void find_recent_files(const char *dir_path, int days) {
    time_t cutoff = time(NULL) - (days * 24 * 3600);
    DIR *dir;
    struct dirent *entry;
    struct stat st;
    char full_path[MAX_PATH];
    
    dir = opendir(dir_path);
    if (!dir) return;
    
    while ((entry = readdir(dir)) != NULL) {
        if (strcmp(entry->d_name, ".") == 0 ||
            strcmp(entry->d_name, "..") == 0) continue;
        
        snprintf(full_path, sizeof(full_path),
                 "%s/%s", dir_path, entry->d_name);
        
        if (lstat(full_path, &st) == -1) continue;
        
        if (S_ISDIR(st.st_mode)) {
            find_recent_files(full_path, days);
        } else if (st.st_mtime >= cutoff) {
            printf("🕐 %s (수정: %s)", 
                   full_path, ctime(&st.st_mtime));
        }
    }
    closedir(dir);
}

📦 8. 파일 복사/이동 완전 구현
📋 8-1. 메타데이터 보존 파일 복사

단순히 내용만 복사하는 게 아니라 수정 시간, 권한까지 보존하는 고급 복사 함수:

C
#include <utime.h>

int copy_file_with_metadata(const char *src, const char *dst) {
    struct stat st;
    
    // 원본 파일 정보 가져오기
    if (stat(src, &st) == -1) {
        perror("stat 실패");
        return -1;
    }
    
    // 파일 내용 복사
    FILE *fsrc = fopen(src, "rb");
    FILE *fdst = fopen(dst, "wb");
    
    if (!fsrc || !fdst) {
        if (fsrc) fclose(fsrc);
        if (fdst) fclose(fdst);
        return -1;
    }
    
    char buf[8192];
    size_t n;
    int error = 0;
    
    while ((n = fread(buf, 1, sizeof(buf), fsrc)) > 0) {
        if (fwrite(buf, 1, n, fdst) != n) {
            error = 1;
            break;
        }
    }
    
    fclose(fsrc);
    fclose(fdst);
    
    if (error) return -1;
    
    // 권한 복사
    chmod(dst, st.st_mode & 0777);
    
    // 수정 시간 복사
    struct utimbuf times;
    times.actime  = st.st_atime;
    times.modtime = st.st_mtime;
    utime(dst, &times);
    
    printf("✅ 복사 완료: %s → %s\n", src, dst);
    return 0;
}

// 파일 이동 (같은 파일시스템이면 rename, 다르면 복사+삭제)
int move_file(const char *src, const char *dst) {
    // rename()은 같은 파일시스템 내에서만 원자적으로 동작
    if (rename(src, dst) == 0) {
        printf("✅ 이동 완료: %s → %s\n", src, dst);
        return 0;
    }
    
    // 다른 파일시스템이면 복사 후 삭제
    if (errno == EXDEV) {
        if (copy_file_with_metadata(src, dst) == 0) {
            remove(src);
            return 0;
        }
    }
    
    perror("이동 실패");
    return -1;
}
💡 rename()은 같은 파일시스템 내에서는 inode 포인터만 바꾸는 원자적 연산이라 매우 빠름. 하지만 다른 파일시스템(예: /tmp → /home)으로 이동할 때는 EXDEV 에러가 나고 실제 복사+삭제가 필요함. 이게 mv 명령어의 내부 동작 원리임!

🛠️ 9. 실전 파일 관리 도구 — 통합 구현

지금까지 배운 내용을 합쳐서 실제로 쓸 수 있는 파일 관리 도구를 만들어보자. 재능넷 같은 플랫폼에서 파일 관련 재능을 거래할 때도 이런 도구 구현 능력이 큰 강점이 됨! 😎

🎯 9-1. 메인 구조 설계
C
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dirent.h>
#include <sys/stat.h>
#include <unistd.h>

#define MAX_PATH 4096
#define VERSION  "1.0.0"

// 명령어 타입 열거형
typedef enum {
    CMD_LIST,    // 목록 보기
    CMD_COPY,    // 복사
    CMD_MOVE,    // 이동
    CMD_DELETE,  // 삭제
    CMD_FIND,    // 검색
    CMD_INFO,    // 정보
    CMD_TREE,    // 트리 보기
    CMD_UNKNOWN
} CommandType;

// 옵션 구조체
typedef struct {
    CommandType cmd;
    char src[MAX_PATH];
    char dst[MAX_PATH];
    char pattern[256];
    int recursive;    // -r 옵션
    int verbose;      // -v 옵션
    int force;        // -f 옵션
} Options;

// 사용법 출력
void print_usage(const char *prog) {
    printf("파일 관리 도구 v%s\n\n", VERSION);
    printf("사용법: %s <명령어> [옵션] [경로]\n\n", prog);
    printf("명령어:\n");
    printf("  list  <경로>          파일 목록 보기\n");
    printf("  tree  <경로>          트리 구조 보기\n");
    printf("  copy  <원본> <대상>   파일/디렉터리 복사\n");
    printf("  move  <원본> <대상>   파일/디렉터리 이동\n");
    printf("  del   <경로>          파일/디렉터리 삭제\n");
    printf("  find  <경로> <패턴>   파일 검색\n");
    printf("  info  <경로>          파일 정보 보기\n");
}
📊 9-2. 파일 정보 출력 (info 명령어)
C
void cmd_info(const char *path) {
    struct stat st;
    
    if (lstat(path, &st) == -1) {
        perror("파일 정보 조회 실패");
        return;
    }
    
    printf("\n📋 파일 정보: %s\n", path);
    printf("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
    
    // 파일 타입
    printf("타입     : ");
    if (S_ISREG(st.st_mode))       printf("일반 파일\n");
    else if (S_ISDIR(st.st_mode))  printf("디렉터리\n");
    else if (S_ISLNK(st.st_mode))  printf("심볼릭 링크\n");
    else if (S_ISFIFO(st.st_mode)) printf("파이프\n");
    else                            printf("기타\n");
    
    // 크기
    printf("크기     : %lld bytes (", (long long)st.st_size);
    print_human_size(st.st_size);
    printf(")\n");
    
    // 권한
    printf("권한     : ");
    print_permissions(st.st_mode);
    
    // 시간 정보
    printf("생성시간 : %s", ctime(&st.st_ctime));
    printf("수정시간 : %s", ctime(&st.st_mtime));
    printf("접근시간 : %s", ctime(&st.st_atime));
    
    // 링크 수
    printf("링크 수  : %lu\n", (unsigned long)st.st_nlink);
    
    // inode 번호
    printf("inode    : %lu\n", (unsigned long)st.st_ino);
    
    printf("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
}
🌳 9-3. 트리 출력 (tree 명령어 스타일)
C
// tree 명령어처럼 예쁜 트리 출력
void print_tree(const char *path, const char *prefix, 
                int is_last) {
    DIR *dir;
    struct dirent *entry;
    struct stat st;
    char full_path[MAX_PATH];
    char new_prefix[MAX_PATH];
    
    dir = opendir(path);
    if (!dir) return;
    
    // 엔트리 수 먼저 세기 (마지막 항목 판단용)
    int count = 0;
    while (readdir(dir) != NULL) count++;
    count -= 2;  // "."과 ".." 제외
    rewinddir(dir);
    
    int current = 0;
    while ((entry = readdir(dir)) != NULL) {
        if (strcmp(entry->d_name, ".") == 0 ||
            strcmp(entry->d_name, "..") == 0) continue;
        
        current++;
        int last = (current == count);
        
        snprintf(full_path, sizeof(full_path),
                 "%s/%s", path, entry->d_name);
        
        lstat(full_path, &st);
        
        // 트리 라인 출력
        printf("%s", prefix);
        printf(last ? "└── " : "├── ");
        
        if (S_ISDIR(st.st_mode)) {
            printf("📁 %s\n", entry->d_name);
            // 재귀 호출
            snprintf(new_prefix, sizeof(new_prefix),
                     "%s%s", prefix, last ? "    " : "│   ");
            print_tree(full_path, new_prefix, last);
        } else {
            printf("📄 %s\n", entry->d_name);
        }
    }
    closedir(dir);
}
💡 이 코드를 컴파일하면 실제 tree 명령어처럼 ├──, └──, │ 문자로 예쁜 트리 구조를 출력함! 터미널에서 보면 진짜 멋있음 ㅋㅋㅋ

⚡ 10. 에러 처리와 안전한 파일 조작

파일 관리 도구에서 에러 처리는 진짜 중요함. 잘못된 에러 처리 하나가 데이터 손실로 이어질 수 있거든. 이건 절대 대충 하면 안 됨!

🛡️ 10-1. errno를 활용한 상세 에러 처리
C
#include <errno.h>
#include <string.h>

// 에러 코드별 한국어 메시지
const char* get_error_msg(int err) {
    switch (err) {
        case ENOENT:  return "파일 또는 디렉터리가 없음";
        case EACCES:  return "권한 없음";
        case EEXIST:  return "이미 존재함";
        case ENOTDIR: return "디렉터리가 아님";
        case EISDIR:  return "디렉터리임 (파일 아님)";
        case ENOSPC:  return "디스크 공간 부족";
        case EROFS:   return "읽기 전용 파일시스템";
        case EMFILE:  return "열린 파일 수 초과";
        case ENOMEM:  return "메모리 부족";
        case EXDEV:   return "다른 파일시스템";
        default:      return strerror(err);
    }
}

// 안전한 파일 삭제 (확인 포함)
int safe_delete(const char *path, int force) {
    struct stat st;
    
    if (lstat(path, &st) == -1) {
        fprintf(stderr, "❌ 오류: %s - %s\n", 
                path, get_error_msg(errno));
        return -1;
    }
    
    // force 옵션 없으면 확인
    if (!force) {
        printf("정말 삭제하시겠습니까? %s [y/N]: ", path);
        char answer[4];
        fgets(answer, sizeof(answer), stdin);
        if (answer[0] != 'y' && answer[0] != 'Y') {
            printf("취소됨\n");
            return 0;
        }
    }
    
    if (S_ISDIR(st.st_mode)) {
        return remove_recursive(path);
    } else {
        if (remove(path) == -1) {
            fprintf(stderr, "❌ 삭제 실패: %s - %s\n",
                    path, get_error_msg(errno));
            return -1;
        }
    }
    
    printf("✅ 삭제 완료: %s\n", path);
    return 0;
}
🔒 10-2. 원자적 파일 쓰기 (데이터 손실 방지)
C
// 임시 파일에 쓰고 rename으로 원자적 교체
// 쓰기 중 프로그램이 죽어도 원본 파일은 안전함!
int atomic_write(const char *path, const char *data, 
                 size_t len) {
    char tmp_path[MAX_PATH];
    snprintf(tmp_path, sizeof(tmp_path), "%s.tmp.XXXXXX", path);
    
    // 임시 파일 생성
    int fd = mkstemp(tmp_path);
    if (fd == -1) {
        perror("임시 파일 생성 실패");
        return -1;
    }
    
    FILE *fp = fdopen(fd, "wb");
    if (!fp) {
        close(fd);
        unlink(tmp_path);
        return -1;
    }
    
    // 데이터 쓰기
    if (fwrite(data, 1, len, fp) != len) {
        fclose(fp);
        unlink(tmp_path);
        return -1;
    }
    
    // 디스크에 강제 플러시
    fflush(fp);
    fsync(fd);  // OS 버퍼 → 디스크
    fclose(fp);
    
    // 원자적으로 파일 교체
    if (rename(tmp_path, path) == -1) {
        unlink(tmp_path);
        return -1;
    }
    
    return 0;
}
🔵 원자적 쓰기 패턴은 데이터베이스, 설정 파일 저장 등에서 필수적으로 사용됨. 임시 파일에 쓰고 rename()으로 교체하면, 쓰기 도중 프로그램이 죽어도 원본 파일은 손상되지 않음!

🪟 11. Windows 호환성 — 크로스 플랫폼 고려

지금까지 코드는 리눅스/macOS(POSIX) 기준임. Windows에서도 돌아가게 하려면 약간의 처리가 필요함.

🔄 11-1. 플랫폼별 조건부 컴파일
C
#ifdef _WIN32
    // Windows 전용 헤더
    #include <windows.h>
    #include <direct.h>
    #define mkdir(path, mode) _mkdir(path)
    #define PATH_SEP '\\'
#else
    // POSIX 전용 헤더
    #include <unistd.h>
    #include <dirent.h>
    #include <sys/stat.h>
    #define PATH_SEP '/'
#endif

// 경로 구분자 정규화
void normalize_path(char *path) {
    for (char *p = path; *p; p++) {
        if (*p == '\\' || *p == '/') {
            *p = PATH_SEP;
        }
    }
}
기능POSIX (Linux/Mac)Windows
디렉터리 생성mkdir(path, mode)_mkdir(path)
디렉터리 삭제rmdir(path)_rmdir(path)
현재 디렉터리getcwd()_getcwd()
경로 구분자/\
파일 숨김이름이 .으로 시작파일 속성 플래그
대소문자 구분구분함구분 안 함

🚀 12. 성능 최적화 팁

대용량 파일이나 수만 개의 파일을 다룰 때 성능이 중요해짐. 몇 가지 최적화 팁을 알아보자.

⚡ 12-1. 버퍼 크기 최적화
파일 복사 시 버퍼 크기는 성능에 직접적인 영향을 미침:

• 너무 작은 버퍼 (예: 1 byte) → 시스템 콜 오버헤드 폭발 💥
• 적당한 버퍼 (4KB~64KB) → 대부분의 경우 최적
• 너무 큰 버퍼 (예: 1GB) → 메모리 낭비, 캐시 미스 증가

일반적으로 4096 ~ 65536 bytes가 최적. 파일시스템 블록 크기(보통 4KB)의 배수가 좋음.
🗂️ 12-2. 대용량 디렉터리 처리
C
// 파일 목록을 배열에 저장 후 정렬 (scandir 활용)
#include <dirent.h>

int compare_names(const struct dirent **a, 
                  const struct dirent **b) {
    return strcmp((*a)->d_name, (*b)->d_name);
}

void list_sorted(const char *path) {
    struct dirent **namelist;
    int n;
    
    // scandir: 디렉터리 내용을 배열로 읽고 정렬
    n = scandir(path, &namelist, NULL, compare_names);
    if (n == -1) {
        perror("scandir 실패");
        return;
    }
    
    for (int i = 0; i < n; i++) {
        if (strcmp(namelist[i]->d_name, ".") != 0 &&
            strcmp(namelist[i]->d_name, "..") != 0) {
            printf("%s\n", namelist[i]->d_name);
        }
        free(namelist[i]);
    }
    free(namelist);
}
💡 scandir()은 opendir() + readdir() + 정렬을 한 번에 해주는 편리한 함수임. 정렬된 파일 목록이 필요할 때 매우 유용함!
🔢 12-3. 파일 개수/크기 통계 빠르게 구하기
C
typedef struct {
    long long total_files;
    long long total_dirs;
    long long total_size;
    long long largest_file_size;
    char largest_file_path[MAX_PATH];
} DirStats;

void calc_stats(const char *path, DirStats *stats) {
    DIR *dir;
    struct dirent *entry;
    struct stat st;
    char full_path[MAX_PATH];
    
    dir = opendir(path);
    if (!dir) return;
    
    while ((entry = readdir(dir)) != NULL) {
        if (strcmp(entry->d_name, ".") == 0 ||
            strcmp(entry->d_name, "..") == 0) continue;
        
        snprintf(full_path, sizeof(full_path),
                 "%s/%s", path, entry->d_name);
        
        if (lstat(full_path, &st) == -1) continue;
        
        if (S_ISDIR(st.st_mode)) {
            stats->total_dirs++;
            calc_stats(full_path, stats);  // 재귀
        } else {
            stats->total_files++;
            stats->total_size += st.st_size;
            
            if (st.st_size > stats->largest_file_size) {
                stats->largest_file_size = st.st_size;
                strncpy(stats->largest_file_path, full_path,
                        MAX_PATH - 1);
            }
        }
    }
    closedir(dir);
}

🎓 13. 실전 활용 사례 — 이런 데 써먹을 수 있음

지금까지 배운 내용으로 실제로 어떤 도구를 만들 수 있는지 살펴보자. 재능넷에서도 이런 C언어 파일 시스템 프로그래밍 관련 재능이 꽤 거래되고 있음 😊

도구 종류핵심 기능사용 API
🗂️ 파일 탐색기디렉터리 탐색, 파일 목록opendir, readdir, stat
💾 백업 도구파일 복사, 변경 감지fread, fwrite, stat(mtime)
🔍 파일 검색기이름/크기/날짜 검색재귀 탐색, stat
🧹 디스크 정리대용량/오래된 파일 찾기stat(size, mtime)
📊 디스크 분석용량 통계, 트리 시각화재귀 stat, 집계
🔄 파일 동기화두 디렉터리 비교/동기화stat, fread, fwrite
📝 로그 관리로그 파일 로테이션rename, fopen("a")
🗜️ 아카이브파일 묶기/풀기fread, fwrite, stat
🔵 실제 프로젝트 팁: 이 모든 기능을 하나의 큰 프로그램으로 만들기보다는, 각 기능을 독립적인 함수/모듈로 분리하고 필요에 따라 조합하는 방식이 훨씬 유지보수하기 좋음. Unix 철학인 "하나의 도구가 하나의 일을 잘 하도록" 원칙을 따르는 거임!

✅ 14. 핵심 정리 — 파일 관리 도구 체크리스트
📌 필수 헤더 파일 정리
헤더주요 기능
stdio.hfopen, fclose, fread, fwrite, printf
dirent.hopendir, readdir, closedir, scandir
sys/stat.hstat, lstat, mkdir, chmod
unistd.hunlink, rmdir, getcwd, access
errno.herrno, 에러 코드 상수
string.hstrcmp, strcpy, strcat, strstr
time.htime, ctime, difftime
utime.hutime (파일 시간 변경)
⚠️ 자주 하는 실수 TOP 5
1️⃣ fopen() 후 fclose() 안 함 → 파일 디스크립터 누수
2️⃣ "."과 ".." 엔트리 처리 안 함 → 무한 재귀 루프
3️⃣ stat() vs lstat() 혼동 → 심볼릭 링크 무한 루프
4️⃣ 경로 버퍼 오버플로우 → snprintf 대신 sprintf 사용
5️⃣ 에러 반환값 무시 → 조용한 데이터 손실
🏆 좋은 파일 관리 도구의 조건
  • ✅ 모든 에러를 적절히 처리하고 사용자에게 명확한 메시지 제공
  • ✅ 파괴적인 작업(삭제, 덮어쓰기) 전 확인 프롬프트
  • ✅ 원자적 쓰기로 데이터 무결성 보장
  • ✅ 심볼릭 링크 순환 참조 방지
  • ✅ 경로 버퍼 크기 안전하게 관리
  • ✅ 크로스 플랫폼 호환성 고려
  • ✅ 대용량 파일/디렉터리에서도 안정적인 성능
🎉 C언어 파일 관리 도구 완성! opendir · readdir · stat · fread · fwrite · rename · remove 📁 디렉터리 탐색 재귀 + POSIX API ⚡ 안전한 파일 조작 원자적 쓰기 + 에러처리 🔍 메타데이터 활용 stat + 권한 + 시간정보 이제 나만의 파일 탐색기를 만들어보자! 🚀
🎯 마무리 — 이제 뭘 만들어볼까?

여기까지 따라왔으면 C언어로 파일 관리 도구를 만드는 핵심 기술을 다 익힌 거임! 👏

정리하자면:

📁 dirent.h 📊 sys/stat.h 📝 stdio.h 🔄 재귀 탐색 ⚡ 원자적 쓰기 🔐 권한 관리 🔍 파일 검색

이 기술들을 조합하면 백업 도구, 파일 동기화 프로그램, 디스크 분석기, 로그 관리 시스템 등 실용적인 도구를 만들 수 있음.

C언어 파일 시스템 프로그래밍은 처음엔 어렵게 느껴지지만, 한 번 익히면 운영체제가 어떻게 동작하는지 깊이 이해하게 되고 다른 언어로 파일 다룰 때도 훨씬 자신감이 생김 ㅋㅋㅋ

더 깊이 공부하고 싶다면 APUE(Advanced Programming in the UNIX Environment) 책을 강추함. C 시스템 프로그래밍의 바이블임!

그리고 이런 C언어 시스템 프로그래밍 관련 도움이 필요하다면 재능넷에서 관련 전문가를 찾아볼 수도 있어. 다양한 개발 재능을 가진 분들이 많이 계시거든 😊

💡 다음 단계 추천:
1. 위 코드들을 직접 컴파일해서 실행해보기
2. 자신만의 파일 관리 도구 기능 추가해보기
3. inotify(리눅스 파일 변경 감지 API) 공부하기
4. mmap()으로 대용량 파일 처리 최적화 공부하기
댓글 작성

이 글에 대한 여러분의 생각을 들려주세요

댓글 0
댓글을 불러오는 중입니다.

아직 댓글이 없습니다.