82.7 기능별로 파일 나누기

지금은 main.c 파일 안에 모든 기능이 다 들어가 있습니다. 이렇게 하면 소스 코드가 간단할 때는 괜찮지만 소스 코드가 점점 복잡해지면 기능 추가도 힘들어지고 유지보수도 쉽지 않게 됩니다.

지금까지 구현했던 함수들을 기능별, 용도별로 나누고, 추가로 필요한 함수를 더 구현해보겠습니다.

먼저 실행 파일에 명령을 지정했을 때 호출할 함수들입니다.

  • 파일 추가(append)
  • 파일 목록 출력(list)
  • 파일 추출(extract)

main 함수에 들어있던 초기화 및 메모리 해제 코드를 분리하고, 파일 중복 추가를 방지하기 위한 함수도 필요합니다.

  • 초기화 함수(initialize)
  • 종료 함수(finalize)
  • 아카이브에 특정 파일이 있는지 검사하는 함수(isExist)

마지막으로 파일 아카이브 기능과는 직접적인 관련이 없는 공통 함수는 따로 분리합니다.

  • 파일의 크기를 구하는 함수(getFileSize)

지금까지 설명한 함수들을 다음과 같이 각 파일에 넣겠습니다.

표 82-1 함수별 파일 분류
헤더 파일/소스 파일 함수 설명
archive.h
archive.c
initialize 아카이브 파일을 열어서 매직 넘버와 파일 버전을 검사하고, 파일 목록을 만듭니다.
finalize 프로그램 종료 전에 각종 구조체 메모리를 해제하고 아카이브 파일을 닫습니다.
isExist 아카이브에 특정 파일이 있는지 검사합니다.
command.h
command.c
append 아카이브에 파일을 추가합니다.
list 아카이브의 파일 목록을 출력합니다.
extract 아카이브에서 파일을 추출합니다.
file.h
file.c

getFileSize 파일의 크기를 구합니다.
main.c main main 함수입니다.

이제 Visual Studio에서 지금까지 나온 .h, .c 파일들을 filearchive 프로젝트에 추가합니다.

  1. 왼쪽 솔루션 탐색기에서 소스 파일을 선택한 뒤 마우스 오른쪽 버튼을 클릭하면 팝업 메뉴가 나옵니다. 추가(D) > 새 항목(W)...을 클릭합니다.
  2. 파일 추가 창이 표시되면 이름(N).h, .c 파일 이름을 입력하고 추가(A) 버튼을 클릭합니다.

프로젝트에 모든 파일을 추가하면 다음과 같은 모양이 됩니다.

그림 82-20 filearchive 프로젝트에 헤더 파일과 소스 파일 추가

프로젝트 및 전체 소스 파일은 GitHub 저장소의 Unit 81/filearchive 폴더에 들어있습니다.

먼저 archive.h 헤더 파일입니다. 여러 곳에서 포함하는 헤더 파일이므로 #ifndef ARCHIVE_H, #define ARCHIVE_H, #endif를 사용하여 중복 포함을 방지합니다(이렇게 조건부 컴파일로 만들지 않으면 중복 포함이 발생하고, 컴파일 에러가 발생합니다). 그리고 각종 구조체와 initialize, finalize, isExist 함수를 선언해줍니다.

filearchive/filearchive/archive.h

#ifndef ARCHIVE_H    // ARCHIVE_H가 정의되어 있지 않다면
#define ARCHIVE_H    // ARCHIVE_H 매크로정의

#include <stdio.h>     // 파일 처리 함수가 선언된 헤더 파일
#include <stdint.h>    // 크기별로 정수 자료형이 정의된 헤더 파일
#include <stdbool.h>   // bool, true, false가 정의된 헤더 파일

#pragma pack(push, 1)    // 구조체를 1바이트 크기로 정렬

// 아카이브 파일에 저장되는 구조체
typedef struct _ARCHIVE_HEADER {    // 아카이브 헤더 구조체 정의
    uint16_t magic;                     // 아카이브 파일 매직 넘버
    uint16_t version;                   // 아카이브 파일 버전
} ARCHIVE_HEADER, *PARCHIVE_HEADER;

// 아카이브 파일에 저장되는 구조체
typedef struct _FILE_DESC {         // 파일 정보 구조체 정의
    char     name[256];                 // 파일 이름
    uint32_t size;                      // 파일 크기
    uint32_t dataOffset;                // 파일 데이터의 위치
} FILE_DESC, *PFILE_DESC;

