기술자료

DBMS, DB 구축 절차, 빅데이터 기술 칼럼, 사례연구 및 세미나 자료를 소개합니다.

다시 시작하는 윈도우 프로그래밍 : 윈도우 파일과 폴더 이야기

기술자료
DBMS별 분류
Etc
작성자
dataonair
작성일
2014-04-01 00:00
조회
3641



다시 시작하는 윈도우 프로그래밍

윈도우 파일과 폴더 이야기



윈도우의 많은 애플리케이션은 파일과 폴더를 액세스하고 생성 및 삭제한다. 그 만큼 윈도우 프로그래밍에 있어 파일과 폴더에 대한 이해는 기본이자 필수다. 이 글에서는 윈도우 환경에서 파일과 폴더의 속성 그리고 이에 얽힌 함수들을 살펴보고 일부 함수의 기능을 보완해본다.



윈도우의 파일 속성 창에서 파일에 대한 수많은 정보를 확인할 수 있다(<그림 1> 참조). 그렇다면 윈도우 프로그래밍을 개발할 때 파일의 크기, 생성 날짜, 특성 등의 정보를 어떻게 얻을 수 있을까 그 답이 바로 GetFileAttributes 함수다. 파일이나 폴더의 속성을 조회하는 이 함수의 원형은 다음과 같다.

DWORD WINAPI GetFileAttributes(LPCTSTR lpFileName);

GetFileAttributes의 lpFileName에 대상 파일의 경로를 대입하면 해당 대상의 속성이 반환된다. 만약 속성을 얻는 데 실패하면 ‘INVALID_FILE_ATTRIBUTES’가 반환되며, 반환값에 대한 자세한 의미는 <표 1>에서 확인할 수 있다.

tech_img1163.png

tech_img1164.jpg

지금부터는 <표 1>에 정리된 속성이 실제 윈도우의 UI에서의 어떤 속성과 관련이 있는지를 알아보자. 윈도우7의 속성 창인 <그림 1>에서 FILE_ATTRIBUTE_READONLY는 ‘읽기 전용’을, FILE_ATTRIBUTE_HIDDEN은 ‘숨김’이다. 이 창의 고급 탭을 클릭하면 <그림 2>와 같은 고급 특성 창이 열리며, 여기서 FILE_ATTRIBUTE_ARCHIVE는 ‘보관 가능’을, FILE_ ATTRIBUTE_NOT_CONTENT_INDEXED는 ‘빠른 검색을 위한 파일 색인 허용’을, FILE_ATTRIBUTE_COMPRESSED은 ‘내용을 압축하여 디스크 공간 절약’을, FILE_ATTRI BUTE_ENCRYPTED는 ‘데이터 보호를 위해 내용을 암호화’를 의미한다. 참고로 FILE_ATTRIBUTE_NOT_CONTENT _INDEXED의 경우 다른 속성과 달리 이 속성이 체크되지 않으면 비트가 조합되는 것이 특징이다.

tech_img1165.png

GetFileAttributesEx는 GetFileAttributes보다 더 많은 속성 정보를 제공하는 함수로, 원형은 다음과 같다.

BOOL WINAPI GetFileAttributesEx(LPCTSTR lpFileName, GET_FILEEX_INFO_LEVELS fInfoLevelId, LPVOID lpFileInformation);

lpFileName에는 대상 파일의 경로를, fInfoLevelId에는 GetFileExInfoStandard를 지정한다. 또한 lpFileInformation에는 WIN32_FILE_ATTRIBUTE_DATA 구조체의 포인터를 지정하면 해당 파일에 대한 정보를 얻을 수 있으며, 반환값으로 함수 실행의 성공 여부를 알 수 있다. 이 함수에 사용된 WIN32_FILE_ATTRIBUTE_DATA 구조체의 원형은 다음과 같으며, 각 필드별 의미는 <표 2>와 같다. 참고로 구조체에 포함된 FILETIME의 경우 FileTimeTo SystemTime 함수를 이용하면 시스템 시간으로 변환할 수 있다.

