C 언어에서 바이너리 파일 쓰기와 읽기 완벽 가이드

바이너리 파일 입출력은 대규모 데이터를 처리하거나 효율적으로 저장할 때 중요한 역할을 합니다. 텍스트 파일과 달리 바이너리 파일은 데이터를 원시 이진 형식으로 저장하므로 파일 크기를 줄이고, 데이터 처리 속도를 높일 수 있습니다. 본 기사에서는 C 언어를 활용하여 바이너리 파일을 생성하고 데이터를 읽고 쓰는 방법을 단계별로 설명합니다. 또한, 구조체를 파일로 저장하는 고급 예제와 실습 문제를 통해 실용적인 지식을 얻을 수 있도록 구성하였습니다.

바이너리 파일이란 무엇인가


바이너리 파일은 데이터를 이진 형식으로 저장하는 파일 형식으로, 사람이 읽을 수 있는 텍스트 형식 대신 컴퓨터가 처리하기 용이한 형태로 데이터를 저장합니다.

텍스트 파일과의 차이점


텍스트 파일은 데이터를 사람이 읽을 수 있는 문자로 저장하며, 각 문자를 ASCII 또는 UTF-8와 같은 인코딩 형식으로 표현합니다. 반면, 바이너리 파일은 데이터를 원시 이진 형식으로 저장하므로 추가적인 인코딩이나 변환 과정이 없습니다.

바이너리 파일의 장점

  • 효율성: 텍스트 파일보다 저장 공간이 적게 들며, 데이터 처리 속도가 빠릅니다.
  • 정확성: 데이터 변환 과정에서 발생할 수 있는 손실이나 오류가 줄어듭니다.

바이너리 파일의 활용 사례


바이너리 파일은 이미지, 비디오, 오디오 파일 및 데이터베이스 파일 등에서 주로 사용됩니다. C 언어에서는 파일 입출력을 통해 다양한 데이터 처리가 가능하며, 이를 통해 프로그램의 데이터 관리 효율을 높일 수 있습니다.

C 언어에서 파일 입출력 기본


C 언어에서 파일 입출력은 표준 라이브러리의 기능을 사용하여 이루어집니다. 이를 통해 텍스트 파일이나 바이너리 파일에 데이터를 저장하거나 읽을 수 있습니다.

파일 포인터와 fopen()


파일을 다루기 위해서는 FILE 구조체 포인터를 사용합니다. fopen() 함수는 파일을 열거나 새로 생성하며, 반환된 파일 포인터를 통해 파일 작업을 수행합니다.

FILE *file;
file = fopen("example.bin", "wb"); // 쓰기 모드로 바이너리 파일 열기
if (file == NULL) {
    perror("파일 열기 실패");
    return 1;
}

파일 모드


fopen() 함수는 파일 모드를 지정하여 파일의 동작을 제어합니다. 주요 모드는 다음과 같습니다:

  • "r": 읽기 모드
  • "w": 쓰기 모드 (파일이 없으면 생성, 있으면 덮어씀)
  • "a": 추가 모드 (파일 끝에 데이터 추가)
  • "rb", "wb", "ab": 각각 읽기, 쓰기, 추가의 바이너리 모드

파일 닫기: fclose()


파일 작업이 끝나면 반드시 fclose()를 호출하여 파일을 닫아야 합니다. 이는 시스템 리소스를 해제하고 데이터 손실을 방지합니다.

fclose(file);

기본적인 파일 작업

  • 쓰기: fprintf()fwrite()
  • 읽기: fscanf()fread()

C 언어의 파일 입출력 기본은 파일의 데이터를 효율적으로 다루기 위한 필수 지식이며, 이를 바탕으로 바이너리 파일 작업을 구현할 수 있습니다.

바이너리 파일 쓰기


C 언어에서 바이너리 파일에 데이터를 쓰기 위해 fwrite() 함수를 사용합니다. 이 함수는 데이터를 이진 형식으로 파일에 저장하며, 텍스트 형식보다 효율적으로 데이터를 기록할 수 있습니다.

fwrite() 함수의 기본 구조


fwrite() 함수는 다음과 같은 형식으로 사용됩니다:

size_t fwrite(const void *ptr, size_t size, size_t count, FILE *stream);
  • ptr: 파일에 기록할 데이터의 메모리 주소
  • size: 각 데이터 요소의 크기 (바이트 단위)
  • count: 기록할 데이터 요소의 개수
  • stream: 파일 포인터

예제: 정수 배열 쓰기


정수 배열을 바이너리 파일로 저장하는 예제를 살펴보겠습니다.

#include <stdio.h>