#pragma pack(pop)

// 프로그램에서만 사용하는 구조체
typedef struct _FILE_NODE {         // 파일 목록 연결 리스트 구조체 정의
    struct _FILE_NODE *next;            // 다음 노드의 주소를 저장할 포인터
    FILE_DESC desc;                     // 파일 정보
} FILE_NODE, *PFILE_NODE;

// 프로그램에서만 사용하는 구조체
typedef struct _ARCHIVE {           // 아카이브 메인 구조체
    ARCHIVE_HEADER header;              // 아카이브 헤더
    FILE *fp;                           // 아카이브 파일 포인터
    FILE_NODE fileList;                 // 파일 목록 연결 리스트의 머리 노드
} ARCHIVE, *PARCHIVE;

PARCHIVE initialize();                             // 초기화 함수
void finalize(PARCHIVE archive);                   // 종료 함수
bool isExist(PARCHIVE archive, char *filename);    // 아카이브에 특정 파일이 있는지 검사하는 함수

#endif               // #ifndef ARCHIVE_H 끝

다음은 archive.c 파일입니다. main 함수에 들어있던 코드를 initialize, finalize 함수로 분리하고, isExist 함수를 구현합니다. 여기서 isExist 함수는 파일 목록 연결 리스트의 첫 번째 노드부터 순회하면서 파일이 있으면 true를 반환하고 없으면 false를 반환합니다.

filearchive/filearchive/archive.c

#define _CRT_SECURE_NO_WARNINGS    // fopen 보안 경고로 인한 컴파일 에러 방지
#include <stdlib.h>    // malloc, free 함수가 선언된 헤더 파일
#include <stdbool.h>   // bool, true, false가 정의된 헤더 파일
#include <string.h>    // strcmp, memset 함수가 선언된 헤더 파일
#include "archive.h"
#include "file.h"

#define ARCHIVE_NAME "archive.bin"    // 아카이브 파일 이름

PARCHIVE initialize()    // 초기화 함수 정의
{
    PARCHIVE archive = malloc(sizeof(ARCHIVE));
    memset(archive, 0, sizeof(ARCHIVE));

    FILE *fp = fopen(ARCHIVE_NAME, "r+b");    // 아카이브 파일을 읽기/쓰기 모드로 열기
    if (fp == NULL)                           // 아카이브 파일이 없으면
    {
        fp = fopen(ARCHIVE_NAME, "w+b");      // 아카이브 파일을 생성
        if (fp == NULL)                       // 파일 생성(열기)에 실패하면
            return NULL;                      // 함수 종료 및 NULL 반환

        // 새 아카이브 헤더 생성
        archive->header.magic = 'AF';         // 매직 넘버 AF 저장(리틀 엔디언에서는 FA로 저장됨)
        archive->header.version = 1;          // 파일 버전 1 저장

        // 아카이브 파일에 아카이브 헤더 저장
        if (fwrite(&archive->header, sizeof(ARCHIVE_HEADER), 1, fp) < 1)
        {
            printf("아카이브 헤더 쓰기 실패\n");
            fclose(fp);
            return NULL;
        }
    }
    else // 아카이브 파일이 있으면
    {
        // 아카이브 파일에서 아카이브 헤더 읽기
        if (fread(&archive->header, sizeof(ARCHIVE_HEADER), 1, fp) < 1)
        {
            printf("아카이브 헤더 읽기 실패\n");
            fclose(fp);
            return NULL;
        }
    }

    // 아카이브 파일 매직 넘버 검사
    if (archive->header.magic != 'AF')
    {
        printf("아카이브 파일이 아닙니다.\n");
        fclose(fp);
        return NULL;
    }

    // 아카이브 파일 버전 검사
    if (archive->header.version != 1)
    {
        printf("버전이 맞지 않습니다.\n");
        fclose(fp);
        return NULL;
    }

    archive->fp = fp;    // 아카이브 파일 포인터 저장

    uint32_t size = getFileSize(fp);    // 아카이브 파일의 크기를 구함
    uint32_t currPos = ftell(fp);       // 현재 파일 포인터의 위치를 구함

    while (size > currPos)         // 파일 포인터의 위치가 파일 크기보다 작을 때 반복
    {
        PFILE_NODE node = malloc(sizeof(FILE_NODE));
        memset(node, 0, sizeof(FILE_NODE));

        // 파일 정보 읽기
        if (fread(&node->desc, sizeof(FILE_DESC), 1, fp) < 1)
        {
            printf("아카이브 파일 읽기 실패\n");
            free(node);
            finalize(archive);
            return NULL;
        }

        // 연결 리스트에 파일 정보 노드(FILE_NODE) 추가
        node->next = archive->fileList.next;
        archive->fileList.next = node;

        // 현재 파일 포인터의 위치에 파일 크기를 더하여 다음 파일 정보 위치로 이동
        currPos = ftell(fp) + node->desc.size;
        fseek(fp, currPos, SEEK_SET);
    }

    return archive;
}

