C언어 파일 입출력 함수: fopen, fclose, fwrite, fread 완벽 가이드

C언어에서 파일 입출력은 프로그램이 외부 데이터와 상호작용할 수 있도록 해주는 필수 기능입니다. 데이터 저장 및 검색 작업을 수행하려면 파일을 열고 데이터를 읽거나 쓰는 작업이 필요합니다. 이를 위해 C언어는 강력한 파일 입출력 함수 세트를 제공합니다. 본 기사에서는 파일 입출력의 기초 개념부터 fopen, fclose, fwrite, fread와 같은 핵심 함수의 실제 사용법까지 단계적으로 설명합니다. 이를 통해 파일 기반 데이터 처리에 필요한 모든 지식을 얻을 수 있을 것입니다.

파일 입출력의 기본 개념


파일 입출력은 프로그램이 파일 시스템과 상호작용하여 데이터를 저장하거나 읽어들이는 작업을 말합니다. 이를 통해 프로그램은 실행 중 데이터를 유지하거나, 다른 프로그램과 데이터를 주고받을 수 있습니다.

파일 입출력의 필요성

  • 데이터 영속성: 프로그램이 종료되어도 데이터가 유지됩니다.
  • 대량 데이터 처리: 메모리에 저장할 수 없는 큰 데이터를 다룰 수 있습니다.
  • 유연한 데이터 교환: 프로그램 간 데이터를 공유하거나 기록을 남길 수 있습니다.

파일 입출력의 주요 과정

  1. 파일 열기: 파일을 읽거나 쓰기 위해 fopen 함수로 파일을 엽니다.
  2. 데이터 읽기/쓰기: fread, fwrite, 또는 fprintf 등의 함수로 파일 데이터를 처리합니다.
  3. 파일 닫기: fclose를 호출해 열려 있는 파일을 닫고 리소스를 반환합니다.

파일 모드


파일 작업을 수행할 때, 모드를 지정하여 파일의 열기 목적을 설정합니다. 주요 모드는 다음과 같습니다.

  • "r": 읽기 전용으로 파일을 엽니다.
  • "w": 쓰기 전용으로 파일을 열며, 기존 내용을 제거합니다.
  • "a": 추가 전용으로 파일을 열며, 기존 내용은 유지됩니다.
  • "rb", "wb", "ab": 바이너리 파일 작업을 위한 모드입니다.

파일 입출력의 기본 원리를 이해하면, 이후 각 함수의 상세한 활용법을 쉽게 익힐 수 있습니다.

fopen과 fclose 함수 사용법

파일 입출력 작업은 fopen 함수로 파일을 열고, 작업이 끝난 후 fclose 함수로 파일을 닫는 것으로 시작됩니다.

fopen 함수


fopen 함수는 파일을 열고 작업할 수 있는 스트림을 반환합니다. 이 스트림은 파일 작업을 수행하는 동안 필요한 정보를 포함합니다.

구문:

FILE *fopen(const char *filename, const char *mode);
  • filename: 열고자 하는 파일의 이름(경로 포함).
  • mode: 파일을 열기 위한 모드(예: “r”, “w”, “a”, “rb”, “wb”, “ab”).
  • 반환값: 성공하면 파일 포인터를 반환하고, 실패하면 NULL을 반환합니다.

예제:

FILE *file = fopen("example.txt", "r");
if (file == NULL) {
    perror("File opening failed");
    return 1;
}

fclose 함수


fclose 함수는 열려 있는 파일을 닫고 관련된 리소스를 해제합니다.

구문:

int fclose(FILE *stream);
  • stream: fopen으로 반환된 파일 포인터.
  • 반환값: 성공하면 0을 반환하고, 실패하면 EOF를 반환합니다.

예제:

if (fclose(file) == EOF) {
    perror("File closing failed");
    return 1;
}

주요 예외 처리

  • 파일이 존재하지 않거나 권한이 없으면 fopenNULL을 반환합니다. 이를 perror 또는 strerror를 사용해 디버깅할 수 있습니다.
  • 파일을 닫지 않으면 리소스 누수가 발생할 수 있으므로 항상 작업 후 fclose를 호출해야 합니다.