int main() {
    FILE *file;
    int data[] = {10, 20, 30, 40, 50};
    size_t count = sizeof(data) / sizeof(data[0]);

    // 바이너리 파일 쓰기 모드로 열기
    file = fopen("data.bin", "wb");
    if (file == NULL) {
        perror("파일 열기 실패");
        return 1;
    }

    // 배열 데이터를 파일에 쓰기
    size_t written = fwrite(data, sizeof(int), count, file);
    if (written != count) {
        perror("쓰기 실패");
    }

    fclose(file);
    return 0;
}

코드 설명

  1. fopen() 함수로 파일을 쓰기 모드("wb")로 열기.
  2. fwrite() 함수로 배열 데이터를 파일에 저장.
  3. 쓰기 성공 여부를 확인하고, 실패 시 에러 메시지 출력.
  4. fclose() 함수로 파일 닫기.

유의사항

  • 데이터 크기(size)와 데이터 개수(count)를 정확히 설정해야 올바른 기록이 가능합니다.
  • 파일 쓰기 중 오류가 발생하면 perror()를 사용해 원인을 진단해야 합니다.

이러한 방법으로 바이너리 파일에 데이터를 기록하면 텍스트 파일보다 효율적이고 정확하게 데이터를 저장할 수 있습니다.

바이너리 파일 읽기


C 언어에서 바이너리 파일을 읽기 위해 fread() 함수를 사용합니다. 이 함수는 파일에서 이진 데이터를 읽어 메모리로 로드하며, 효율적인 데이터 처리가 가능합니다.

fread() 함수의 기본 구조


fread() 함수는 다음과 같은 형식으로 사용됩니다:

size_t fread(void *ptr, size_t size, size_t count, FILE *stream);
  • ptr: 데이터를 저장할 메모리의 주소
  • size: 각 데이터 요소의 크기 (바이트 단위)
  • count: 읽을 데이터 요소의 개수
  • stream: 파일 포인터

예제: 정수 배열 읽기


바이너리 파일에서 정수 배열 데이터를 읽는 예제를 살펴보겠습니다.

#include <stdio.h>

int main() {
    FILE *file;
    int data[5];
    size_t count = sizeof(data) / sizeof(data[0]);

    // 바이너리 파일 읽기 모드로 열기
    file = fopen("data.bin", "rb");
    if (file == NULL) {
        perror("파일 열기 실패");
        return 1;
    }

    // 파일에서 배열 데이터를 읽기
    size_t read = fread(data, sizeof(int), count, file);
    if (read != count) {
        if (feof(file)) {
            printf("파일 끝에 도달했습니다.\n");
        } else if (ferror(file)) {
            perror("읽기 실패");
        }
    } else {
        printf("파일에서 데이터 읽기 성공\n");
        for (size_t i = 0; i < count; i++) {
            printf("data[%zu]: %d\n", i, data[i]);
        }
    }

    fclose(file);
    return 0;
}

코드 설명

  1. fopen() 함수로 바이너리 파일을 읽기 모드("rb")로 열기.
  2. fread() 함수로 데이터를 메모리 배열에 읽어오기.
  3. 읽기 결과를 확인하여 성공 여부를 판별:
  • feof(): 파일 끝에 도달한 경우
  • ferror(): 읽기 오류가 발생한 경우
  1. 데이터를 화면에 출력하여 읽기 결과를 확인.
  2. fclose() 함수로 파일 닫기.

유의사항

  • 읽어올 데이터의 크기와 개수를 정확히 지정해야 합니다.
  • 파일 끝이나 오류 여부를 반드시 확인해야 데이터 손실을 방지할 수 있습니다.
  • 읽은 데이터를 정확히 검증하여 예상된 결과와 일치하는지 확인해야 합니다.

fread()를 사용하면 바이너리 파일에서 데이터를 효율적으로 읽어와 프로그램에서 활용할 수 있습니다.

구조체와 바이너리 파일


C 언어에서 구조체는 여러 데이터 타입을 하나로 묶어 관리할 수 있는 강력한 도구입니다. 구조체를 바이너리 파일에 저장하거나 읽으면 복잡한 데이터 구조를 효율적으로 다룰 수 있습니다.

구조체 데이터를 파일에 쓰기


구조체 데이터를 바이너리 파일에 저장하려면 fwrite()를 사용합니다. 구조체 포인터와 크기를 지정하여 데이터를 기록할 수 있습니다.

#include <stdio.h>

typedef struct {
    int id;
    char name[20];
    float score;
} Student;