void finalize(PARCHIVE archive)    // 종료 함수 정의
{
    // 파일 목록 연결 리스트를 순회하면서 메모리 해제
    PFILE_NODE curr = archive->fileList.next;    // 첫 번째 노드
    while (curr != NULL)
    {
        PFILE_NODE next = curr->next;
        free(curr);

        curr = next;
    }

    fclose(archive->fp);    // 아카이브 파일 포인터 닫기

    free(archive);    // 아카이브 메인 구조체 해제
}

bool isExist(PARCHIVE archive, char *filename)    // 아카이브에 특정 파일이 있는지 검사하는 함수
{
    // 파일 목록 연결 리스트를 순회하면서 파일이 있는지 검사
    PFILE_NODE curr = archive->fileList.next;    // 첫 번째 노드
    while (curr != NULL)
    {
        // 파일이 있으면 true 반환
        if (strcmp(curr->desc.name, filename) == 0)
            return true;

        curr = curr->next;
    }

    return false;    // 파일을 찾지 못했으면 false 반환
}

isExist 함수에서 파일이 있는지 검사하려면 파일 목록 연결 리스트를 순회하면서 strcmp 함수로 desc.name과 매개변수로 받은 filename을 비교하여 같은지(0이 나오는지) 확인하면 됩니다.

        // 파일이 있으면 true 반환
        if (strcmp(curr->desc.name, filename) == 0)
            return true;

이번에는 아카이브 명령을 구현할 command.h 파일과 command.c 파일입니다. 여기에는 실행 파일에 append, list, extract 명령을 지정했을 때 호출할 함수들이 들어갑니다. 명령과 함수 이름은 같습니다.

다음과 같이 헤더 파일에서 함수를 선언할 때 매개변수에서 PARCHIVE를 사용하고 있으므로 archive.h 헤더 파일을 포함해야 합니다.

filearchive/filearchive/command.h

#ifndef COMMAND_H    // COMMAND_H가 정의되어 있지 않다면
#define COMMAND_H    // COMMAND_H 매크로정의

#include "archive.h"

int append(PARCHIVE archive, char *filename);     // 파일 추가 함수
void list(PARCHIVE archive);                      // 파일 목록 출력 함수
int extract(PARCHIVE archive, char *filename);    // 파일 추출 함수

#endif               // #ifndef COMMAND_H 끝

이제 append, list,extract 함수를 구현합니다. append 함수는 아카이브에 파일이 하나도 없을 때와 파일이 있다면 가장 마지막에 추가된 파일의 뒤에 새 파일을 추가하도록 변경되었습니다. list,extract 함수는 앞과 바뀐 부분이 없으므로 생략하겠습니다.

filearchive/filearchive/command.c

#define _CRT_SECURE_NO_WARNINGS    // fopen, strcpy 보안 경고로 인한 컴파일 에러 방지
#include <stdio.h>     // 파일 처리 함수가 선언된 헤더 파일
#include <stdlib.h>    // malloc, free 함수가 선언된 헤더 파일
#include <stdint.h>    // 크기별로 정수 자료형이 정의된 헤더 파일
#include <string.h>    // strcpy, strcmp, memset 함수가 선언된 헤더 파일
#include "archive.h"
#include "file.h"