파일 열기와 닫기의 중요성


파일을 제대로 열고 닫지 않으면 프로그램의 안정성이 떨어질 수 있습니다. 특히, 리소스 누수는 메모리 부족이나 파일 시스템 오류로 이어질 수 있으므로, 반드시 파일 작업 후 정리(clean-up)를 수행해야 합니다.

fwrite 함수의 활용법

fwrite 함수는 데이터를 이진 형태로 파일에 쓰는 데 사용됩니다. 이 함수는 구조체와 같은 복잡한 데이터를 저장하거나 바이너리 파일 작업을 수행할 때 유용합니다.

fwrite 함수의 기본 구조


구문:

size_t fwrite(const void *ptr, size_t size, size_t count, FILE *stream);
  • ptr: 파일에 쓸 데이터를 가리키는 포인터입니다.
  • size: 쓰려는 데이터의 단위 크기(바이트)입니다.
  • count: 데이터를 몇 개의 단위로 쓸 것인지 지정합니다.
  • stream: fopen으로 생성된 파일 포인터입니다.
  • 반환값: 성공적으로 쓰여진 단위 수를 반환합니다. 실패 시 반환값은 적거나 0입니다.

예제: 정수 배열 쓰기

#include <stdio.h>

int main() {
    FILE *file = fopen("data.bin", "wb");
    if (file == NULL) {
        perror("File opening failed");
        return 1;
    }

    int data[] = {10, 20, 30, 40, 50};
    size_t written = fwrite(data, sizeof(int), 5, file);

    if (written != 5) {
        perror("File write failed");
    }

    fclose(file);
    return 0;
}


위 코드는 정수 배열을 바이너리 파일에 저장합니다. 각 정수의 크기(4바이트)가 계산되어 5개의 요소를 파일에 작성합니다.

사용 시 주의 사항

  • fwrite는 데이터가 연속된 메모리에 저장되어 있어야 제대로 작동합니다. 예를 들어 구조체 배열이나 배열과 같은 데이터에 적합합니다.
  • 반환값은 실제로 쓰여진 단위 수를 나타내므로, 예상한 단위 수와 비교하여 오류를 확인해야 합니다.

효율적인 fwrite 사용 팁

  • 데이터 블록을 한 번에 처리하여 호출 횟수를 줄임으로써 성능을 개선할 수 있습니다.
  • 파일이 열릴 때 "wb" 모드(바이너리 모드)를 사용하는 것이 중요합니다. 텍스트 모드에서는 데이터가 제대로 저장되지 않을 수 있습니다.

fwrite와 텍스트 파일


fwrite는 이진 데이터 처리를 목적으로 설계되었습니다. 텍스트 데이터를 저장하려면 fprintf 같은 다른 함수를 사용하는 것이 더 적합합니다.

fwrite는 이진 데이터 처리의 핵심 도구로, 파일 입출력 성능을 높이고 복잡한 데이터 구조를 효과적으로 다룰 수 있게 합니다.

fread 함수로 파일 읽기

fread 함수는 이진 데이터를 파일에서 읽어오는 데 사용됩니다. 이는 바이너리 파일에서 구조체나 배열 데이터를 읽는 데 매우 적합합니다.

fread 함수의 기본 구조


구문:

size_t fread(void *ptr, size_t size, size_t count, FILE *stream);
  • ptr: 데이터를 저장할 메모리 위치를 가리키는 포인터입니다.
  • size: 읽어올 데이터의 단위 크기(바이트)입니다.
  • count: 읽어올 데이터의 단위 개수입니다.
  • stream: fopen으로 생성된 파일 포인터입니다.
  • 반환값: 성공적으로 읽어온 단위 수를 반환합니다. 실패 시 반환값은 적거나 0입니다.

예제: 정수 배열 읽기

#include <stdio.h>

int main() {
    FILE *file = fopen("data.bin", "rb");
    if (file == NULL) {
        perror("File opening failed");
        return 1;
    }

    int data[5];
    size_t read = fread(data, sizeof(int), 5, file);

    if (read != 5) {
        perror("File read failed");
    } else {
        for (int i = 0; i < 5; i++) {
            printf("data[%d] = %d\n", i, data[i]);
        }
    }

    fclose(file);
    return 0;
}


