파일 조작의 기본 접근 방식
C 프로그래밍 환경에서 파일을 읽고 쓰는 작업은 크게 시스템 콜을 사용하는 저수준 API 와 표준 라이브러리 함수를 제공하는 고수준 API 로 나뉩니다. 각 방식마다 사용되는 헤더 파일과 동작 원리가 다르며, 용도에 따라 선택하여 사용해야 합니다.
1. POSIX 시스템 호출 기반 입출력
open(), read(), write(), close() 는 운영체제와 직접 인터랙션하는 저수준 함수들입니다. 이 방식은 파일 기술을 사용하여 데이터를 처리하며, 특히 리눅스 환경에서 많이 활용됩니다.
개방 옵션 플래그 (Open Flags)
파일을 열 때 두 번째 인자로 전달되는 플래그는 다음과 같은 권한과 행위를 결정합니다.
- O_RDONLY / O_WRONLY / O_RDWR: 읽기 전용, 쓰기 전용, 그리고 읽기와 쓰기를 모두 허용하는 모드를 지정합니다.
- O_CREAT: 지정한 경로에 파일이 존재하지 않을 경우 새로 생성합니다. 이 플래그를 사용할 때는 파일 생성 권한을 명시해야 합니다.
- O_EXCL:
O_CREAT와 함께 사용 시 파일이 이미 존재하면 에러를 반환하며, 동시성 제어가 필요한 경우에 유용합니다. - O_TRUNC: 기존 파일을 열었을 때 내용을 초기화하여 길이를 0 으로 만듭니다. 중첩 기록을 방지하기 위해 중요합니다.
- O_APPEND: 모든 쓰기 작업이 자동으로 파일 맨 끝에 추가되도록 설정합니다. 여러 프로세스가 동시에 쓸 때 데이터 무결성을 보장하는 역할을 합니다.
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
ssize_t secure_write_data(const char* filepath, const void* buffer, size_t size) {
int fd = open(filepath, O_WRONLY | O_CREAT | O_APPEND, 0644);
if (fd == -1) {
perror("Error opening file");
return -1;
}
ssize_t written = write(fd, buffer, size);
if (written == -1) {
perror("Error writing data");
close(fd);
return -1;
}
sync(); // 디스크 즉시 동기화
close(fd);
return written;
}
ssize_t secure_read_data(char* buffer, size_t max_len, const char* filepath) {
int fd = open(filepath, O_RDONLY);
if (fd == -1) {
perror("Error opening file");
return -1;
}
ssize_t bytes_read = read(fd, buffer, max_len);
close(fd);
return bytes_read;
}
2. 표준 입출력 라이브러리 기반 (std io)
stdio.h 에 정의된 fopen(), fread(), fwrite() 등을 사용하면 더 높은 수준의 추상화를 통해 파일을 처리할 수 있습니다. 이 방식은 버퍼링을 내장하고 있어 대용량 데이터 처리 시 성능 최적화가 자동으로 이루어집니다.
파일 모드 문자열 (File Mode Strings)
fopen() 의 두 번째 인자는 텍스트 모드와 바이너리 모드로 구분되며, 주요 모드는 다음과 같습니다.
- "r" / "rb": 읽기 전용으로 엽니다. 파일이 없으면 실패합니다.
- "w" / "wb": 쓰기 전용으로 엽니다. 파일이 있으면 내용물을 비운 뒤 시작합니다.
- "a" / "ab": 추가 모드로 엽니다. 쓰기 위치는 항상 파일 끝부분으로 이동합니다.
- "+" 접미사: 예를 들어 "r+", "w+" 등의 형태로 읽기와 쓰기를 모두 지원합니다.
#include <stdio.h>
#include <stdlib.h>
#define BUFFER_SIZE 4096
int append_stream(const char* filename, const unsigned char* data, size_t count) {
FILE* stream = fopen(filename, "ab"); // 바이너리 추가 모드
if (!stream) {
perror("Failed to open stream");
return -1;
}
size_t result = fwrite(data, sizeof(unsigned char), count, stream);
// 변경 사항이 있을 경우 버퍼 즉시 강제로 출력
fflush(stream);
fclose(stream);
return (result == count) ? 0 : -1;
}
size_t load_from_file(unsigned char* dest, size_t capacity, const char* filename) {
FILE* stream = fopen(filename, "rb");
if (!stream) {
perror("Failed to load stream");
return 0;
}
size_t loaded = fread(dest, 1, capacity, stream);
fclose(stream);
return loaded;
}
핵심 주의사항 및 행동 패턴
두 방법을 혼용하거나 잘못 사용할 경우 예상치 못한 데이터 손실이 발생할 수 있습니다.
- 버퍼 관리 및 플러시: 표준 입출력을 사용할 경우
fclose()를 호출해야 할 뿐만 아니라, 프로그램 종료 전 중요한 데이터라면fflush()를 명시적으로 호출하여 메모리 버퍼를 디스크로 밀어넣어야 합니다. - 트렁케이션 (Truncation) 이해:
O_TRUNC나"w"모드를 사용하지 않고도 기존 파일 위에 덮어쓸 경우, 원본 파일의 남은 데이터가 남을 수 있습니다. 반드시 새로운 파일 생성 시 명확히 길이 초기화 옵션을 사용해야 합니다. - 추가 모드 (Append) 의 특이점:
O_APPEND플래그를 시스템 호출에서 적용할 때, 내부적으로 파일 포인터 갱신 로직이 자동으로 실행되어 데이터 충돌을 막아줍니다. 하지만 이를O_TRUNC와 같이 사용할 경우 의미가 상쇄될 수 있으므로 주의를 기울여야 합니다. - 호환성 문제: 일부 레거시 시스템에서는
open()에O_CREAT를 사용할 경우 권한 인자가 생략되면 에러를 반환하기도 합니다. 최신 코드는 세 개의 인자를 갖춘 형태를 표준으로 사용합니다.