int main() {
    FILE *file;
    Student student = {1, "John Doe", 85.5};

    // 바이너리 파일 쓰기 모드로 열기
    file = fopen("student.bin", "wb");
    if (file == NULL) {
        perror("파일 열기 실패");
        return 1;
    }

    // 구조체 데이터를 파일에 쓰기
    size_t written = fwrite(&student, sizeof(Student), 1, file);
    if (written != 1) {
        perror("쓰기 실패");
    }

    fclose(file);
    return 0;
}

구조체 데이터를 파일에서 읽기


구조체 데이터를 바이너리 파일에서 읽으려면 fread()를 사용합니다. 읽어온 데이터를 구조체 변수에 저장할 수 있습니다.

#include <stdio.h>

typedef struct {
    int id;
    char name[20];
    float score;
} Student;

int main() {
    FILE *file;
    Student student;

    // 바이너리 파일 읽기 모드로 열기
    file = fopen("student.bin", "rb");
    if (file == NULL) {
        perror("파일 열기 실패");
        return 1;
    }

    // 파일에서 구조체 데이터 읽기
    size_t read = fread(&student, sizeof(Student), 1, file);
    if (read != 1) {
        perror("읽기 실패");
    } else {
        printf("ID: %d\n", student.id);
        printf("Name: %s\n", student.name);
        printf("Score: %.2f\n", student.score);
    }

    fclose(file);
    return 0;
}

코드 설명

  1. 구조체 정의: 데이터 요소들을 포함하는 Student 구조체 정의.
  2. 파일 열기: fopen()을 사용해 바이너리 파일 쓰기("wb") 또는 읽기("rb") 모드로 열기.
  3. 데이터 쓰기: fwrite()로 구조체 데이터를 바이너리 형식으로 파일에 기록.
  4. 데이터 읽기: fread()로 파일에서 구조체 데이터를 읽고 구조체 변수에 저장.
  5. 데이터 확인: 읽은 데이터를 출력하여 정확성을 검증.
  6. 파일 닫기: fclose()로 파일을 닫아 리소스 해제.

유의사항

  • 구조체에 포함된 데이터가 올바르게 정렬되어 있어야 합니다(구조체 패딩).
  • 파일에서 읽고 쓰는 데이터 크기를 항상 확인하여 데이터 손실을 방지해야 합니다.
  • 파일 입출력 작업 후 반드시 파일을 닫아 리소스를 반환해야 합니다.

구조체와 바이너리 파일을 사용하면 복잡한 데이터를 효율적으로 저장하고 관리할 수 있습니다.

파일 입출력 에러 처리


파일 작업 중 오류를 처리하는 것은 안정적이고 신뢰할 수 있는 프로그램을 작성하는 데 중요합니다. 파일 입출력에서 발생할 수 있는 일반적인 오류를 감지하고 적절히 처리하는 방법을 알아보겠습니다.

fopen() 실패 처리


fopen() 함수는 파일 열기에 실패하면 NULL을 반환합니다. 이는 파일 경로가 잘못되었거나 파일에 접근 권한이 없을 때 발생할 수 있습니다.

FILE *file = fopen("data.bin", "rb");
if (file == NULL) {
    perror("파일 열기 실패");
    return 1;
}
  • perror(): 시스템 오류 메시지를 출력합니다.
  • 해결 방법: 파일 경로를 확인하거나 적절한 권한을 부여합니다.

fwrite() 실패 처리


fwrite() 함수는 실제로 기록된 데이터 개수를 반환하며, 기록 실패 시 반환 값이 예상된 개수보다 작아집니다.

size_t written = fwrite(data, sizeof(int), count, file);
if (written != count) {
    perror("쓰기 실패");
}
  • 가능한 원인: 디스크 용량 부족, 파일 권한 문제.
  • 해결 방법: 디스크 상태를 점검하고 권한을 확인합니다.

fread() 실패 처리


fread() 함수는 실제로 읽은 데이터 개수를 반환하며, 읽기 실패 시 반환 값이 예상된 개수보다 작아집니다.

size_t read = fread(data, sizeof(int), count, file);
if (read != count) {
    if (feof(file)) {
        printf("파일 끝에 도달했습니다.\n");
    } else if (ferror(file)) {
        perror("읽기 실패");
    }
}
  • feof(): 파일 끝에 도달했는지 확인합니다.
  • ferror(): 파일 스트림에서 오류가 발생했는지 확인합니다.

파일 닫기 실패 처리


fclose() 함수는 파일 닫기에 실패하면 EOF를 반환합니다.

if (fclose(file) == EOF) {
    perror("파일 닫기 실패");
}
  • 가능한 원인: 파일 스트림이 손상되었거나 디스크 오류 발생.
  • 해결 방법: 파일 작업이 올바르게 완료되었는지 점검합니다.