이 코드는 data.bin 파일에서 정수 배열을 읽어와 화면에 출력합니다.

사용 시 주의 사항

  • 읽어올 데이터 크기(size)와 개수(count)가 파일 크기보다 크면 읽기 실패가 발생할 수 있습니다.
  • fread의 반환값은 실제로 읽어온 단위 수를 나타내므로, 반환값을 통해 읽기 오류를 확인해야 합니다.

fread의 성능 향상 팁

  • 대량 데이터를 읽을 때는 한 번에 큰 블록으로 읽어 호출 횟수를 줄이는 것이 좋습니다.
  • 읽기 작업 전에 파일 포인터의 위치를 fseek를 사용해 적절히 설정할 수 있습니다.

오류 처리

  • 읽기 중 오류가 발생하면 ferror 함수를 사용해 오류 원인을 확인할 수 있습니다.
  • 파일의 끝에 도달한 경우 feof 함수를 사용해 이를 확인합니다.

예:

if (feof(file)) {
    printf("End of file reached.\n");
}
if (ferror(file)) {
    printf("Error reading the file.\n");
}

fread와 텍스트 파일


fread는 이진 데이터를 읽는 데 적합합니다. 텍스트 파일을 읽으려면 fgetsfscanf 같은 함수를 사용하는 것이 더 적합합니다.

fread는 이진 데이터 처리에 최적화된 함수로, 대용량 데이터의 효율적인 파일 입출력을 가능하게 합니다. 데이터를 읽은 후 항상 반환값과 파일 상태를 확인해 안정적인 동작을 보장해야 합니다.

텍스트 파일과 바이너리 파일의 차이

텍스트 파일과 바이너리 파일은 저장되는 데이터 형식과 처리 방법에서 중요한 차이를 가집니다. 두 파일 유형을 이해하면 올바른 입출력 방식을 선택할 수 있습니다.

텍스트 파일


텍스트 파일은 사람이 읽을 수 있는 형식으로 데이터가 저장됩니다. 주로 ASCII 또는 UTF-8 같은 문자 인코딩을 사용합니다.

특징:

  • 데이터는 줄바꿈 문자(\n)와 공백 같은 텍스트 기반 구문으로 구성됩니다.
  • 파일 내용은 텍스트 편집기를 사용해 직접 열어 확인하거나 수정할 수 있습니다.
  • 크로스 플랫폼 처리 시 줄바꿈 형식(CRLF vs. LF) 차이로 문제가 발생할 수 있습니다.

예제:
텍스트 파일(example.txt)에 저장된 내용:

10
20
30

바이너리 파일


바이너리 파일은 데이터가 이진 형식으로 저장됩니다. 이 데이터는 숫자, 구조체, 배열 등과 같이 연속된 메모리 데이터를 그대로 파일에 기록합니다.

특징:

  • 데이터는 사람이 읽을 수 없는 이진 형식으로 저장되며, 파일 크기가 더 작을 수 있습니다.
  • 플랫폼 간 데이터 크기나 엔디언 차이로 문제가 발생할 수 있습니다.
  • 이진 데이터를 저장하므로 처리 속도가 빠르고 저장 공간이 효율적입니다.

예제:
바이너리 파일(data.bin)에 저장된 데이터:

  • 정수 배열 [10, 20, 30]이 메모리 데이터 그대로 저장됩니다.

텍스트 파일과 바이너리 파일 비교

특징텍스트 파일바이너리 파일
형식사람이 읽을 수 있는 텍스트이진 데이터
저장 방식문자 인코딩 사용메모리 데이터 그대로 저장
읽기/쓰기 함수fgets, fprintffread, fwrite
크기더 큼더 작음
휴대성높음낮음 (엔디언/크기 차이 발생)

입출력 방식 선택 가이드

  • 텍스트 파일을 선택할 때
  • 데이터가 사람이 읽고 수정 가능해야 하는 경우.
  • 설정 파일이나 로그 파일을 작성할 때.
  • 바이너리 파일을 선택할 때
  • 성능이 중요하거나 대용량 데이터를 처리할 때.
  • 구조체, 배열 등의 복잡한 데이터를 저장해야 할 때.