int append(PARCHIVE archive, char *filename)    // 파일 추가 함수 정의
{
    int ret = 0;    // 함수가 성공했는지 실패했는지 반환값으로 표현

    // 추가할 파일 열기
    FILE *fp = fopen(filename, "rb");
    if (fp == NULL)
    {
        printf("%s 파일이 없습니다.\n", filename);
        return -1;    // 함수 종료. -1은 실패
    }

    uint8_t *buffer;
    uint32_t size;

    size = getFileSize(fp);    // 추가할 파일의 크기를 구함
    buffer = malloc(size);

    // 추가할 파일의 내용을 읽음
    if (fread(buffer, size, 1, fp) < 1)
    {
        printf("%s 파일 읽기 실패\n", filename);
        ret = -1;       // -1은 실패
        goto Error1;    // fp를 닫고 buffer를 해제하는 에러 처리로 이동
    }

    // 새 파일 정보 생성
    PFILE_DESC desc = malloc(sizeof(FILE_DESC));
    memset(desc, 0, sizeof(FILE_DESC));
    strcpy(desc->name, filename);    // 파일 정보 구조체에 추가할 파일의 이름 저장
    desc->size = size;               // 파일 정보 구조체에 추가할 파일의 크기 저장

    PFILE_NODE node = archive->fileList.next;    // 첫 번째 노드
    if (node == NULL)    // 아카이브 파일에 파일이 하나도 없으면
    {
        // 아카이브 헤더 바로 다음으로 파일 포인터를 이동시킴
        fseek(archive->fp, sizeof(ARCHIVE_HEADER), SEEK_SET);

        // 새로 추가될 파일 데이터의 시작 위치는
        // 현재 파일 포인터의 위치에 파일 정보 크기만큼 순방향으로 이동시킨 값
        desc->dataOffset = ftell(archive->fp) + sizeof(FILE_DESC);
    }
    else    // 연결 리스트에서 첫 번째 노드가 가장 마지막에 추가된 파일
    {
        // 마지막에 추가된 파일의 파일 데이터 위치에서 파일 크기만큼 순방향으로 이동
        fseek(archive->fp, node->desc.dataOffset + node->desc.size, SEEK_SET);

        // 새로 추가될 파일 데이터의 시작 위치는 
        // 마지막에 추가된 파일의 파일 데이터 위치에서 
        // 파일 크기, 파일 정보 크기만큼 순방향으로 이동시킨 값
        desc->dataOffset = node->desc.dataOffset + node->desc.size + sizeof(FILE_DESC);
    }

    // 아카이브 파일에 새 파일 정보 쓰기
    if (fwrite(desc, sizeof(FILE_DESC), 1, archive->fp) < 1)
    {
        printf("파일 정보 쓰기 실패\n");
        ret = -1;
        goto Error2;    // fp를 닫고, desc와 buffer를 해제하는 에러 처리로 이동
    }

    // 아카이브 파일에 새 파일 데이터 쓰기
    if (fwrite(buffer, size, 1, archive->fp) < 1)
    {
        printf("파일 데이터 쓰기 실패\n");
        ret = -1;
        goto Error2;    // fp를 닫고, desc와 buffer를 해제하는 에러 처리로 이동
    }

    printf("%s 파일 추가 성공\n크기: %d\n", filename, size);

Error2:
    free(desc);      // 파일 정보 저장용 동적 메모리 해제

Error1:
    free(buffer);    // 파일 내용 저장용 동적 메모리 해제

    fclose(fp);      // 파일 포인터 닫기

    return ret;      // 성공이냐 실패냐에 따라 0 또는 -1을 반환
}

void list(PARCHIVE archive)    // 파일 목록 출력 함수 정의
{
    // 생략 ...
}

int extract(PARCHIVE archive, char *filename)    // 파일 추출 함수 정의
{
    // 생략 ...
}

파일을 추가하기 전에 먼저 파일 목록 연결 리스트의 노드가 있는지 확인합니다. 여기서 첫 번째 노드가 NULL이면 파일이 없는 상태이므로 아카이브 헤더 바로 다음에 파일 정보를 쓸 수 있도록파일 포인터를 이동시키고, 새로 추가될 파일 데이터의 시작 위치는 현재 파일 포인터의 위치에 파일 정보크기만큼 순방향으로 이동시킨 값을 설정합니다.

    PFILE_NODE node = archive->fileList.next;    // 첫 번째 노드
    if (node == NULL)    // 아카이브 파일에 파일이 하나도 없으면
    {
        // 아카이브 헤더 바로 다음으로 파일 포인터를 이동시킴
        fseek(archive->fp, sizeof(ARCHIVE_HEADER), SEEK_SET);

        // 새로 추가될 파일 데이터의 시작 위치는
        // 현재 파일 포인터의 위치에 파일 정보 크기만큼 순방향으로 이동시킨 값
        desc->dataOffset = ftell(archive->fp) + sizeof(FILE_DESC);
    }

이 코드를 그림으로 나타내면 다음과 같은 모양이 됩니다.

그림 82-21 아카이브에 파일이 없을 때 새 파일을 추가하는 방법

