82.5 파일 목록 출력하기

이번에는 아카이브 파일 안에 들어있는 파일의 목록을 출력해보겠습니다. 파일 목록을 관리하는 방법은 여러 가지가 있지만, 이번 예제에서는 연결 리스트로 구현해보겠습니다.

다음은 아카이브 파일에서 파일 목록을 출력하는 과정입니다.

그림 82-14 아카이브 파일에서 파일 목록을 출력하는 과정

먼저 다음과 같이 파일 노드(FILE_NODE) 구조체가 필요합니다. 이 구조체는 파일 정보를 연결 리스트에 저장해서 파일 목록을 관리하기 위한 구조체입니다(FILE_NODE 구조체는 ARCHIVE 구조체와 마찬가지로 아카이브 파일에는 저장되지 않고 프로그램에서 데이터를 다룰 때만 사용합니다). FILE_NODE는 연결 리스트의 노드이므로 다음 노드의 주소를 저장할 포인터와 파일 정보 구조체 FILE_DESC 를 멤버로 가지고 있습니다.

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

만약 파일 세 개를 연결 리스트로 관리한다면 다음과 같은 모양이 되겠죠?

그림 82-15 파일 목록을 연결 리스트로 관리

ARCHIVE 구조체는 다음과 같이 파일 목록 연결 리스트의 머리 노드를 멤버로 추가해줍니다. 이렇게 하면 필요할 때마다 파일 목록을 확인할 수 있습니다.

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

이제 main 함수는 다음과 같이 수정합니다(전체 파일은 GitHub 저장소의 Unit 82/82.5 폴더에 들어있습니다).

main.c

int main()
{
    PARCHIVE archive = malloc(sizeof(ARCHIVE));
    memset(archive, 0, sizeof(ARCHIVE));

    PFILE_NODE curr;

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

        // 새 아카이브 헤더 생성
        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 -1;
        }
    }
    else    // 아카이브 파일이 있으면
    {
        // 아카이브 파일에서 아카이브 헤더 읽기
        if (fread(&archive->header, sizeof(ARCHIVE_HEADER), 1, fp) < 1)
        {
            printf("아카이브 헤더 읽기 실패\n");
            fclose(fp);
            return -1;
        }
    }

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

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

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

    int ret = 0;
    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);       // 동적 메모리 해제
            ret = -1;         // -1은 실패
            goto FINALIZE;    // 모든 동적 메모리 해제 코드로 이동
        }

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

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

    list(archive);    // 파일 목록 출력

FINALIZE:
    // 파일 목록 연결 리스트를 순회하면서 메모리 해제
    curr = archive->fileList.next;    // 첫 번째 노드
    while (curr != NULL)
    {
        PFILE_NODE next = curr->next;
        free(curr);

        curr = next;
    }

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

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

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

앞에서는 아카이브 파일이 없으면 새 아카이브 파일을 생성만 했습니다. 이번에는 아카이브 파일이 있으면 아카이브 파일에서 헤더를 읽는 부분을 추가했습니다.

FILE *fp = fopen(ARCHIVE_NAME, "r+b");    // 아카이브 파일을 읽기/쓰기 모드로 열기
if (fp == NULL)                           // 아카이브 파일이 없으면
{
    // 생략 ...
}
else    // 아카이브 파일이 있으면
{
    // 아카이브 파일에서 아카이브 헤더 읽기
    if (fread(&archive->header, sizeof(ARCHIVE_HEADER), 1, fp) < 1)
    {
        printf("아카이브 헤더 읽기 실패\n");
        fclose(fp);
        return -1;
    }
}

아카이브 헤더를 읽었으면 아카이브 파일이 맞는지 확인해야 합니다. 먼저 archive->header.magic에 들어있는 값이 'AF'('FA'를 리틀 엔디언으로 읽었으므로)가 맞는지 확인합니다. 그리고 archive->header.version에 들어있는 값이 1인지 확인합니다. 여기서 매직 넘버가 맞지 않으면 아카이브 파일이 아니므로 처리할 수 없습니다. 또한, 파일 버전이 맞지 않아도 처리하지 않습니다(예제에서는 버전이 1로 끝나지만 만약 여러분들이 파일 구조를 변경했다면 버전을 2로 올립니다).

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