일반적인 에러 처리 방법

  1. 오류 감지: 모든 파일 입출력 함수의 반환 값을 확인합니다.
  2. 로그 기록: 오류 메시지를 파일이나 콘솔에 기록하여 문제를 진단합니다.
  3. 복구 시도: 가능한 경우 파일 스트림을 재설정하거나 다른 경로를 시도합니다.
  4. 종료 처리: 심각한 오류 발생 시 프로그램을 종료하기 전에 리소스를 해제합니다.

유용한 함수

  • perror(): 에러 메시지를 출력.
  • strerror(errno): 오류 번호(errno)를 기반으로 오류 메시지 반환.
  • clearerr(): 파일 스트림의 오류 상태를 초기화.

파일 입출력 에러 처리는 프로그램의 안정성을 높이고 데이터 손상을 방지하기 위한 필수 과정입니다. 이를 통해 예상치 못한 상황에서도 프로그램이 올바르게 작동하도록 보장할 수 있습니다.

예제: 학생 데이터 관리 프로그램


이 예제에서는 바이너리 파일을 사용하여 학생 정보를 저장하고 조회하는 간단한 관리 프로그램을 작성합니다. 학생 데이터는 구조체를 활용하여 관리하며, 파일 입출력을 통해 데이터를 영구적으로 저장합니다.

프로그램 주요 기능

  1. 학생 데이터를 파일에 저장.
  2. 파일에서 학생 데이터를 읽어와 출력.

코드 예제

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

typedef struct {
    int id;
    char name[50];
    float grade;
} Student;

void addStudent(const char *filename) {
    FILE *file = fopen(filename, "ab"); // 추가 모드로 파일 열기
    if (file == NULL) {
        perror("파일 열기 실패");
        return;
    }

    Student student;
    printf("학생 ID: ");
    scanf("%d", &student.id);
    printf("학생 이름: ");
    scanf("%s", student.name);
    printf("학생 성적: ");
    scanf("%f", &student.grade);

    if (fwrite(&student, sizeof(Student), 1, file) != 1) {
        perror("학생 데이터 쓰기 실패");
    } else {
        printf("학생 데이터가 성공적으로 저장되었습니다.\n");
    }

    fclose(file);
}

void viewStudents(const char *filename) {
    FILE *file = fopen(filename, "rb"); // 읽기 모드로 파일 열기
    if (file == NULL) {
        perror("파일 열기 실패");
        return;
    }

    Student student;
    printf("\n저장된 학생 데이터:\n");
    printf("--------------------------\n");
    while (fread(&student, sizeof(Student), 1, file) == 1) {
        printf("ID: %d\n", student.id);
        printf("이름: %s\n", student.name);
        printf("성적: %.2f\n", student.grade);
        printf("--------------------------\n");
    }

    if (feof(file)) {
        printf("모든 데이터를 출력했습니다.\n");
    } else {
        perror("데이터 읽기 중 오류 발생");
    }

    fclose(file);
}

int main() {
    const char *filename = "students.bin";
    int choice;

    while (1) {
        printf("\n학생 데이터 관리 시스템\n");
        printf("1. 학생 추가\n");
        printf("2. 학생 보기\n");
        printf("3. 종료\n");
        printf("선택: ");
        scanf("%d", &choice);

        switch (choice) {
            case 1:
                addStudent(filename);
                break;
            case 2:
                viewStudents(filename);
                break;
            case 3:
                printf("프로그램을 종료합니다.\n");
                return 0;
            default:
                printf("잘못된 선택입니다. 다시 시도하세요.\n");
        }
    }
}

코드 설명

  1. addStudent 함수: 학생 정보를 입력받아 바이너리 파일에 추가.
  2. viewStudents 함수: 바이너리 파일에 저장된 학생 정보를 읽어와 출력.
  3. 파일 열기: "ab"(추가 모드)와 "rb"(읽기 모드)를 사용하여 파일을 엽니다.
  4. 데이터 쓰기와 읽기: fwrite()fread()를 사용하여 구조체 데이터를 바이너리 형식으로 파일에 저장 및 읽기.
  5. 메인 함수: 메뉴 기반의 사용자 인터페이스를 제공하여 사용자가 원하는 작업을 선택할 수 있도록 구성.

실행 예

학생 데이터 관리 시스템  
1. 학생 추가  
2. 학생 보기  
3. 종료  
선택: 1  
학생 ID: 101  
학생 이름: Alice  
학생 성적: 92.5  
학생 데이터가 성공적으로 저장되었습니다.