만약 첫 번째 노드가 있다면 가장 마지막에 추가된 파일이므로 첫 번째 노드의 데이터 위치(node->desc.dataOffset)에서 파일 크기(node->desc.size)만큼 순방향으로 이동합니다. 그리고 새로 추가될 파일 데이터의 시작 위치는 마지막에 추가된 파일의 데이터 위치(node->desc.dataOffset)에서 파일 크기(node->desc.size), 파일 정보 크기(sizeof(FILE_DESC))만큼 순방향으로 이동시킨 값을 설정합니다.

    else    // 연결 리스트에서 첫 번째 노드가 가장 마지막에 추가된 파일
    {
        // 마지막에 추가된 파일의 파일 데이터 위치에서 파일 크기만큼 순방향으로 이동
        fseek(archive->fp, node->desc.dataOffset + node->desc.size, SEEK_SET);

        // 새로 추가될 파일 데이터의 시작 위치는 
        // 마지막에 추가된 파일의 파일 데이터 위치에서 
        // 파일 크기, 파일 정보 크기만큼 순방향으로 이동시킨 값
        desc->dataOffset = node->desc.dataOffset + node->desc.size + sizeof(FILE_DESC);
    }

이 코드를 그림으로 나타내면 다음과 같은 모양이 됩니다.

그림 82-22 아카이브에 파일이 있을 때 새 파일을 추가하는 방법

새 파일을 추가할 준비가 끝났다면 fwrite 함수를 사용하여 파일 정보와 파일 데이터를 씁니다.

    // 아카이브 파일에 새 파일 정보 쓰기
    if (fwrite(desc, sizeof(FILE_DESC), 1, archive->fp) < 1)
    {
        printf("파일 정보 쓰기 실패\n");
        ret = -1;
        goto Error2;    // fp를 닫고, desc와 buffer를 해제하는 에러 처리로 이동
    }

    // 아카이브 파일에 새 파일 데이터 쓰기
    if (fwrite(buffer, size, 1, archive->fp) < 1)
    {
        printf("파일 데이터 쓰기 실패\n");
        ret = -1;
        goto Error2;    // fp를 닫고, desc와 buffer를 해제하는 에러 처리로 이동
    }

내용이 생각보다 까다롭죠? 바이너리 파일 처리는 머릿속에서 생각을 많이 해야 됩니다. 지금은 이해가 잘 되지 않겠지만 나중에 파일을 많이 다루다 보면 서서히 이해가 될 것입니다.

다음은 파일 처리와 관련된 공통 함수가 들어가는 file.h, file.c 파일입니다. 여기서는 getFileSize 함수 하나만 들어가며 아카이브 초기화 함수, 파일 추가 함수에서 사용됩니다. 함수 하나만 구현하더라도 이처럼 기능에 따라 잘게 모듈화하는 것이 좋습니다.

filearchive/filearchive/file.h

#ifndef FILE_H    // FILE_H가 정의되어 있지 않다면
#define FILE_H    // FILE_H 매크로정의

#include <stdio.h>     // 파일 처리 함수가 선언된 헤더 파일
#include <stdint.h>    // 크기별로 정수 자료형이 정의된 헤더 파일

uint32_t getFileSize(FILE *fp);    // 파일의 크기를 구하는 함수

#endif            // #ifndef FILE_H 끝

filearchive/filearchive/file.c

#include <stdio.h>     // 파일 처리 함수가 선언된 헤더 파일
#include <stdint.h>    // 크기별로 정수 자료형이 정의된 헤더 파일

uint32_t getFileSize(FILE *fp)    // 파일의 크기를 구하는 함수 정의
{
    uint32_t size;
    uint32_t currPos = ftell(fp);    // 현재 파일 포인터의 위치 저장

    // 파일의 끝으로 이동하여 파일 크기를 구함
    fseek(fp, 0, SEEK_END);
    size = ftell(fp);

    fseek(fp, currPos, SEEK_SET);    // 파일 포인터를 이전 위치로 되돌림

    return size;
}

지금까지 아카이브 초기화, 종료를 담당하는 archive.h, archive.c, 아카이브 명령을 담당하는 command.h, command.c, 파일 처리 공통 함수가 들어있는 file.h, file.c를 만들어서 기능, 용도별로 파일을 분리했습니다. 파일을 분리하는 방법은 특별히 정해진 방법이 없습니다. 그때 그때 상황에 맞게 분리를 해주고, 지속적으로 개선해나가면 됩니다.