// 아카이브 파일 버전 검사
if (archive->header.version != 1)
{
    printf("버전이 맞지 않습니다.\n");
    fclose(fp);
    return -1;
}
참고 | 버전별 호환성 확보

만약 파일 아카이브 구조가 변경되어 버전이 1, 2가 있을 때 프로그램 하나에서 모든 버전을 처리하려면 어떻게 해야 할까요?

다음과 같이 버전별로 구조체를 만들어놓고, 버전에 따라 해당 구조체를 사용하면 됩니다. 하지만 코드가 복잡해지는 것은 어쩔 수 없습니다.

typedef struct _FILE_DESCv1 {     // 파일 정보 구조체 버전 1
    // ...
} FILE_DESCv1, *PFILE_DESCv1;

typedef struct _FILE_DESCv2 {     // 파일 정보 구조체 버전 2
    // ...
} FILE_DESCv2, *PFILE_DESCv2;

typedef struct _FILE_NODEv1 {     // 파일 노드 구조체 버전 1
    struct FILE_NODEv1 *next;
    FILE_DESCv1 desc;
    // ...
} FILE_NODEv1, *PFILE_NODEv1;

typedef struct _FILE_NODEv2 {     // 파일 노드 구조체 버전 2
    struct FILE_NODEv2 *next;
    FILE_DESCv2 desc;
    // ...
} FILE_NODEv2, *PFILE_NODEv2;

switch 분기문이나 if, else로 버전에 따라 구조체를 선택해서 사용하면 됩니다.

switch (archive->header.version)
{
case 1:
{
    PFILE_NODE node = malloc(sizeof(FILE_NODE_v1));

    // 파일 정보 읽기
    if (fread(&node->desc, sizeof(FILE_DESC_v1), 1, fp) < 1)
    // 생략

    break;
}
case 2:
    PFILE_NODE node = malloc(sizeof(FILE_NODE_v2));

    // 파일 정보 읽기
    if (fread(&node->desc, sizeof(FILE_DESC_v2), 1, fp) < 1)
    // 생략

    break;
}

바이너리 파일의 버전별 호환성을 유지하는 작업은 쉬운 일이 아닙니다. 보통 어느 정도 호환성을 유지하다가 한계에 다다르면 호환성을 버리고 버전업을 하기도 합니다.

아카이브 파일에 들어있는 파일 정보를 읽는 부분인데 규칙은 간단합니다. 아카이브 헤더 바로 다음부터 파일 끝까지 계속 읽으면서 파일 정보를 가져오면 됩니다.

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

    while (size > currPos)         // 파일 포인터의 위치가 파일 크기보다 작을 때 반복
    {

앞에서 아카이브 헤더를 읽었으므로 파일 포인터는 아카이브 헤더가 끝나는 부분에 와있습니다. 이 상태에서 파일 정보를 읽으면 첫 번째 파일 정보를 읽게 되겠죠? 파일 정보를 읽었으면 연결 리스트에 파일 정보 노드를 추가합니다. 이때 연결 리스트에 파일 정보 노드를 추가할 때마다 머리 노드 다음에 추가하게 되므로 맨 뒤에 저장된 파일이 연결 리스트의 맨 앞에 옵니다. 즉, a, b, c 순서로 저장되어 있다면 연결 리스트는 머리 노드 → c → b → a와 같은 모양이 됩니다.

        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);       // 동적 메모리 해제
            ret = -1;         // -1은 실패
            goto FINALIZE;    // 모든 동적 메모리 해제 코드로 이동
        }

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