예제: 텍스트와 바이너리 파일 처리 비교


텍스트 파일 쓰기:

FILE *file = fopen("example.txt", "w");
fprintf(file, "10\n20\n30\n");
fclose(file);

바이너리 파일 쓰기:

FILE *file = fopen("data.bin", "wb");
int data[] = {10, 20, 30};
fwrite(data, sizeof(int), 3, file);
fclose(file);

텍스트와 바이너리 파일의 차이를 이해하고 적절한 입출력 방식을 사용하면 효율적이고 안정적인 파일 작업을 수행할 수 있습니다.

파일 입출력에서 에러 핸들링

파일 입출력 작업 중 오류는 다양한 이유로 발생할 수 있습니다. 예를 들어, 파일이 존재하지 않거나, 읽기/쓰기 권한이 없거나, 디스크 용량이 부족한 경우입니다. 이러한 상황에서 적절한 에러 핸들링은 프로그램의 안정성을 높이는 데 필수적입니다.

주요 에러 상황과 원인

  1. 파일 열기 오류 (fopen 실패)
  • 파일이 존재하지 않음.
  • 파일 경로가 잘못되었음.
  • 읽기/쓰기 권한이 없음.
  1. 파일 읽기/쓰기 오류 (fread/fwrite 실패)
  • 읽거나 쓸 데이터 크기가 예상보다 작음.
  • 디스크 용량 부족.
  • 파일 포인터가 잘못된 위치를 가리킴.
  1. 파일 닫기 오류 (fclose 실패)
  • 스트림이 이미 닫혔거나 유효하지 않음.

에러 핸들링을 위한 함수

  • perror: 최근 발생한 파일 오류를 출력합니다.
  • ferror: 스트림에서 발생한 에러를 확인합니다.
  • feof: 스트림이 파일 끝에 도달했는지 확인합니다.

예제:

FILE *file = fopen("example.txt", "r");
if (file == NULL) {
    perror("Error opening file");
    return 1;
}

// 파일 읽기
char buffer[100];
if (fread(buffer, sizeof(char), 100, file) < 100) {
    if (feof(file)) {
        printf("End of file reached.\n");
    } else if (ferror(file)) {
        perror("Error reading file");
    }
}

if (fclose(file) == EOF) {
    perror("Error closing file");
}

에러 핸들링 전략

  1. 파일 작업 전 조건 확인
  • 파일이 존재하는지 확인 (access 함수 활용 가능).
  • 읽기/쓰기 권한 확인.
  1. 반환값 확인
  • 모든 파일 입출력 함수의 반환값을 점검하여 실패 여부를 감지.
  1. 리소스 정리
  • 오류 발생 시 열려 있는 파일을 닫고, 메모리를 해제하여 리소스 누수 방지.

입출력 에러를 방지하기 위한 팁

  • 파일을 열 때 항상 반환값을 확인하고 오류 메시지를 출력합니다.
  • 파일 포인터를 적절히 관리하여 유효하지 않은 참조를 방지합니다.
  • 대용량 데이터를 처리할 때 디스크 용량과 파일 시스템 제한을 고려합니다.

종합 예제: 안전한 파일 읽기

#include <stdio.h>

int main() {
    FILE *file = fopen("example.txt", "r");
    if (file == NULL) {
        perror("Error opening file");
        return 1;
    }

    char buffer[256];
    while (fgets(buffer, sizeof(buffer), file)) {
        printf("%s", buffer);
    }

    if (ferror(file)) {
        perror("Error reading file");
    }

    if (fclose(file) == EOF) {
        perror("Error closing file");
    }

    return 0;
}

파일 작업 중 오류를 감지하고 적절히 처리하면 프로그램이 예상치 못한 중단 없이 안정적으로 동작할 수 있습니다. 에러 핸들링을 철저히 구현해 파일 입출력을 안전하게 수행하세요.

고급 활용: 스트림과 버퍼링