선택: 2  
저장된 학생 데이터:  
--------------------------  
ID: 101  
이름: Alice  
성적: 92.50  
--------------------------  

유의사항

  • 파일 경로가 올바른지 확인해야 합니다.
  • 데이터 입력 시 사용자 입력 값을 검증하여 잘못된 데이터가 저장되지 않도록 해야 합니다.
  • 파일 읽기 오류 발생 시 feof()ferror()를 사용하여 원인을 확인해야 합니다.

이 프로그램은 구조체와 바이너리 파일을 활용하여 간단한 데이터베이스 시스템을 구현하는 좋은 연습 사례입니다.

실습 문제


바이너리 파일을 사용하여 데이터를 저장하고 읽는 연습을 통해 C 언어의 파일 입출력에 대한 이해를 높일 수 있습니다. 다음 실습 문제를 해결해 보세요.

문제 1: 상품 관리 시스템


요구사항

  1. 구조체 Product를 정의하고, id, name, price를 포함하세요.
  2. 바이너리 파일을 사용하여 다음 작업을 수행하세요:
  • 상품 추가
  • 저장된 상품 목록 보기
  1. 사용자 입력을 받아 파일에서 데이터를 읽고 쓰는 기능을 구현하세요.

힌트

  • fwrite()fread()를 사용하여 데이터를 기록하고 읽습니다.
  • 파일 모드는 "ab""rb"를 사용합니다.

예시 코드

typedef struct {
    int id;
    char name[50];
    float price;
} Product;

문제 2: 데이터 수정 기능 추가


요구사항

  1. 기존 상품 관리 시스템에 데이터를 수정하는 기능을 추가하세요.
  2. 사용자로부터 수정할 상품 ID를 입력받아 해당 상품의 정보를 업데이트합니다.
  3. 기존 데이터를 읽어오고, 변경된 데이터를 덮어씁니다.

힌트

  • fseek()를 사용하여 파일의 특정 위치로 이동합니다.
  • 수정하려는 데이터의 위치는 구조체 크기를 이용해 계산할 수 있습니다.

예시 코드 조각

fseek(file, sizeof(Product) * (target_index), SEEK_SET);
fwrite(&updated_product, sizeof(Product), 1, file);

문제 3: 파일에서 특정 데이터 삭제


요구사항

  1. 특정 ID의 데이터를 삭제하는 기능을 구현하세요.
  2. 삭제할 데이터를 제외한 나머지 데이터를 새로운 임시 파일에 복사한 뒤 원본 파일을 덮어씁니다.

힌트

  • 파일을 순차적으로 읽고, 조건에 맞는 데이터를 새로운 파일에 기록합니다.
  • 파일 작업 완료 후 기존 파일을 삭제하고 임시 파일을 원본 파일로 이름 변경합니다.

예시 코드 흐름

  1. 원본 파일 읽기: "rb"
  2. 임시 파일 쓰기: "wb"
  3. 조건에 따라 데이터를 새 파일로 복사
  4. 원본 파일 삭제 후 임시 파일 이름 변경

문제 4: 사용자별 데이터베이스 생성


요구사항

  1. 프로그램 실행 시 사용자 이름을 입력받고 해당 이름으로 별도의 파일을 생성합니다.
  2. 각 사용자 파일에만 해당 사용자의 데이터를 저장하도록 구현하세요.
  3. 프로그램 종료 후에도 각 사용자의 데이터가 유지되도록 만드세요.

힌트

  • 파일 이름을 동적으로 생성하기 위해 sprintf()를 사용합니다.
  • 사용자 이름에 따라 다른 파일을 엽니다.
char filename[100];
sprintf(filename, "%s_data.bin", username);
FILE *file = fopen(filename, "wb");

문제 해결 팁

  • 코드를 작성하기 전에 파일 작업 흐름과 필요한 기능을 설계합니다.
  • 파일 작업 중 오류 처리를 반드시 포함하여 예외 상황에 대응합니다.
  • 실습 완료 후 프로그램이 요구사항을 정확히 만족하는지 테스트합니다.

이 실습 문제를 통해 바이너리 파일 입출력의 다양한 시나리오를 직접 경험해 보세요!

요약


이 기사에서는 C 언어를 활용하여 바이너리 파일을 읽고 쓰는 방법과 그 활용 사례를 다루었습니다. 바이너리 파일의 개념부터 구조체 데이터를 처리하는 방법, 파일 입출력 시 발생할 수 있는 오류 처리, 그리고 학생 관리 프로그램과 실습 문제를 통해 실용적인 적용 방식을 익혔습니다. 이러한 지식을 통해 효율적이고 신뢰성 있는 데이터 관리를 구현할 수 있습니다.