typedef struct _WIN32_FILE_ATTRIBUTE_DATA {
DWORD dwFileAttributes;
FILETIME ftCreationTime;
FILETIME ftLastAccessTime;
FILETIME ftLastWriteTime;
DWORD nFileSizeHigh;
DWORD nFileSizeLow;
} WIN32_FILE_ATTRIBUTE_DATA, *LPWIN32_FILE_ATTRIBUTE_DATA;

tech_img1166.jpg

지금까지 배운 내용을 이해했다면 손쉽게 파일 속성 정보를 출력하는 프로그램을 개발할 수 있을 것이다. <리스트 1>은 하나의 예로, 파일 크기, 생성 시간 등의 다양한 정보를 알려준다. 프로그램 실행 시 인자로 대상 파일의 경로를 입력하면 되는데, 이 프로그램에서의 SystemTimeToTzSpecificLocalTime 함수는 FileTimeToSystemTime 함수로 얻은 UTC 정보를 현재 시스템에 설정된 타임존으로 변경한다.

<리스트 1> 파일 속성 출력#include int _tmain(int argc, _TCHAR* argv[])
{
if(argc < 2)
return 0; WIN32_FILE_ATTRIBUTE_DATA wfad;
if(!GetFileAttributesEx(argv[1], GetFileExInfoStandard, &wfad))
return 0; DWORD attr = wfad.dwFileAttributes;
if(attr & FILE_ATTRIBUTE_ARCHIVE)
printf("보관 가능\n"); if(attr & FILE_ATTRIBUTE_COMPRESSED)
printf("내용을 압축하여 디스크 공간 절약\n"); if(attr & FILE_ATTRIBUTE_ENCRYPTED)
printf("데이터 보호를 위해 암호화\n"); if((attr & FILE_ATTRIBUTE_NOT_CONTENT_INDEXED) == 0)
printf("이 파일의 속성 및 내용 색인 허용\n"); if(attr & FILE_ATTRIBUTE_READONLY)
printf("읽기 전용\n"); if(attr & FILE_ATTRIBUTE_HIDDEN)
printf("숨김\n"); LARGE_INTEGER size; size.LowPart = wfad.nFileSizeLow;
size.HighPart = wfad.nFileSizeHigh; printf("크기: %I64d\n", size.QuadPart); SYSTEMTIME utc, lt;
FileTimeToSystemTime(&wfad.ftCreationTime, &utc);
SystemTimeToTzSpecificLocalTime(NULL, &utc, <);

printf("만든 날짜: %04d-%02d-%02d %02d:%02d:%02d\n"
, lt.wYear
, lt.wMonth
, lt.wDay
, lt.wHour
, lt.wMinute
, lt.wSecond);
return 0;
}

GetFileAttributes 함수는 지금까지 소개한 파일 속성을 얻는 것 외에도 파일의 존재 여부를 체크할 수 있다. GetFile Attributes에 특정 파일 경로를 체크했을 때 해당 경로의 파일이나 디렉터리가 존재하지 않으면 INVALID_FILE_ATTRI BUTES를 반환하기 때문에 특정 파일의 존재 여부를 확인하는 데에도 유용하다.

<리스트 2> GetFileAttributes 함수로 파일 존재 여부 체크
BOOL MyPathFileExists(LPCTSTR path)
{
return GetFileAttributes(path) != INVALID_FILE_ATTRIBUTES;
}



폴더 속 파일 열거하기

윈도우 상의 폴더에 속한 파일을 열거하는 데 FindFirstFile과 FindNextFile 함수가 주로 사용된다.

HANDLE WINAPI FindFirstFile(LPCTSTR lpFileName, LPWIN32_ FIND_DATA lpFindFileData);

FindFirstFile 함수 원형의 lpFileName에는 검색할 파일 이름을 대입하며, 와일드카드 문자(*, ) 등을 사용해도 된다. 예컨대 ‘C:\util\*.*’을 대입하면 ‘C:\util’ 하위에 있는 모든 파일과 폴더가 열거되며, lpFindFileData에는 FindFirstFile이 처음 찾은 파일의 정보가 전달된다. 함수 실행이 성공한 경우 파일 검색 핸들이 반환되며, 실패할 경우 INVALID_HANDLE_VALUE가 반환된다. WIN32_FIND_DATA 구조체의 원형은 다음과 같으며, 각 필드별 의미는 <표 3>과 같다.