C언어에서 스트림과 버퍼링은 파일 입출력 성능을 최적화하는 핵심 요소입니다. 스트림은 파일과 메모리 간 데이터를 교환하는 추상적인 인터페이스이며, 버퍼링은 데이터를 효율적으로 읽고 쓰기 위해 중간 메모리 공간을 사용하는 방식입니다.

스트림의 역할


스트림은 파일 입출력 작업을 간소화하고, 다양한 입출력 장치와 프로그램 간의 데이터를 효율적으로 처리할 수 있도록 설계된 계층입니다.

C언어 스트림 종류:

  • 표준 입력 스트림 (stdin): 기본 입력(키보드).
  • 표준 출력 스트림 (stdout): 기본 출력(화면).
  • 표준 에러 스트림 (stderr): 오류 메시지 출력.

사용 예제:

fprintf(stdout, "Hello, World!\n"); // 화면에 출력
fscanf(stdin, "%d", &num);          // 사용자 입력 받기

버퍼링의 개념


버퍼링은 데이터를 읽고 쓰는 효율성을 높이기 위해 사용됩니다. 작은 데이터 단위로 파일에 접근하는 대신, 데이터 블록을 메모리에 저장한 뒤 일괄적으로 처리합니다.

버퍼링의 이점:

  • 디스크 접근 횟수 감소.
  • 입출력 성능 향상.
  • CPU와 디스크 간의 작업 속도 차이 보완.

버퍼링 유형

  1. 전역 버퍼링: 표준 스트림(stdin, stdout, stderr)은 기본적으로 전역 버퍼링을 사용합니다.
  2. 라인 버퍼링: stdout은 기본적으로 줄 단위로 버퍼링됩니다.
  3. 비버퍼링: stderr는 실시간 출력을 위해 비버퍼링됩니다.

버퍼 크기 설정:
setvbuf 함수를 사용해 버퍼 크기를 변경하거나 버퍼링 방식을 조정할 수 있습니다.

예제:

FILE *file = fopen("example.txt", "w");
if (file == NULL) {
    perror("File opening failed");
    return 1;
}

// 버퍼링 설정
char buffer[1024];
if (setvbuf(file, buffer, _IOFBF, sizeof(buffer)) != 0) {
    perror("Setting buffer failed");
}

// 데이터 쓰기
fprintf(file, "This is a test.\n");
fclose(file);

버퍼 플러시


파일을 닫기 전에 버퍼에 남아 있는 데이터를 강제로 출력해야 할 경우 fflush를 사용합니다.

예제:

fprintf(stdout, "Buffered data");
fflush(stdout); // 즉시 출력

스트림과 버퍼링을 활용한 성능 최적화

  1. 대량 데이터 처리
  • 한 번에 데이터를 블록 단위로 처리하여 디스크 접근 횟수를 최소화합니다.
  1. 적절한 버퍼 크기 선택
  • 디스크와 메모리 특성을 고려해 최적의 버퍼 크기를 설정합니다.
  1. 자동 버퍼링 관리
  • 대부분의 경우 C 표준 라이브러리의 기본 버퍼링 방식을 사용하면 적절한 성능을 얻을 수 있습니다.

종합 예제: 버퍼링 활용

#include <stdio.h>

int main() {
    FILE *file = fopen("largefile.txt", "r");
    if (file == NULL) {
        perror("File opening failed");
        return 1;
    }

    char buffer[4096];
    setvbuf(file, buffer, _IOFBF, sizeof(buffer)); // 버퍼 설정

    char line[256];
    while (fgets(line, sizeof(line), file)) {
        printf("%s", line);
    }

    fclose(file);
    return 0;
}

스트림과 버퍼링을 적절히 활용하면 파일 입출력 성능을 대폭 개선할 수 있습니다. 특히 대용량 데이터 처리 시 스트림 버퍼링은 필수적인 도구입니다. 파일 작업의 효율성을 높이기 위해 이러한 개념을 활용해 보세요.

실습 예제: 파일 복사 프로그램

이 섹션에서는 앞서 배운 파일 입출력 함수(fopen, fclose, fread, fwrite)를 활용하여 파일 복사 프로그램을 구현합니다. 이 예제는 C언어에서 파일 입출력의 실질적인 활용을 보여줍니다.

