소개 en ko

파일명의 대소문자 구별

2012-12-09

프로그램에서 문자열을 다룰 때 (특히 Key 역할을 하는 문자열의 경우) 가능한 대소문자를 구별하도록 하는 편이다. 이유는 다음과 같다.

  • 거의 모든 환경에서 대소문자를 구별하는 것이 문자열의 기본 동작이다. 예를 들어 문자열 타입의 두 변수를 == 로 비교하는 것은 일반적으로 대소문자를 구별한다. 코드가 간결하도록 하기 위해서는 대소문자를 구별하는 쪽이 유리하다.
  • 대소문자를 구별하는 경우와 그렇지 않은 경우가 혼재되어 있는 경우 인코딩 문제와 동일한 문제가 발생한다. (사실 대소문자를 구별/구별하지 않는 것 역시 인코딩이다) 어떤 변수를 사용할 때 이 변수가 대소문자를 구별하는지 아닌지를 사용할 때마다 신경써야 한다. 특히 이 문제는 잘못 사용했다고 문제가 늘 발생하는 것이 아니기 때문에 더 골치아프다.
  • 대소문자만 다른 두 개 이상의 문자열이 없다면 (Apple, apple 등의 경우) 대소문자를 구별하는 시스템이 대소문자를 구별하지 않는 시스템을 사용할 수는 있지만 반대로는 불가능하다.
  • 대소문자를 구별하는 쪽이 최적화에 유리하다. 특히 문자열키가 성능 크리티컬한 상황에서는 lower, upper 처리 혹은 stricmp 의 시간 소모 마저 아쉬울 때가 있다.

이런 이유로 통제가 가능하다면 대소문자를 구별하는 쪽이 시스템의 복잡도를 낮추는데 도움이 된다.

파일명의 대소문자 구별

윈도우의 경우 파일명의 대소문자를 구별하지 않는다. 하지만 대소문자를 보존해준다. (Case Preserving) 따라서 Apple 이라는 파일이 있을 때 apple 로도 파일을 열 수 있고 폴더 내 파일 목록을 얻으면 Apple 으로 원래 생성 때의 대소문자를 보존한 이름을 확인할 수 있다. 하지만 리눅스의 경우는 그렇지 않다. (ext) Apple 과 apple 은 엄밀히 다른 파일이며 두 개의 파일은 동시에 존재할 수 있다.

자 여기서 문제가 있다. 데이터 파일의 대소문자를 구별하고 싶은데 개발 환경이 윈도우라면 대소문자를 구별하는 시스템을 유지하기 어렵다. CreateFile 이 대소문자를 구별하지 않기 때문이다. 때문에 대부분의 윈도우 플랫폼의 프로젝트는 파일명에 대해 대소문자를 구별하지 않도록 작성한다. 또한 시스템의 투명성을 유지하기 위해 패킹된 파일 시스템을 만들더라도 내부적으로 대소문자를 구별하지 않도록 한다.

하지만 프로그램을 간단하게 유지하기 위해 대소문자를 구별하도록 하고 싶다. 어떻게 해결할 수 있을까?

윈도우 플랫폼에서 파일명의 대소문자 구별

파일에 접근할 때 파일명의 대소문자가 일치하는지 확인해서 일치하지 않으면 파일이 없다는 오류를 발생시키자. 이를 위해 아래와 같이 주어진 파일명의 원래 (대소문자가 올바른) 파일명을 반환하는 함수를 작성한다.

TCHAR* GetCaseCorrectedFileName(const TCHAR* directory,
                                TCHAR* fileName) {
  TCHAR findPath[MAX_PATH];
  _tcscpy(findPath, directory);
  _tcscat(findPath, _T("\\"));
  _tcscat(findPath, fileName);

  WIN32_FIND_DATA fd;
  HANDLE fh = FindFirstFile(findPath, &fd);
  if (fh == INVALID_HANDLE_VALUE) {
    return NULL;
  }
  if (FindNextFile(fh, &fd) == 0 &&
      GetLastError() == ERROR_NO_MORE_FILES &&
      _tcslen(fileName) == _tcslen(fd.cFileName)) {
    _tcscpy(fileName, fd.cFileName);
    FindClose(fh);
    return fileName;
  }
  FindClose(fh);
  return NULL;
}

이 함수는 아래와 같이 사용할 수 있다. 파일명을 넣으면 대소문자가 올바르게 수정된 파일명을 얻을 수 있다. (다음을 실행하면 dbgview.exe → DbgView.exe 가 출력된다)

void test() {
  TCHAR buf[] = _T("dbgview.exe");
  _tprintf(_T("%s -> "), buf);
  if (GetCaseCorrectedFileName(_T("C:\\SysinternalsSuite"), buf)) {
    _tprintf(_T("%s\n"), buf);
  } else {
    _tprintf(_T("?\n"), buf);
  }
}

간단하다. 이 함수를 파일을 열 때 먼저 실행해서 입력된 파일명과 대소문자가 수정된 파일명과 비교하기만 하면 된다. 간단하다! 이 방식을 사용하면 패킹 시스템에서 대소문자를 구별 하더라도 큰 문제가 없다. 이미 패킹되어 있지 않은 상태에서도 대소문자를 확인하기 때문에 패킹 했다고 추가로 파일명과 관련된 문제가 발생하지 않는다.

또 유저가 파일명을 툴 등에서 직접 입력하는 경우 위 함수를 사용해 파일명을 정리해 주면 입력에 의한 대소문자 불일치 문제를 줄일 수 있다.

XML 등 유저가 직접 파일명을 입력하는 경우 문제가 될 수 있는데 이 문제는 바로바로 파일명 오류로 드러나기 때문에 사실 큰 문제가 되지 않는다. 오타와 동일한 수준의 문제가 되고 파일명을 복사 & 붙여넣기를 권장하면 더더욱 문제가 되기 어렵다.

결론

대소문자는 가능하면 구별하도록 환경을 조성하면 환경의 복잡도를 낮추는데 좋다. 특히 파일명은 위에서 소개한 함수를 잘 사용하면 큰 어려움 없이 적용할 수 있다. 다만 문자열의 사용 패턴을 잘 살펴서 이 것이 적용될 수 없는 경우에도 억지로 넣게 되면 문제가 될 수 있으니 유의해야 한다. (가령 email 주소를 대소문자 구별하도록 하는 것은 메일 주소를 손으로 입력하는 환경에서는 곤란한 것처럼.)