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

C언어 파일 시스템과 디렉터리 조작을 통한
실전 파일 관리 도구 완전 정복
파일 탐색기를 직접 만들어보는 C언어 실전 여정 🗂️✨
솔직히 말하면 요즘 Python이나 Node.js로도 파일 관리 도구 뚝딱 만들 수 있는데, 굳이 C언어로?? 라고 생각할 수 있음 ㅋㅋㅋ
근데 진짜 시스템 레벨에서 파일이 어떻게 돌아가는지 이해하려면 C언어가 답임. 운영체제 커널이랑 가장 가까운 언어가 C거든.
파일 탐색기, 백업 도구, 로그 정리 스크립트, 디렉터리 동기화 프로그램... 이런 거 다 C로 만들 수 있고, 실제로 리눅스 유틸리티들(ls, cp, mv, rm 등)이 전부 C로 짜여 있음.
이 글에서는 C언어의 파일 시스템 API를 활용해서 실전에서 쓸 수 있는 파일 관리 도구를 만드는 방법을 처음부터 끝까지 파헤쳐볼 거야. 🔥
C언어로 파일 관리 도구를 만들기 전에, 운영체제가 파일을 어떻게 관리하는지 알아야 함. 이거 모르면 나중에 버그 만났을 때 멘붕 옴 ㅋㅋ
파일 시스템은 크게 두 가지 관점으로 볼 수 있어:
운영체제에서 파일은 단순히 바이트의 연속된 시퀀스야. 텍스트 파일이든, 이미지 파일이든, 실행 파일이든 OS 입장에서는 그냥 바이트 덩어리임.
파일에는 두 가지 핵심 요소가 있어:
② 메타데이터(Metadata) — 파일 이름, 크기, 생성일, 수정일, 권한, 소유자 등
리눅스/유닉스 계열에서는 이 메타데이터를 inode(아이노드)라는 자료구조에 저장함. inode에는 파일 이름 빼고 거의 모든 메타데이터가 들어있어. (파일 이름은 디렉터리 엔트리에 저장됨)
디렉터리도 사실 파일임! 특수한 파일이지. 디렉터리 파일 안에는 파일 이름 → inode 번호의 매핑 테이블이 들어있어.
그래서 디렉터리를 열면 그 안에 있는 파일/서브디렉터리 목록을 얻을 수 있는 거임.
| 종류 | 설명 | 예시 |
|---|---|---|
| 절대 경로 | 루트(/)부터 시작하는 전체 경로 | /home/user/docs/file.txt |
| 상대 경로 | 현재 작업 디렉터리 기준 경로 | ./docs/file.txt |
C언어에서 파일 경로를 다룰 때 이 두 가지를 명확히 구분해야 함. 특히 재귀적으로 디렉터리를 탐색할 때 경로 조합을 잘못하면 버그 터짐 ㅠㅠ
C언어에서 파일을 다루는 API는 크게 두 계층으로 나뉨:
② POSIX/시스템 콜 (unistd.h, fcntl.h) — open, close, read, write, stat 등 → 저수준, 리눅스/유닉스 전용
가장 기본적인 파일 열기/읽기/쓰기/닫기 패턴:
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;
}
| 모드 | 의미 | 파일 없을 때 | 기존 내용 |
|---|---|---|---|
"r" | 읽기 전용 | 에러 반환 | 유지 |
"w" | 쓰기 전용 | 새로 생성 | 삭제됨 ⚠️ |
"a" | 추가 쓰기 | 새로 생성 | 유지, 끝에 추가 |
"r+" | 읽기+쓰기 | 에러 반환 | 유지 |
"w+" | 읽기+쓰기 | 새로 생성 | 삭제됨 ⚠️ |
"a+" | 읽기+추가 | 새로 생성 | 유지, 끝에 추가 |
"rb" | 바이너리 읽기 | 에러 반환 | 유지 |
"wb" | 바이너리 쓰기 | 새로 생성 | 삭제됨 ⚠️ |
바이너리 파일(이미지, 실행파일 등) 복사할 때는 반드시 "rb", "wb" 모드 써야 함. 텍스트 모드로 바이너리 읽으면 Windows에서 줄바꿈 변환 때문에 파일 깨짐 😱
// 파일 복사 함수 (바이너리 방식)
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; // 성공
}
파일 관리 도구의 핵심은 디렉터리 탐색임. C언어에서 디렉터리를 다루려면 dirent.h 헤더가 필요해. 이게 POSIX 표준이라 리눅스/macOS에서 잘 돌아감.
#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);
}
| 필드 | 타입 | 설명 |
|---|---|---|
d_name | char[] | 파일/디렉터리 이름 |
d_type | unsigned char | 파일 타입 (DT_REG, DT_DIR 등) |
d_ino | ino_t | inode 번호 |
| 상수 | 의미 |
|---|---|
DT_REG | 일반 파일 |
DT_DIR | 디렉터리 |
DT_LNK | 심볼릭 링크 |
DT_FIFO | 파이프 |
DT_SOCK | 소켓 |
DT_CHR | 문자 장치 |
DT_BLK | 블록 장치 |
DT_UNKNOWN | 알 수 없음 |
d_type이 DT_UNKNOWN을 반환하는 파일시스템도 있음 (일부 NFS, FAT32 등). 이럴 때는 stat()으로 타입을 확인해야 함!
파일 관리 도구에서 파일 크기, 수정 시간, 권한 등을 알려면 stat() 함수가 필수임. 이거 진짜 자주 씀 ㅋㅋ
#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");
}
| 필드 | 타입 | 설명 |
|---|---|---|
st_size | off_t | 파일 크기 (바이트) |
st_mode | mode_t | 파일 타입 + 권한 비트 |
st_mtime | time_t | 마지막 수정 시간 |
st_atime | time_t | 마지막 접근 시간 |
st_ctime | time_t | 마지막 상태 변경 시간 |
st_nlink | nlink_t | 하드 링크 수 |
st_uid | uid_t | 소유자 UID |
st_gid | gid_t | 그룹 GID |
st_ino | ino_t | inode 번호 |
st_mode에서 권한 비트를 읽는 방법:
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 명령어가 어떻게 동작하는지 이해됨 ㅋㅋ
파일 관리 도구의 꽃은 재귀 탐색임! 디렉터리 안의 디렉터리를 계속 파고들어가는 거. 이거 구현하면 진짜 뿌듯함 ㅋㅋㅋ
#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()으로 처리하면 무한 루프 빠질 수 있음 😱
// 디렉터리 전체 크기 재귀 계산 (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));
}
#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);
}
#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); // 빈 디렉터리 삭제
}
리눅스의 find 명령어처럼 특정 조건에 맞는 파일을 찾는 기능을 구현해보자. 이거 만들면 진짜 뿌듯함 ㅋㅋㅋ
#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;
}
#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);
}
단순히 내용만 복사하는 게 아니라 수정 시간, 권한까지 보존하는 고급 복사 함수:
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, ×);
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 명령어의 내부 동작 원리임!
지금까지 배운 내용을 합쳐서 실제로 쓸 수 있는 파일 관리 도구를 만들어보자. 재능넷 같은 플랫폼에서 파일 관련 재능을 거래할 때도 이런 도구 구현 능력이 큰 강점이 됨! 😎
#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");
}
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");
}
// 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 명령어처럼 ├──, └──, │ 문자로 예쁜 트리 구조를 출력함! 터미널에서 보면 진짜 멋있음 ㅋㅋㅋ
파일 관리 도구에서 에러 처리는 진짜 중요함. 잘못된 에러 처리 하나가 데이터 손실로 이어질 수 있거든. 이건 절대 대충 하면 안 됨!
#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;
}
// 임시 파일에 쓰고 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()으로 교체하면, 쓰기 도중 프로그램이 죽어도 원본 파일은 손상되지 않음!
지금까지 코드는 리눅스/macOS(POSIX) 기준임. Windows에서도 돌아가게 하려면 약간의 처리가 필요함.
#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() |
| 경로 구분자 | / | \ |
| 파일 숨김 | 이름이 .으로 시작 | 파일 속성 플래그 |
| 대소문자 구분 | 구분함 | 구분 안 함 |
대용량 파일이나 수만 개의 파일을 다룰 때 성능이 중요해짐. 몇 가지 최적화 팁을 알아보자.
• 너무 작은 버퍼 (예: 1 byte) → 시스템 콜 오버헤드 폭발 💥
• 적당한 버퍼 (4KB~64KB) → 대부분의 경우 최적
• 너무 큰 버퍼 (예: 1GB) → 메모리 낭비, 캐시 미스 증가
일반적으로 4096 ~ 65536 bytes가 최적. 파일시스템 블록 크기(보통 4KB)의 배수가 좋음.
// 파일 목록을 배열에 저장 후 정렬 (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() + 정렬을 한 번에 해주는 편리한 함수임. 정렬된 파일 목록이 필요할 때 매우 유용함!
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);
}
지금까지 배운 내용으로 실제로 어떤 도구를 만들 수 있는지 살펴보자. 재능넷에서도 이런 C언어 파일 시스템 프로그래밍 관련 재능이 꽤 거래되고 있음 😊
| 도구 종류 | 핵심 기능 | 사용 API |
|---|---|---|
| 🗂️ 파일 탐색기 | 디렉터리 탐색, 파일 목록 | opendir, readdir, stat |
| 💾 백업 도구 | 파일 복사, 변경 감지 | fread, fwrite, stat(mtime) |
| 🔍 파일 검색기 | 이름/크기/날짜 검색 | 재귀 탐색, stat |
| 🧹 디스크 정리 | 대용량/오래된 파일 찾기 | stat(size, mtime) |
| 📊 디스크 분석 | 용량 통계, 트리 시각화 | 재귀 stat, 집계 |
| 🔄 파일 동기화 | 두 디렉터리 비교/동기화 | stat, fread, fwrite |
| 📝 로그 관리 | 로그 파일 로테이션 | rename, fopen("a") |
| 🗜️ 아카이브 | 파일 묶기/풀기 | fread, fwrite, stat |
| 헤더 | 주요 기능 |
|---|---|
stdio.h | fopen, fclose, fread, fwrite, printf |
dirent.h | opendir, readdir, closedir, scandir |
sys/stat.h | stat, lstat, mkdir, chmod |
unistd.h | unlink, rmdir, getcwd, access |
errno.h | errno, 에러 코드 상수 |
string.h | strcmp, strcpy, strcat, strstr |
time.h | time, ctime, difftime |
utime.h | utime (파일 시간 변경) |
2️⃣ "."과 ".." 엔트리 처리 안 함 → 무한 재귀 루프
3️⃣ stat() vs lstat() 혼동 → 심볼릭 링크 무한 루프
4️⃣ 경로 버퍼 오버플로우 → snprintf 대신 sprintf 사용
5️⃣ 에러 반환값 무시 → 조용한 데이터 손실
- ✅ 모든 에러를 적절히 처리하고 사용자에게 명확한 메시지 제공
- ✅ 파괴적인 작업(삭제, 덮어쓰기) 전 확인 프롬프트
- ✅ 원자적 쓰기로 데이터 무결성 보장
- ✅ 심볼릭 링크 순환 참조 방지
- ✅ 경로 버퍼 크기 안전하게 관리
- ✅ 크로스 플랫폼 호환성 고려
- ✅ 대용량 파일/디렉터리에서도 안정적인 성능
여기까지 따라왔으면 C언어로 파일 관리 도구를 만드는 핵심 기술을 다 익힌 거임! 👏
정리하자면:
이 기술들을 조합하면 백업 도구, 파일 동기화 프로그램, 디스크 분석기, 로그 관리 시스템 등 실용적인 도구를 만들 수 있음.
C언어 파일 시스템 프로그래밍은 처음엔 어렵게 느껴지지만, 한 번 익히면 운영체제가 어떻게 동작하는지 깊이 이해하게 되고 다른 언어로 파일 다룰 때도 훨씬 자신감이 생김 ㅋㅋㅋ
더 깊이 공부하고 싶다면 APUE(Advanced Programming in the UNIX Environment) 책을 강추함. C 시스템 프로그래밍의 바이블임!
그리고 이런 C언어 시스템 프로그래밍 관련 도움이 필요하다면 재능넷에서 관련 전문가를 찾아볼 수도 있어. 다양한 개발 재능을 가진 분들이 많이 계시거든 😊
1. 위 코드들을 직접 컴파일해서 실행해보기
2. 자신만의 파일 관리 도구 기능 추가해보기
3. inotify(리눅스 파일 변경 감지 API) 공부하기
4. mmap()으로 대용량 파일 처리 최적화 공부하기
관련 키워드
댓글 작성
이 글에 대한 여러분의 생각을 들려주세요
로그인이 필요합니다
댓글을 작성하려면 먼저 로그인해주세요.
댓글 0
지식인의 숲 - 지적 재산권 보호 고지
지적 재산권 보호 고지
- 저작권 및 소유권: 본 컨텐츠는 재능넷의 독점 AI 기술로 생성되었으며, 대한민국 저작권법 및 국제 저작권 협약에 의해 보호됩니다.
- AI 생성 컨텐츠의 법적 지위: 본 AI 생성 컨텐츠는 재능넷의 지적 창작물로 인정되며, 관련 법규에 따라 저작권 보호를 받습니다.
- 사용 제한: 재능넷의 명시적 서면 동의 없이 본 컨텐츠를 복제, 수정, 배포, 또는 상업적으로 활용하는 행위는 엄격히 금지됩니다.
- 데이터 수집 금지: 본 컨텐츠에 대한 무단 스크래핑, 크롤링, 및 자동화된 데이터 수집은 법적 제재의 대상이 됩니다.
- AI 학습 제한: 재능넷의 AI 생성 컨텐츠를 타 AI 모델 학습에 무단 사용하는 행위는 금지되며, 이는 지적 재산권 침해로 간주됩니다.
아직 댓글이 없습니다.