파일 정보를 읽은 뒤에는 현재 파일 포인터의 위치에 파일 크기를 더하여 다음 파일 정보(FILE_DESC) 위치로 이동합니다. 이렇게 계속 반복하면서 파일 정보를 읽은 뒤 파일 목록을 만들고, 파일 끝까지 갔을 때 반복을 중단하면 됩니다.

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

다음은 반복문에서 파일 정보를 읽는 과정입니다.

그림 82-16 파일 정보 읽기

파일 정보를 다 읽었으면 list 함수를 호출하여 파일 목록을 출력합니다.

    list(archive);    // 파일 목록 출력

프로그램을 종료하기 전에 모든 동적 메모리를 해제하고 파일 포인터를 닫습니다. goto 레이블이 있으므로 중간에 실패했을 때 이 코드로 바로 올 수 있습니다.

FINALIZE:
    // 파일 목록 연결 리스트를 순회하면서 메모리 해제
    curr = archive->fileList.next;    // 첫 번째 노드
    while (curr != NULL)
    {
        PFILE_NODE next = curr->next;
        free(curr);

        curr = next;
    }

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

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

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

이제 소스 코드의 main 함수 윗부분에 다음과 같이 list 함수를 추가합니다. archive를 매개변수로 받은 뒤 archive->fileList를 활용하여 연결 리스트의 첫 번째 노드부터 순회하면서 파일 이름을 출력하면 됩니다.

void list(PARCHIVE archive)    // 파일 목록 출력 함수 정의
{
    printf("파일 목록:\n");

    // 파일 목록 연결 리스트를 순회하면서 파일 이름 출력
    PFILE_NODE curr = archive->fileList.next;    // 첫 번째 노드
    while (curr != NULL)
    {
        printf("    %s\n", curr->desc.name);

        curr = curr->next;
    }
}

완성된 소스 코드는 다음과 같습니다(앞과 중복되는 부분은 생략, 전체 파일은 GitHub 저장소의 Unit 81/81.5 폴더에 들어있습니다).

main.c

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

#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;

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

uint32_t getFileSize(FILE *fp)   // 파일의 크기를 구하는 함수 정의
{
    // 생략 ...
}

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

void list(PARCHIVE archive)    // 파일 목록 출력 함수 정의
{
    printf("파일 목록:\n");

    // 파일 목록 연결 리스트를 순회하면서 파일 이름 출력
    PFILE_NODE curr = archive->fileList.next;    // 첫 번째 노드
    while (curr != NULL)
    {
        printf("    %s\n", curr->desc.name);

        curr = curr->next;
    }
}

int main()
{
    PARCHIVE archive = malloc(sizeof(ARCHIVE));
    memset(archive, 0, sizeof(ARCHIVE));

    PFILE_NODE curr;

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

        // 새 아카이브 헤더 생성
        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 -1;
        }
    }
    else    // 아카이브 파일이 있으면
    {
        // 아카이브 파일에서 아카이브 헤더 읽기
        if (fread(&archive->header, sizeof(ARCHIVE_HEADER), 1, fp) < 1)
        {
            printf("아카이브 헤더 읽기 실패\n");
            fclose(fp);
            return -1;
        }
    }

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

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

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

    int ret = 0;
    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);       // 동적 메모리 해제
            ret = -1;         // -1은 실패
            goto FINALIZE;    // 모든 동적 메모리 해제 코드로 이동
        }

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

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

    list(archive);    // 파일 목록 출력

FINALIZE:
    // 파일 목록 연결 리스트를 순회하면서 메모리 해제
    curr = archive->fileList.next;    // 첫 번째 노드
    while (curr != NULL)
    {
        PFILE_NODE next = curr->next;
        free(curr);

        curr = next;
    }

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

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

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

archive.bin 파일에는 hello.txt 파일만 들어있으므로 소스 코드를 컴파일하여 실행(Ctrl+F5)해보면 다음과 같이 출력됩니다.

실행 결과

파일 목록:
    hello.txt