목표

  • 소스 파일을 읽어 동일한 내용을 대상 파일에 복사.
  • 파일이 없거나 읽기/쓰기 오류가 발생했을 때 에러를 처리.
  • 효율적인 버퍼링을 통해 성능 최적화.

프로그램 코드

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

int main(int argc, char *argv[]) {
    // 인자 확인
    if (argc != 3) {
        fprintf(stderr, "Usage: %s <source_file> <destination_file>\n", argv[0]);
        return 1;
    }

    // 파일 열기
    FILE *source = fopen(argv[1], "rb");
    if (source == NULL) {
        perror("Error opening source file");
        return 1;
    }

    FILE *destination = fopen(argv[2], "wb");
    if (destination == NULL) {
        perror("Error opening destination file");
        fclose(source);
        return 1;
    }

    // 데이터 복사
    char buffer[4096]; // 4KB 버퍼
    size_t bytesRead;
    while ((bytesRead = fread(buffer, 1, sizeof(buffer), source)) > 0) {
        if (fwrite(buffer, 1, bytesRead, destination) != bytesRead) {
            perror("Error writing to destination file");
            fclose(source);
            fclose(destination);
            return 1;
        }
    }

    // 에러 확인
    if (ferror(source)) {
        perror("Error reading source file");
    }

    // 파일 닫기
    fclose(source);
    fclose(destination);

    printf("File copied successfully from %s to %s\n", argv[1], argv[2]);
    return 0;
}

코드 설명

  1. 명령줄 인자 확인
  • 프로그램은 두 개의 파일 경로(소스와 대상)를 인자로 받습니다.
  • 잘못된 인자 개수가 입력되면 오류 메시지를 출력합니다.
  1. 파일 열기
  • 소스 파일은 fopen"rb" 모드로 열어 읽기 전용으로 설정합니다.
  • 대상 파일은 "wb" 모드로 열어 쓰기 전용으로 설정하며, 기존 내용을 덮어씁니다.
  1. 버퍼 기반 데이터 복사
  • fread 함수로 소스 파일에서 데이터를 읽고, fwrite 함수로 대상 파일에 씁니다.
  • 4KB 크기의 버퍼를 사용해 디스크 접근 횟수를 줄입니다.
  1. 오류 처리
  • 파일 읽기와 쓰기 작업의 반환값을 확인하여 오류를 감지합니다.
  • 소스 파일 읽기 오류는 ferror로 확인합니다.
  1. 리소스 정리
  • 파일 작업이 끝나면 항상 fclose를 호출하여 파일을 닫습니다.

실행 예제


입력:

./file_copy source.txt destination.txt

출력:

File copied successfully from source.txt to destination.txt

개선 사항

  • 대상 파일 중복 확인: 대상 파일이 이미 존재하는 경우 경고 메시지를 출력하고 사용자 확인 후 덮어씁니다.
  • 진행 상태 표시: 파일 크기와 진행률을 출력하여 사용자 피드백을 제공합니다.
  • 대용량 파일 지원: 플랫폼에 따라 큰 파일을 처리할 수 있도록 파일 크기와 버퍼를 조정합니다.

이 예제는 파일 입출력 함수와 에러 핸들링을 실제로 적용해 보는 훌륭한 실습입니다. 이를 기반으로 더 복잡한 파일 처리 프로그램을 확장할 수 있습니다.

요약

본 기사에서는 C언어 파일 입출력의 기초와 주요 함수(fopen, fclose, fwrite, fread)의 사용법을 다뤘습니다. 텍스트 파일과 바이너리 파일의 차이를 이해하고, 에러 핸들링과 스트림 버퍼링을 통해 입출력 성능을 최적화하는 방법을 배웠습니다. 또한, 실습 예제인 파일 복사 프로그램을 통해 이론을 실질적으로 적용하는 방법을 살펴보았습니다. 이를 통해 파일 작업의 기본 원리와 활용법을 익혀 효율적이고 안정적인 파일 입출력을 구현할 수 있습니다.