typedef struct _WIN32_FIND_DATA {
DWORD dwFileAttributes;
FILETIME ftCreationTime;
FILETIME ftLastAccessTime;
FILETIME ftLastWriteTime;
DWORD nFileSizeHigh;
DWORD nFileSizeLow;
DWORD dwReserved0;
DWORD dwReserved1;
TCHAR cFileName[MAX_PATH];
TCHAR cAlternateFileName[14];
} WIN32_FIND_DATA, *PWIN32_FIND_DATA, *LPWIN32_FIND_DATA;

tech_img1167.jpg

FindNextFile 함수는 FindFirstFile로 찾은 파일이 하나 이상인 경우 현재 찾은 파일의 다음 파일에 대한 정보를 알려주며, 그 원형은 다음과 같다.

BOOL WINAPI FindNextFile(HANDLE hFindFile, LPWIN32_FIND_ DATA lpFindFileData);

hFindFile에 FindFirstFile에서 반환된 파일 검색 핸들을 대입하면 lpFindFileData에 검색된 파일 정보가 전달된다. 또한 검색된 파일이 있는 경우 TRUE가, 실패할 경우 FALSE가 반환된다.

BOOL WINAPI FindClose(HANDLE hFindFile);

FindClose 함수는 FindFirstFile로 열은 검색 핸들을 닫는다. hFindFile에는 FindFirstFile에서 반환한 파일 검색 핸들을 넣으면 되며 성공하면 TURE를, 실패하면 FALSE를 반환한다. 지금까지 배운 내용을 활용해 특정 폴더 내의 파일을 열거하는 함수를 만들어보자. 우리가 만들 함수의 원형과 기능은 다음과 같다.

BOOL WINAPI EnumFiles(const tstring &dir, FEnumFileCB cb, PVOID param) ;

특정 폴더의 파일을 열거하는 EnumFiles 함수의 원형에서 dir에는 열거할 문자열을, cb에는 열 작업을 할 콜백 함수를, param에는 해당 콜백 함수로 전달된 정보를 지정한다. 모든 폴더가 성공적으로 열거되면 TRUE를, 실패한 경우 FLASE가 반환되며 코드는 <리스트 3>을 참고하자.

typedef BOOL (CALLBACK *FEnumFileCB)(LPCTSTR filepath, LPWIN32_FIND_DATA fd, PVOID param);

이 코드는 EnumFiles에서 각각의 파일을 찾았을 때마다 호출하는 콜백 함수로, filepath로 현재 검색된 파일의 절대 경로를 얻을 수 있다. fd에서는 검색된 파일의 정보를, param에서는 EnumFiles에서 전달한 param 값을 확인할 수 있으며, 열거 작업을 중단하고 싶으면 FALSE를, 계속 열거를 진행해야 할 경우 TRUE를 반환하면 된다.

<리스트 3> EnumFiles 함수
#include typedef BOOL (CALLBACK *FEnumFileCB)(LPCTSTR filepath, LPWIN32_FIND_DATA fd, PVOID param);
typedef std::basic_string tstring;BOOL EnumFiles(LPCTSTR dir, FEnumFileCB cb, PVOID param)
{
WIN32_FIND_DATA wfd;
HANDLE h; tstring findpath;
tstring currpath;
tstring path; BOOL ret = TRUE; findpath = dir;
currpath = dir;
tstring::size_type pos = currpath.rfind(_T('\\'));
if(pos != tstring::npos)
currpath.erase(pos+1);
else
currpath.clear(); h = FindFirstFile(findpath.c_str(), &wfd);
if(h == INVALID_HANDLE_VALUE)
return ret; for(;;)
{
path = currpath;
path += wfd.cFileName; try
{
ret = cb(path.c_str(), &wfd, param);
if(!ret)
break;
}
catch(...)
{
} if(!FindNextFile(h, &wfd))
{
if(GetLastError() != ERROR_NO_MORE_FILES)
ret = FALSE; break;
}
} FindClose(h);
return ret;
}

이제 <리스트 3>의 EnumFiles 함수를 활용해 C 드라이브에 있는 내용을 열거하는 프로그램을 제작해보자(<리스트 4> 참조).

<리스트 4> C 드라이브의 모든 파일 열거
#include
#include
#include BOOL CALLBACK OnEnumFiles(LPCTSTR path, LPWIN32_FIND_DATA wfd, PVOID param)
{
_tprintf(_T("%s\n"), path);
return TRUE;
}int _tmain(int argc, _TCHAR* argv[])
{
setlocale(LC_ALL, "");
EnumFiles(_T("c:\\*.*"), OnEnumFiles, NULL);
return 0;
}



폴더 생성과 삭제

폴더 생성과 삭제는 윈도우에서 일상적인 작업 중 하나임에도 윈도우에서 제공하는 함수들은 기능이 다소 부족하다. 이러한 기본 함수를 살펴보고 이를 바탕으로 더 강력한 기능의 폴더 생성과 삭제 함수를 개발해보자.

BOOL WINAPI CreateDirectory(LPCTSTR lpPathName, LPSECURITY_ATTRIBUTES lpSecurityAttributes);

CreateDirectory 함수는 폴더를 생성하는 윈도우 함수로 lpPathName에는 생성할 폴더의 경로를, lpSecurityAttributes에는 보안 속성을 대입하며 일반적인 lpSecurityAttributes의 값은 NULL이다. 마찬가지로 함수가 성공하면 TRUE를, 실패하면 FALSE가 반환된다. 이 함수의 가장 불편한 점은 생성할 폴더의 상위 경로가 반드시 존재해야만 폴더가 생성된다는 점이다. 예컨대 ‘d:\a\b\c\d\e’란 폴더를 생성한다면 반드시 ‘d:\a\b\c\d’가 존재해야만 한다. 따라서 a부터 경로가 하나도 없는 경우 반복적으로 함수를 호출해 순차적으로 폴더를 생성해야 한다. 이런 불편을 해소할 수 있는 함수가 쉘에서 제공하는 SHCreateDirectory, SHCreateDirectoryEx다.

int SHCreateDirectory(HWND hwnd, LPCWSTR pszPath);

SHCreateDirectory 함수의 hWnd에는 소유자의 윈도우 핸들을 대입하는데 보통 NULL을 지정한다. pszPath에는 생성할 경로를 입력하면 되고 폴더 생성에 성공하면 ERROR_SUCCESS가 반환된다.

int SHCreateDirectoryEx(HWND hwnd, LPCTSTR pszPath, SECURITY_ATTRIBUTES *psa);

SHCreateDirectoryEx의 경우 SHCreateDirectory에 보안 속성 지정이 추가된 함수로, hWnd와 pszPath의 의미와 반환값은 SHCreateDirectory 함수와 같다. 단지 psa에 지정할 보안 속성을 지정하는 점이 다르다. 한 가지 주의할 것은 SHCreateDirectory와 SHCreate DirectoryEx 함수는 윈도우NT와 같은 오래된 OS 상에서 동작하지 않는다는 점이다. 그러므로 윈도우NT를 지원해야 한다면 해당 함수를 직접 개발해야 한다(<리스트 5> 참조). <리스트 5>는 CreateDirectory와 GetFileAttributes 함수를 활용해 폴더를 생성하는 함수다. 필자가 직접 개발한 이 함수는 경로가 없는 경우에도 모든 경로를 생성하며 첫 번째 인자에 생성할 폴더의 경로를, 두 번째 인자에는 보안 속성을 지정한다.

<리스트 5> MyCreateDirectory 함수BOOL
MyCreateDirectoryInternalA(LPCSTR path, LPSECURITY_ATTRIBUTES sa)
{
ULONG attr; attr = GetFileAttributesA(path); if(attr == INVALID_FILE_ATTRIBUTES)
{
return CreateDirectoryA(path, sa);
} if(attr & FILE_ATTRIBUTE_DIRECTORY)
return TRUE; return FALSE;
}BOOL
MyCreateDirectoryA(LPCSTR apath, LPSECURITY_ATTRIBUTES sa)
{
char path[512];GetFullPathNameA(apath, ARRAYSIZE(path), path, NULL); char *ptr = path;
while(*ptr)
{
if(*ptr == '\\')
{
*ptr = '\0'; if(!MyCreateDirectoryInternalA(path, sa))
return FALSE; *ptr = '\\';
} ++ptr;
} return MyCreateDirectoryInternalA(path, sa);
}BOOL
MyCreateDirectoryW(LPCWSTR wpath, LPSECURITY_ATTRIBUTES sa)
{
CHAR path[MAX_PATH]; WideCharToMultiByte(CP_UTF8, 0, wpath, -1, path, ARRAYSIZE(path), NULL, NULL);
return MyCreateDirectoryA(path, sa);
}

RemoveDirectory 함수는 인자로 전달받은 폴더를 삭제하며, 그 원형은 다음과 같다.

BOOL WINAPI RemoveDirectory(LPCTSTR lpPathName);

RemoveDirectory 함수도 CreateDirectory와 동일한 제약 사항이 있어 파일이 없는 경우에만 삭제할 수 있다. 안타깝게도 RemoveDirectory에 대한 쉘 헬퍼 함수가 없어 폴더 내부를 모두 삭제하기 위해서는 별도의 함수를 제작해야 한다. <리스트 6>은 폴더 속에 포함된 모든 파일과 폴더를 삭제하는 함수로, 앞서 소개한 EnumFiles 함수를 이용해 내부 파일을 모두 삭제한 후 폴더를 삭제한다. ‘C:\util’ 폴더를 삭제하고 싶은 경우 MyRemoveDirectory(“c:\\util”)처럼 이용하면 되며, 동작 원리는 코드를 분석하면 쉽게 이해할 수 있을 것이다. 만약 이해가 쉽지 않다면 DeleteFile, RemoveDirectory, DeleteFile 함수 앞에 printf 출력문을 추가하면 쉽게 이해할 수 있을 것이다.

<리스트 6> MyRemoveDirectory 함수
BOOL WINAPI
DeleteFileCB(LPCTSTR path, LPWIN32_FIND_DATA fd, PVOID param)
{
if(fd->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
{
return MyRemoveDirectory(path);
} return DeleteFile(path);
}BOOL WINAPI
MyRemoveDirectory(LPCTSTR path)
{
TCHAR tpath[MAX_PATH]; GetFullPathName(path, ARRAYSIZE(tpath), tpath, NULL);
StringCbCat(tpath, sizeof(tpath), _T("\\*.*")); EnumFiles(tpath, DeleteFileCB, NULL);
return RemoveDirectory(path);
}

그런데 MyRemoveDirectory 함수에는 한 가지 문제가 있다. 바로 읽기 전용 파일의 경우 삭제되지 않는다는 점이다. 이를 해결하기 위해서는 읽기 전용 파일의 속성을 해제해야 하며, SetFileAttributes 함수로 파일의 속성을 손쉽게 변경할 수 있다.
BOOL WINAPI SetFileAttributes(LPCTSTR lpFileName, DWORD dwFileAttributes);

SetFileAttributes 함수의 lpFileName에는 파일 경로를, dwFileAttributes에는 변경할 속성을 대입하며, 여기에 입력해야 할 속성은 GetFileAttributes의 속성값과 같다. 또한 반환값은 함수의 성공 여부다. MyRemoveDirectory 함수에 읽기 전용 속성 처리 기능을 추가한 결과는 <리스트 7>과 같다.

<리스트 7> 개선된 MyRemoveDirectory 함수
BOOL WINAPI
DeleteFileCB(LPCTSTR path, LPWIN32_FIND_DATA fd, PVOID param)
{
if(fd->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
{
return MyRemoveDirectory(path);
} if(fd->dwFileAttributes & FILE_ATTRIBUTE_READONLY)
SetFileAttributes(path, fd->dwFileAttributes & ~FILE_ATTRIBUTE_READONLY); return DeleteFile(path);
}BOOL WINAPI
MyRemoveDirectory(LPCTSTR path)
{
TCHAR tpath[MAX_PATH]; GetFullPathName(path, ARRAYSIZE(tpath), tpath, NULL);
StringCbCat(tpath, sizeof(tpath), _T("\\*.*")); EnumFiles(tpath, DeleteFileCB, NULL);
return RemoveDirectory(path);
}

여기서 기본 부분은 동일한데 단지 DeleteFile과 Remove Directory 앞에 FILE_ATTRIBUTE_ READONLY가 설정된 경우 해당 속성을 제거하는 부분만이 추가됐다.