“사용자의 단말기에서만 ‘파일을 찾을 수 없습니다’가 뜬다”, “복사는 됐는데 그 파일이 열리지 않는다” ── 파일 입출력을 수반하는 업무 앱의 불량 조사에서, 결국 경로의 길이나 파일 이름 자체가 원인이었던 사례는 드물지 않습니다. 사용자는 프로젝트명이나 날짜를 폴더 이름에 채워 넣고 계층을 깊이 파고들어, 이쪽의 예상을 가볍게 뛰어넘습니다.
까다로운 점은, 이 영역의 제한이 “Win32 API의 제한”, “파일 시스템의 제한”, “셸(Explorer)의 제한”, “.NET 런타임의 제한”으로 여러 겹 나뉘어 있어, 어디까지 대응할 수 있고 무엇이 남는지 알기 어렵다는 것입니다. 이 글에서는 MAX_PATH=260자의 정체부터 긴 경로를 합법적으로 다루는 조건, CON 등의 예약어와 끝 점 같은 파일 이름의 함정, 대소문자 처리까지 C#에서의 실무 대응과 함께 정리합니다.
1. 먼저 결론
- MAX_PATH=260은 “드라이브 문자+콜론+백슬래시+최대 256자의 경로 문자열+종료 NUL”을 포함한 Win32 API의 제한입니다. 파일 시스템 쪽(NTFS 등)은 더 긴 경로를 다룰 수 있고, 유니코드 버전 API에
\\?\접두사를 붙이면 합계 약 32,767자까지 지정할 수 있습니다.12 - 260자 제한 해제에는 Windows 10 버전 1607 이후에서 “레지스트리의 LongPathsEnabled=1”과 “앱 매니페스트의 longPathAware” 둘 다 필요합니다. 한쪽만으로는 적용되지 않습니다.3
- .NET(Core)/.NET 5 이후의 런타임은 MAX_PATH 검사를 하지 않고, 긴 경로를 암묵적으로 다룹니다. .NET Framework는 4.6.2 이후를 타깃으로 하면 런타임 쪽의 260자 검사가 풀립니다.45
- 다만 긴 경로를 지원하지 않는 앱은 실제로 남습니다. Win32 API로 만들 수 있는 경로를 셸(Explorer)이 올바르게 해석하지 못할 때가 있다고 공식 문서 자체가 명시하고 있습니다.1
- 파일 이름에는
< > : " / \ | ? *와 제어 문자(0~31)를 사용할 수 없으며, CON·PRN·AUX·NUL·COM1~9·LPT1~9는 확장자를 붙여도(CON.txt여도) 예약어로 취급됩니다.6 - 이름 끝의 공백과 점은 경로 정규화 과정에서 소리 없이 제거됩니다. “hoge.”를 지정한 셈인데 “hoge”로 바뀌어 있다거나, 다른 OS가 만든 끝에 공백이 붙은 파일에 Windows에서 접근할 수 없다는 사고의 원인입니다.67
- Windows의 파일 이름은 “대소문자를 유지하지만 구분하지 않는다”가 기본값입니다. NTFS는 디렉터리 단위의 구분(
fsutil.exe file setCaseSensitiveInfo)도 지원하지만, Windows 앱 쪽이 따라가지 못하는 부작용이 있습니다.89 - 구현 측면에서는, 경로 결합은
Path.Combine의 “루트가 붙은 인수에서 앞선 인수가 버려진다”는 사양에 주의하고(.NET Core 계열이라면Path.Join도 선택지입니다), 사용자 입력 파일 이름은Path.GetInvalidFileNameChars+예약어·끝 문자에 대한 자체 점검으로 새니타이즈합니다.1011
2. MAX_PATH=260의 정체
Win32 API에서 경로의 최대 길이는 일부 예외를 제외하고 MAX_PATH=260자로 정의되어 있습니다. 이 260에는 내역이 있습니다. 로컬 경로는 “드라이브 문자, 콜론, 백슬래시, 백슬래시로 구분된 이름 부분, 종료 NUL 문자”로 구성되며, 예를 들어 D 드라이브라면 “D:\ + 256자 분량의 경로 문자열 + 종료 NUL”이 최대입니다.1
내역을 나누어 보면 다음과 같습니다.
| 위치 | 구성 요소 | 예 | 문자 수 |
|---|---|---|---|
| 앞부분 | 드라이브 문자+콜론+백슬래시 | D:\ |
3 |
| 중간 | 백슬래시로 구분된 이름 부분(폴더 이름·파일 이름과 구분 기호) | 2026\프로젝트\…\보고서.xlsx |
최대 256 |
| 끝부분 | 종료 NUL 문자(화면에는 보이지 않음) | ─ | 1 |
| 합계 | 260 = MAX_PATH |
즉 260자를 “파일 이름에 쓸 수 있는 길이”로 생각하고 있다면, 실제로는 드라이브 표기와 종료 NUL로 4자만큼 줄어들어 있다는 뜻입니다. 더 세밀한 제한으로, 디렉터리 생성 API는 8.3 형식의 파일 이름을 뒤에 덧붙일 여유를 요구하므로, 디렉터리 경로는 MAX_PATH−12자를 넘을 수 없습니다.1
중요한 것은 이것이 Win32 API 계층의 제한이지, 파일 시스템의 한계가 아니라는 점입니다. NTFS는 긴 파일 이름과 확장 경로를 지원하며, 많은 Win32 함수의 유니코드 버전은 합계 약 32,767자의 확장 긴 경로를 받아들입니다. 경로를 구성하는 개별 컴포넌트(폴더 이름·파일 이름 하나)의 상한은 GetVolumeInformation이 반환하는 값이며, 일반적으로 255자입니다.12
이 “API는 260, 파일 시스템은 약 32,767”이라는 간극이야말로 현장 장애의 원천입니다. 어떤 도구에서는 만들 수 있었던 경로가, 다른 도구(또는 자사 앱)에서는 열리지 않는다는 비대칭 상황이 정당하게 발생합니다. git clone으로 깊은 저장소를 이름이 긴 폴더에 전개했더니 빌드가 통과하지 않게 되었다는 것은 공식 문서에도 실려 있는 전형적인 예입니다.1
참고로 기존의 .NET Framework에서는 전체 경로가 260자 이상이 되면 System.IO.PathTooLongException이 발생했습니다. 이 예외를 보면, 먼저 경로 길이를 의심해 보세요.12
3. 260자의 벽을 넘는 방법과 그 조건
긴 경로를 다루는 수단은 크게 두 가지, “\\?\ 접두사”와 “OS의 긴 경로 활성화”입니다.
3.1. \\?\ 접두사
경로 문자열 앞에 \\?\를 붙이면, Win32 API는 문자열 해석을 멈추고 그대로 파일 시스템에 전달합니다. 이를 통해 MAX_PATH 제한을 넘을 수 있습니다(UNC 경로는 \\?\UNC\server\share 형식). 다만 조건과 부작용이 있습니다.16
- 유니코드 버전 API(~W나 .NET처럼 UTF-16으로 호출하는 것)여야 합니다.
- 정규화가 건너뛰어지므로,
/구분이나.·..을 통한 상대 지정은 사용할 수 없습니다. 상대 경로에는\\?\를 붙일 수 없으므로, 상대 경로는 항상 MAX_PATH까지입니다.1 - 모든 API가 지원하는 것은 아니며, 지원 여부는 각 API의 레퍼런스에서 확인해야 합니다.6
3.2. Windows 10 1607 이후의 긴 경로 활성화 ── 조건은 “둘 다”
Windows 10 버전 1607 이후에서는 많은 일반적인 Win32 파일·디렉터리 함수(CreateFileW, FindFirstFileW, GetFileAttributesW 등)에서 MAX_PATH 제한을 없앨 수 있습니다. 다만 앱 쪽의 옵트인이 전제이며, 다음 두 조건을 모두 충족해야 합니다.3
- 레지스트리 값
HKLM\SYSTEM\CurrentControlSet\Control\FileSystem의LongPathsEnabled(REG_DWORD)가 1일 것. 그룹 정책 “컴퓨터 구성 > 관리 템플릿 > 시스템 > 파일 시스템 > Win32 긴 경로 사용”에서도 설정할 수 있습니다. - 애플리케이션 매니페스트에
longPathAware요소가 있을 것.
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings xmlns:ws2="http://schemas.microsoft.com/SMI/2016/WindowsSettings">
<ws2:longPathAware>true</ws2:longPathAware>
</windowsSettings>
</application>
# 레지스트리 쪽(관리자 권한)
New-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem" `
-Name "LongPathsEnabled" -Value 1 -PropertyType DWORD -Force
매니페스트 쪽 절차도 보충해 두겠습니다. 위에서 든 longPathAware의 XML은 공식 문서에 실려 있는 조각으로, 이것만으로는 파일이 되지 않습니다. 실제로는 애플리케이션 매니페스트 파일(관례적으로 app.manifest)의 assembly 요소 안에 넣습니다. Visual Studio라면 “프로젝트 우클릭 > 추가 > 새 항목”에서 “애플리케이션 매니페스트 파일”을 추가하면 UAC 설정 등이 들어간 템플릿이 생성되므로, 그 assembly 요소 안에 application 요소로 덧붙여 씁니다.13
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<!-- 여기에 VS가 생성한 trustInfo 등이 들어 있습니다 -->
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings xmlns:ws2="http://schemas.microsoft.com/SMI/2016/WindowsSettings">
<ws2:longPathAware>true</ws2:longPathAware>
</windowsSettings>
</application>
</assembly>
추가한 항목을 빌드와 연결하는 것은 MSBuild의 ApplicationManifest 속성입니다. Visual Studio에서 항목을 추가하면 보통 자동으로 기록되지만, csproj를 직접 편집하는 경우에는 다음 한 줄을 추가합니다(이 속성이 가리키는 매니페스트는 기본값으로 exe에 내장됩니다).14
<PropertyGroup>
<ApplicationManifest>app.manifest</ApplicationManifest>
</PropertyGroup>
“레지스트리를 설정했는데도 적용되지 않는다”는 상담은 대부분 매니페스트 쪽의 누락입니다. 공식 문서도 “이 레지스트리 설정은 새 기능을 이용하도록 수정된 앱에만 영향을 미친다”고 못 박고 있습니다. 또한 레지스트리 값은 최초의 파일 함수 호출 시 프로세스 단위로 캐시되며, 프로세스가 살아 있는 동안에는 다시 읽어들이지 않습니다. 설정 변경을 모든 앱에 확실히 반영하려면 재부팅이 필요한 경우가 있습니다.3
3.3. .NET에서는 어떻게 되는가
- .NET (Core) / .NET 5 이후: 런타임은 MAX_PATH 검사를 하지 않고, 긴 경로를 암묵적으로 다룹니다. 앱 쪽에서 특별한 코드는 필요하지 않습니다.4
- .NET Framework: 4.6.2 이후를 타깃으로 하면 260자 런타임 검사가 풀리고,
PathTooLongException은 “32,767자 초과” 또는 “OS가 오류를 반환한 경우”에 한정됩니다. 그 이전을 타깃으로 한 기존 앱이라도,Switch.System.IO.BlockLongPaths=false(및 기존 경로 처리를 비활성화하는Switch.System.IO.UseLegacyPathHandling=false)의 AppContext 스위치로 옵트인할 수 있습니다.512 - .NET Framework 앱이 실무에서 긴 경로를 통과시키려면, 위의 런타임 설정에 더해 OS 쪽의 긴 경로 활성화와 매니페스트 병용이 필요합니다. NuGet.exe의 긴 경로 지원 문서가 이 구성(Windows 10 1607 이후+longPathAware 매니페스트+UseLegacyPathHandling 비활성화)을 실제 사례로 명시하고 있습니다.15
3.4. 그래도 남는 “미지원 앱”이라는 현실
여기까지 해도, 세상의 모든 앱이 긴 경로를 다룰 수 있게 되는 것은 아닙니다. 공식 문서는 “셸과 파일 시스템은 요구 사항이 다르다. Win32 API로 만들 수 있는 경로를 셸 UI가 올바르게 해석하지 못할 수 있다”고 명시하고 있으며1, 실제로 긴 경로 지원을 표방하지 않는 도구도 남아 있습니다(예를 들어 NuGet 문서는 Visual Studio나 msbuild -t:restore의 restore가 긴 경로를 지원하지 않는다고 기재하고 있습니다15). 자사 앱이 긴 경로로 파일을 만들 수 있어도, 사용자가 Explorer나 다른 도구로 그것을 열 수 있는지는 별개의 문제입니다. 이 비대칭성을 감안한 설계 판단은 6장의 판단표에 정리합니다.
4. 사용할 수 없는 문자와 예약 디바이스 이름, 끝의 점·공백
경로 길이와 나란한 또 하나의 지뢰밭이 파일 이름 자체의 규칙입니다. 공식 명명 규칙 중에서 업무 앱에서 밟기 쉬운 것을 정리합니다.6
| 분류 | 내용 | 비고 |
|---|---|---|
| 예약 문자 | < > : " / \ \| ? * |
경로 구분자인 \(와 /), 드라이브의 : 포함 |
| 제어 문자 | 정수값 0(NUL)과 1~31 | 대체 데이터 스트림 내부를 제외하고 불가 |
| 예약 디바이스 이름 | CON, PRN, AUX, NUL, COM1~COM9, LPT1~LPT9(및 위첨자 숫자의 COM¹~³, LPT¹~³) | 확장자를 붙여도 불가(NUL.txt나 NUL.tar.gz는 NUL과 동일) |
| 끝의 문자 | 공백 또는 점으로 끝나는 이름 | 파일 시스템은 허용해도 셸과 UI가 미지원 |
4.1. 예약 디바이스 이름 ── CON.txt도 안 된다
CON이나 NUL은 MS-DOS 시절의 디바이스 이름으로, 지금도 NT 네임스페이스의 예약어로 남아 있습니다. 그래서 “CON”이라는 파일은 보통의 방법으로는 만들 수 없고, CON.txt처럼 확장자를 붙여도 예약어로 해석됩니다.6 시리얼 포트 연계 로그를 “COM1.log” 같은 이름으로 저장하려다가 사고가 나거나, Linux 쪽에서 만들어진 “aux”라는 폴더를 Windows에서 압축 해제할 수 없다는 것이 현장에서 드러나는 모습입니다.
보충하자면, 경로 정규화 사양상 종래에는 “CON”, “COM1.TXT”처럼 예약어로 시작하는 경로는 디바이스 경로(\\.\CON)로 변환되어 해석되어 왔습니다. Windows 11에서는 이 해석이 바뀌어, 레거시 디바이스를 가리키려면 \\.\CON처럼 완전한 형식의 지정이 필요해졌습니다.7 다만 공식 문서의 서술은 “Windows 11 이전에는”, “Windows 11에서는 이것이 해당하지 않는다”는 수준이며, 어느 빌드·어느 업데이트부터 전환되었는지는 나와 있지 않습니다.7 혼재 환경에서 영향 범위를 가늠할 때는, 빌드 단위가 아니라 “Windows 10 이전인가, Windows 11 이후인가”까지만 구분할 수 있다고 생각하세요. 그렇다고 해도 오래된 OS와 기존 앱 양쪽 모두 기존 해석 그대로 남아 있으므로, 업무 데이터의 이름으로 예약어를 피해야 한다는 결론은 변하지 않습니다.
4.2. 끝의 공백·점은 “말없이” 사라진다
공식 명명 규칙은 “파일 이름·디렉터리 이름을 공백이나 점으로 끝내지 말 것”이라고 정하고 있습니다.6 한 걸음 더 들어가면, Windows의 경로 정규화에는 “경로가 구분 문자로 끝나지 않는 경우, 끝의 점과 공백(U+0020)을 모두 제거한다”는 명확한 규칙이 있습니다.7
이것이 실무에서 성가신 이유는 오류가 나지 않고 소리 없이 다른 이름이 된다는 데 있습니다. 사용자가 “보고서v2.”라는 이름을 입력하면, 실제로 만들어지는 것은 “보고서v2”입니다. 반대로 SMB를 통해 Linux 쪽에서 만들어진 “report “(끝에 공백)와 같은 파일은, Windows의 일반적인 경로 지정으로는 정규화 과정에서 이름이 바뀌어 버려 도달할 수 없습니다. 이런 “정규화로는 도달할 수 없지만 합법적인 이름”에 접근하는 수단이, 정규화를 건너뛰는 \\?\ 접두사입니다. 공식 문서도 “hidden.과 같은 파일은 다른 방법으로는 접근할 수 없다”고 이 용도를 명시하고 있습니다.7
참고로 앞쪽의 점은 합법입니다(.gitignore와 같은 이름은 문제없이 만들 수 있습니다).6
5. 대소문자는 “유지하지만 구분하지 않는다”
Windows 파일 시스템의 기본 동작은 case-preserving, case-insensitive입니다. Readme.txt라는 이름으로 만들면 그 대소문자가 유지되어 표시되지만, 검색이나 비교에서는 대소문자가 무시되어 README.TXT로도 같은 파일에 도달합니다. 드라이브 문자도 마찬가지로 대소문자를 구분하지 않습니다.86
공식 명명 규칙은 앱 개발자를 대상으로 “대소문자 구분을 가정하지 말라(OSCAR, Oscar, oscar는 같은 이름으로 간주하라)”고 명시하는 한편, NTFS 자체는 POSIX적인 대소문자 구분을 지원한다(다만 기본값으로는 비활성)고도 말하고 있습니다.6
이것이 겉으로 드러나는 곳이 Linux 연계입니다. Windows 10 빌드 17107 이후에서는 디렉터리 단위로 대소문자 구분을 활성화할 수 있습니다.9
# 관리자 권한의 PowerShell에서
fsutil.exe file setCaseSensitiveInfo C:\work\linux-src enable
fsutil.exe file queryCaseSensitiveInfo C:\work\linux-src
WSL에서 Linux 유래 소스 트리(Makefile과 makefile이 공존하는 등)를 다루는 경우에는 유효한 수단이지만, 공식 문서 자체가 경고하는 부작용이 있습니다. 파일 시스템을 대소문자 비구분으로 가정하는 Windows 앱은 구분이 활성화된 디렉터리에서 파일에 접근할 수 없게 될 수 있습니다. 또한 플래그 변경은 대상 디렉터리가 비어 있지 않으면 할 수 없고, 새로 만들어지는 하위 디렉터리는 부모의 설정을 상속합니다.9 역사적으로는 이름이 대소문자만 다른 두 파일이 있으면, Explorer에는 둘 다 표시되는데도 어느 쪽을 선택해도 같은 하나만 열리는 현상도 공식적으로 기록되어 있습니다.9
업무 앱의 설계로서는 “Windows상에서는 대소문자 차이는 같은 이름”으로 취급하는 것이 기본, 다만 Linux로 넘어가는 파일 이름은 대소문자 차이로 인한 충돌을 점검한다가 실무적인 낙착점입니다. Linux 연계에서는 파일 이름 이전에 문자 코드에서도 함정이 있으므로, 「Windows 문자 코드 입문 - Linux 연계에서 일어나는 문자 깨짐」도 함께 확인해 주세요.
6. 업무 앱에서의 실무 ── 경로 결합·새니타이즈·판단표
6.1. 경로 결합은 Path.Combine의 사양을 알고 사용한다
경로 문자열 연결을 +로 작성하는 것은 논외로 치더라도, Path.Combine에도 알아 두어야 할 사양이 있습니다. 두 번째 이후의 인수에 루트가 붙은 경로가 전달되면, 그보다 앞의 인수는 모두 무시됩니다.10
var baseDir = @"C:\App\Data";
// 사용자 입력이 루트가 붙은 형태이면, baseDir는 소리 없이 버려진다
Path.Combine(baseDir, @"C:\Windows\secret.txt"); // → "C:\Windows\secret.txt"
Path.Combine(baseDir, @"\evil.txt"); // → "\evil.txt"(현재 드라이브의 루트)
사용자 입력이나 설정 파일에서 온 문자열을 그대로 두 번째 인수로 전달하면, 의도한 저장 폴더 밖으로 쓰는 취약점이 됩니다. 공식 문서도 이 동작이 민감한 파일에 대한 의도치 않은 접근으로 이어질 수 있다고 주의를 주고 있으며, 대안으로 Path.Join / Path.TryJoin(.NET Framework에는 없습니다)을 들고 있습니다.1016 어느 쪽을 쓰든, 최종적으로는 Path.GetFullPath로 정규화한 결과가 베이스 디렉터리 하위인지를 검증하는 것이 정석입니다.
// 베이스 쪽도 정규화한 뒤, 상대 경로로 변환해 판정한다.
// 문자열의 앞부분 일치보다, 끝 구분자의 유무나 베이스가 드라이브 루트인 경우의 흔들림에 강하다
var baseFull = Path.GetFullPath(baseDir);
var fullPath = Path.GetFullPath(Path.Combine(baseFull, userInput));
var relative = Path.GetRelativePath(baseFull, fullPath);
if (relative == ".." ||
relative.StartsWith(".." + Path.DirectorySeparatorChar) ||
Path.IsPathRooted(relative)) // 다른 드라이브·UNC로 빠져나간 경우는 절대 경로가 반환된다
{
throw new InvalidOperationException("저장 위치가 예상 폴더 밖을 가리키고 있습니다.");
}
참고로 Path.GetRelativePath는 .NET Core 2.0 이후·.NET Standard 2.1 이후·.NET 5 이후의 API로, .NET Framework에는 없습니다.17 Framework 쪽에서는 “베이스를 정규화한 뒤 끝에 구분 문자를 붙이고, 앞부분 일치로 판정하는” 형태로 대체합니다. 끝의 구분 문자가 핵심이며, 이것이 없으면 C:\App\Data에 대한 검사가 C:\App\DataBackup을 하위로 잘못 판정합니다.
// .NET Framework 용 대안(Path.GetRelativePath가 없는 환경)
var baseFull = Path.GetFullPath(baseDir);
if (!baseFull.EndsWith(Path.DirectorySeparatorChar.ToString(), StringComparison.Ordinal))
{
baseFull += Path.DirectorySeparatorChar; // "C:\App\Data"가 "C:\App\DataBackup"에 앞부분 일치하지 않도록 한다
}
var fullPath = Path.GetFullPath(Path.Combine(baseFull, userInput));
if (!fullPath.StartsWith(baseFull, StringComparison.OrdinalIgnoreCase)) // Windows의 기본값에 맞춰 대소문자는 무시
{
throw new InvalidOperationException("저장 위치가 예상 폴더 밖을 가리키고 있습니다.");
}
Path.GetRelativePath는 OS 기본 방식으로 경로를 비교합니다. 즉 Windows에서는 대소문자를 구분하지 않는다는 전제의 판정이며, 이는 다음 장에서 설명한 “Windows의 기본값은 대소문자를 구분하지 않는다”는 동작에 부합합니다. 뒤집어 말하면, 디렉터리 단위로 대소문자 구분을 활성화한 위치(다음 장 참조)에서는 Data와 data가 서로 다른 디렉터리가 될 수 있으므로, 대소문자를 무시한 판정으로는 “대소문자만 다른 별개의 폴더”를 하위로 잘못 판정할 여지가 생깁니다. 그런 구성을 다룰 가능성이 있다면, 구분이 활성화된 위치를 베이스 디렉터리로 받아들이지 않는 방침으로 하는 것이 안전합니다.
한 가지 더, 이 판정은 문자열로서 정규화한 경로에 대한 판정일 뿐이라는 점도 염두에 두세요. 베이스 디렉터리 하위에 정션이나 심볼릭 링크가 존재하면, 문자열상으로는 하위를 가리키고 있어도 실체는 베이스 밖이라는 일이 일어날 수 있습니다. 게다가 링크는 말단 파일뿐 아니라 중간 폴더에도 끼어들 수 있으므로(베이스\링크\파일.txt 형태), 말단만 File.ResolveLinkTarget으로 조사해도 알아낼 수 없습니다. 먼저 신뢰할 수 없는 사용자가 베이스 하위에 링크나 정션을 만들 수 있는 구성을 애초에 피하는 것이 첫걸음입니다. 그 위에서 엄격히 지켜야 한다면, 실제로 파일을 연 핸들에서 확정 경로를 얻어(Win32의 GetFinalPathNameByHandle) 그것이 베이스 하위인지 검증하거나, 경로의 각 폴더 구성 요소를 순서대로 링크가 아닌지 검사하세요.
6.2. 사용자 입력 파일 이름 새니타이즈
“거래처명+날짜.csv”처럼 사용자 입력으로부터 파일 이름을 조립하는 기능에서는, 새니타이즈를 한 곳에 모읍니다. Path.GetInvalidFileNameChars가 출발점이지만, 이 배열은 부적합 문자의 완전한 집합을 보장하지 않는다고 공식적으로 명시되어 있습니다.11 예약 디바이스 이름과 끝의 점·공백은 이 API로는 검출할 수 없으므로, 자체 점검을 추가합니다.
private static readonly HashSet<string> ReservedNames =
new(StringComparer.OrdinalIgnoreCase)
{
"CON", "PRN", "AUX", "NUL",
"COM1","COM2","COM3","COM4","COM5","COM6","COM7","COM8","COM9",
"LPT1","LPT2","LPT3","LPT4","LPT5","LPT6","LPT7","LPT8","LPT9",
"COM¹","COM²","COM³", // 위첨자 숫자인 COM¹~COM³도 예약어
"LPT¹","LPT²","LPT³", // 마찬가지로 LPT¹~LPT³
};
public static string SanitizeFileName(string input)
{
var invalid = Path.GetInvalidFileNameChars();
var name = new string(input.Select(c => invalid.Contains(c) ? '_' : c).ToArray());
name = name.TrimEnd(' ', '.'); // 끝의 공백·점은 소리 없이 사라지므로 제거
// 파일 이름 1개 요소의 길이 상한(보통 255자)에도 맞춘다.
// 폴더 계층이나 앱이 나중에 붙이는 접미사의 여유를 남기고 보수적으로 자른다
const int MaxNameLength = 120;
if (name.Length > MaxNameLength)
{
var ext = Path.GetExtension(name);
if (ext.Length > 20)
{
ext = ""; // 비정상적으로 긴 "확장자"는 확장자로 보존하지 않는다(음수 범위 지정으로 예외가 나는 것을 방지)
}
name = name[..(MaxNameLength - ext.Length)].TrimEnd(' ', '.') + ext;
}
// 빈 문자열·예약어 판정은 반드시 "최종 형태"에 대해 수행한다.
// 자르기나 TrimEnd의 결과 빈 문자열이나 예약어(NUL 등)로 바뀌는 경우를 잡아내기 위해
var stem = name.Split('.')[0]; // NUL.txt 대책: 확장자 앞부분으로 예약어를 판정
if (name.Length == 0 || ReservedNames.Contains(stem))
{
name = "_" + name; // 1글자를 더해도 상한 255자에는 충분히 들어간다
}
return name;
}
이 함수의 의도는 입출력 대응표로 만들면 전달하기 쉬우므로, 단위 테스트의 첫 케이스로 그대로 쓸 수 있는 형태로 정리해 둡니다.
| 입력 | 출력 | 작동하는 처리 |
|---|---|---|
보고서v2. |
보고서v2 |
끝 점을 TrimEnd로 제거(정규화로 소리 없이 사라지는 것을 미리 막는다) |
CON.txt |
_CON.txt |
확장자를 뗀 CON이 예약어. Split('.')[0]으로 판정해 접두사를 붙인다 |
nul.tar.gz |
_nul.tar.gz |
예약어 판정은 OrdinalIgnoreCase. 이중 확장자여도 첫 요소를 본다 |
COM1의 끝에 공백 1개 |
_COM1 |
TrimEnd의 결과가 예약어로 바뀐다. 그래서 판정은 최종 형태에 대해 수행한다 |
... |
_ |
TrimEnd로 빈 문자열이 된 경우. 빈 파일 이름을 반환하지 않는다 |
A/B:C.csv |
A_B_C.csv |
GetInvalidFileNameChars에 포함된 /와 :를 _로 치환 |
150자+.csv |
앞 116자+.csv(총 120자) |
길이 상한으로 자르고, 확장자는 보존한다 |
4번째 줄과 5번째 줄이, 코드에서 “판정은 반드시 최종 형태에 대해 수행한다”고 주석을 단 이유 그 자체입니다. 치환·자르기·TrimEnd를 먼저 마친 뒤에 예약어와 빈 문자열을 보지 않으면, 끝에 공백이 붙은 COM1과 같은 입력이 점검을 빠져나갑니다.
CSV 출력 파일 이름에서 이 문제를 만나는 일이 많으므로, CSV 자체의 실무는 「CSV는 “그냥 텍스트”가 아니다 ── C# 업무 앱의 CSV 실무」도 참고해 주세요.
6.3. 상대 경로와 현재 디렉터리의 함정
상대 경로에는 두 가지 함정이 있습니다. 첫째, 현재 디렉터리는 프로세스 단위의 설정이므로, 어느 스레드에서든 언제든지 변경될 수 있습니다. 공식 문서는 “상대 경로는 멀티스레드 앱에서는 위험하다”고까지 적고 있으며, .NET Core 2.1 이후라면 기준 경로를 명시할 수 있는 Path.GetFullPath(string, string)을 쓸 수 있습니다.7 둘째, C:tmp.txt처럼 드라이브 문자 바로 뒤에 백슬래시가 없는 형식은 “C 드라이브의 현재 디렉터리로부터의 상대 경로”이며, 절대 경로가 아닙니다. 이 “드라이브 상대 경로”는 프로그램이나 스크립트의 단골 버그 원인으로 공식적으로 지목되어 있습니다.7
설정 파일이나 사용자 입력으로 받은 경로는, 받은 시점에 Path.GetFullPath로 절대 경로화한 뒤 로그에 남기고 검증하는 것을 습관으로 삼으세요.
6.4. 판단표 ── 긴 경로를 지원해야 하는가, 입구에서 막아야 하는가
| 상황 | 권장 | 이유 |
|---|---|---|
| 사용자가 저장 위치를 자유롭게 선택하는 일반 업무 앱 | 입구에서 검증해 막는다(전체 경로 길이·파일 이름을 저장 전에 확인하고 명확한 오류를 낸다) | 자사 앱이 대응해도 Explorer나 연계 도구가 열지 못하는 사고가 남기 때문1 |
| 백업·동기화·아카이브 전개 등, 다른 사람이 만든 깊은 계층을 “읽는” 쪽 | 긴 경로를 지원한다(.NET Core 계열+필요에 따라 매니페스트, Framework라면 4.6.2+설정) | 입력을 통제할 수 없고, 읽지 못하면 업무가 멈추기 때문54 |
| 자사 앱이 깊은 계층을 “만드는” 쪽 | 원칙적으로 만들지 않는 설계로 재검토한다(계층 평탄화, 해시 이름 채택 등) | 만든 경로의 이용자(사람·다른 앱)가 미지원일 가능성이 높기 때문1 |
| Linux/WSL과 파일을 주고받는다 | 예약어·대소문자 충돌·끝 문자를 전송 전에 점검 | Windows 쪽에서 도달 불가능한 파일이 생기기 때문69 |
| 사용자 입력으로부터 파일 이름을 생성한다 | GetInvalidFileNameChars+예약어·끝 문자 새니타이즈를 공통 함수에 모은다 |
API의 배열만으로는 불완전하기 때문11 |
7. 문제 해결 ── “탐색기에서는 보이는데 열리지 않는다”
“Explorer에는 파일이 보이는데, 앱에서 열면 ‘파일을 찾을 수 없습니다’“라는 상담의 원인 분리 절차입니다.
| 확인 포인트 | 수단 | 해당하면 |
|---|---|---|
| 전체 경로가 260자 근방인가 | PowerShell로 (Get-ChildItem -Recurse).FullName \| Where-Object { $_.Length -ge 250 } |
상위 폴더 이름을 줄이거나, 긴 경로 대응(3장)을 검토 |
| 파일 이름이 예약어인가(aux, con, com1 등) | 이름을 육안으로 확인. 확장자가 붙은 것도 대상6 | 이름 바꾸기(작성처가 Linux 등이라면 전송 시 변환) |
| 끝에 공백·점이 없는가 | cmd /c dir /x나 따옴표가 붙은 표시로 확인 |
\\?\ 접두사가 붙은 경로로 삭제·이름 바꾸기7 |
| 대소문자만 다른 같은 이름의 파일이 없는가 | WSL/Git 유래 폴더에서 발생하기 쉬움9 | 한쪽의 이름을 바꾸거나, 대상 디렉터리의 용도를 재검토 |
| 상대 경로·드라이브 상대 경로를 쓰고 있지 않은가 | 로그에 실제로 열려고 한 경로를 절대 경로로 기록 | Path.GetFullPath로 절대화한 뒤 사용7 |
표 안에서 쓰고 있는 dir /x의 읽는 법만 보충하겠습니다. /x는 “8.3 형식에 들어가지 않는 이름에 대해 생성된 단축 이름을 표시한다”는 옵션으로, 표시 형식은 /n(이름이 오른쪽 끝에 오는 형태)과 같고, 그 사이에 단축 이름 칸이 긴 이름 앞에 끼워집니다.18 즉 “날짜 시각 크기 ─ 단축 이름 ─ 긴 이름”의 순서이며, 오른쪽 끝이 원래 이름, 그 왼쪽 옆이 단축 이름(T97B4~1.TXT와 같은 형태)입니다. 이름이 처음부터 8.3 형식에 들어가는 파일에는 단축 이름이 생성되지 않으므로, 그 칸은 비어 있게 됩니다. 경로 길이가 상한에 걸려 열리지 않을 때, 도중의 폴더를 이 단축 이름으로 지정해 경로를 줄이는 응급 처치로도 쓸 수 있습니다.
조사의 첫걸음으로 권장하는 것은, 앱의 오류 로그에 “열려고 한 경로 그 자체”를 절대 경로·따옴표를 붙여 기록하는 것입니다. “파일을 찾을 수 없습니다”라는 예외 메시지만으로는, 경로가 잘렸는지, 정규화로 이름이 바뀌었는지, 애초에 다른 디렉터리를 보고 있었는지를 나중에 구별할 수 없습니다. 따옴표로 감싸서 기록해 두면, 끝 공백처럼 눈으로 확인하기 어려운 문제도 한눈에 알 수 있습니다.
참고로 DLL 로드 실패도 “파일을 찾을 수 없습니다” 계열 오류의 단골이지만, 이쪽은 경로 길이보다 검색 순서 문제인 경우가 많으며, 「Windows DLL 이름 해결의 구조 - 검색 순서와 SxS」에서 정리했습니다.
8. 정리
- MAX_PATH=260은 “
D:\+최대 256자+종료 NUL”을 포함하는 Win32 API의 제한이며, NTFS 자체는 약 32,767자의 확장 긴 경로를 다룰 수 있습니다. 디렉터리는 추가로 MAX_PATH−12까지라는 제한도 있습니다. - 260자를 넘으려면
\\?\접두사(유니코드 버전 API 한정·상대 경로 불가)나, Windows 10 1607 이후의 긴 경로 활성화(레지스트리LongPathsEnabled+매니페스트longPathAware를 둘 다)가 필요합니다. - .NET (Core)/5+는 긴 경로를 암묵적으로 다루고, .NET Framework는 4.6.2 이후 타깃에서 런타임 검사가 풀립니다. 다만 Explorer를 포함한 미지원 앱이 남으므로, “만들 수 있는가”와 “사용자가 다룰 수 있는가”를 나누어 판단합니다.
- 파일 이름은 예약 문자(
< > : " / \ | ? *)와 제어 문자가 불가하고, CON·NUL·COM1 등의 예약 디바이스 이름은 확장자를 붙여도 불가하며, 끝의 공백·점은 정규화로 소리 없이 사라집니다. - 대소문자는 “유지하지만 구분하지 않는다”가 기본값입니다.
fsutil file setCaseSensitiveInfo에 의한 디렉터리 단위의 구분은 WSL 연계에서는 유효하지만, Windows 앱 쪽의 오작동 위험과 맞바꾸는 것입니다. - 구현은
Path.Combine의 루트가 붙은 인수 사양을 감안한 베이스 디렉터리 검증,GetInvalidFileNameChars+예약어·끝 문자 점검의 새니타이즈 공통화, 상대 경로 배제(Path.GetFullPath를 통한 절대화)가 정석입니다.
관련 글
- CSV는 “그냥 텍스트”가 아니다 ── C# 업무 앱의 CSV 실무(문자 코드・Excel 호환・인젝션 대책)
- Windows의 문자 코드를 정리한다 - 왜 문자 깨짐이 일어나는가, 특히 Linux와 조합했을 때 무엇이 어긋나는가
- Windows의 문자 코드와 개행 코드를 정리한다 - Shift_JIS / UTF-8 / UTF-16, 문자 깨짐, CRLF / LF, 왜 혼란스러운가
- Windows에서 DLL 이름 해결의 메커니즘 - 검색 순서, Known DLLs, API set, SxS를 실무용으로 정리
관련 상담 영역
합동회사 코무라소프트에서는 “특정 환경·특정 파일만 열리지 않는다”와 같은 파일 I/O에서 비롯된 불량 조사, 기존 업무 앱의 긴 경로 대응이나 파일 이름 유효성 검사 설계의 재검토, Windows·Linux 혼재 환경에서의 파일 연계 설계 상담을 다루고 있습니다.
참고 링크
-
Microsoft Learn, Maximum Path Length Limitation. MAX_PATH=260의 정의와 “드라이브 문자+콜론+백슬래시+256자+종료 NUL”이라는 내역, 유니코드 버전 API와
\\?\접두사에 의한 약 32,767자의 확장 긴 경로, 컴포넌트 길이(일반적으로 255자), 상대 경로가 항상 MAX_PATH로 제한된다는 점, 디렉터리 생성이 MAX_PATH−12까지라는 점, 셸과 파일 시스템의 요구 사항이 달라 Win32로 만들 수 있는 경로를 셸 UI가 해석하지 못하는 경우가 있다는 점에 대해. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11 -
Microsoft Learn, NTFS overview. NTFS가 긴 파일 이름과 약 32,767자의 확장 긴 경로를 지원한다는 점, 8.3 별칭에 의한 하위 호환에 대해. ↩ ↩2
-
Microsoft Learn, Maximum Path Length Limitation ── Enable long paths in Windows 10, version 1607, and later. Windows 10 1607 이후에서 레지스트리 값 LongPathsEnabled=1과 앱 매니페스트의 longPathAware 요소가 둘 다 필요하다는 점, 그룹 정책을 통한 설정, 레지스트리 값이 프로세스 단위로 캐시된다는 점, 제한이 풀리는 Win32 함수 목록에 대해. ↩ ↩2 ↩3
-
Microsoft Learn, File path formats on Windows systems ── Skip normalization. .NET Core 및 .NET 5 이후가 긴 경로를 암묵적으로 처리하며 MAX_PATH 검사를 하지 않는다는 점(MAX_PATH 검사는 .NET Framework에서만 이루어짐),
\\?\가 정규화를 건너뛰는 구조라는 점에 대해. ↩ ↩2 ↩3 -
Microsoft Learn, Retargeting changes for migration to .NET Framework 4.6.x. .NET Framework 4.6.2 타깃에서 긴 경로(최대 32K자)가 지원되어 260자 제한이 제거되었다는 점, 이전 타깃의 앱이 Switch.System.IO.BlockLongPaths=false로 옵트인할 수 있다는 점에 대해. ↩ ↩2 ↩3
-
Microsoft Learn, Naming Files, Paths, and Namespaces. 예약 문자(
< > : " / \ | ? *)와 제어 문자(0~31), 예약 디바이스 이름(CON/PRN/AUX/NUL/COM1~9/LPT1~9 및 위첨자 숫자), NUL.txt처럼 확장자가 붙은 것도 예약어와 동일하다는 점, 이름을 끝의 공백·점으로 끝내지 말 것, 앞쪽의 점이 합법이라는 점, 대소문자 구분을 가정해서는 안 된다는 점과 NTFS의 POSIX 시맨틱스,\\?\접두사의 동작과 유니코드 API 요구 사항에 대해. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11 ↩12 -
Microsoft Learn, File path formats on Windows systems ── Path normalization. 경로 정규화로 끝의 점과 공백이 제거된다는 점,
hidden.과 같은 이름은\\?\로만 접근할 수 있다는 점, CON 등 레거시 디바이스 이름의 해석과 Windows 11에서의 변경, 드라이브 상대 경로(C:tmp.txt)가 버그의 흔한 원인이라는 점, 현재 디렉터리가 프로세스 단위이며 상대 경로가 멀티스레드에서 위험하다는 점, Path.GetFullPath(String, String)에 대해. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 -
Microsoft Learn, File path formats on Windows systems ── Case and the Windows file system. 디렉터리 이름·파일 이름이 생성 시의 대소문자를 유지하는 한편, 이름 비교는 대소문자를 구분하지 않는다는 점에 대해. ↩ ↩2
-
Microsoft Learn, Adjust case sensitivity. Windows 10 빌드 17107 이후의 디렉터리 단위 대소문자 구분(fsutil.exe file setCaseSensitiveInfo), 변경에는 관리자 권한과 빈 디렉터리가 필요하다는 점, 새로 만들어지는 하위 디렉터리가 설정을 상속한다는 점, 대소문자 비구분을 가정하는 Windows 앱이 오작동할 수 있다는 경고, 대소문자만 다른 두 파일은 Explorer에는 둘 다 표시되어도 한쪽만 열렸다는 점에 대해. ↩ ↩2 ↩3 ↩4 ↩5 ↩6
-
Microsoft Learn, Path.Combine Method. 첫 번째 이외의 인수에 루트가 붙은 경로가 포함되면, 그보다 앞의 경로 요소가 무시되고 루트가 붙은 요소부터 시작하는 문자열이 반환된다는 점, 민감한 파일에 대한 의도치 않은 접근으로 이어질 수 있다는 점, 대안으로 Join/TryJoin(.NET Framework에서는 이용 불가)이 제시되어 있다는 점에 대해. ↩ ↩2 ↩3
-
Microsoft Learn, Path.GetInvalidFileNameChars Method. 파일 이름에 쓸 수 없는 문자의 배열을 반환한다는 점, 반환되는 배열이 부적합 문자의 완전한 집합을 보장하지 않으며 파일 시스템에 따라 다를 수 있다는 점에 대해. ↩ ↩2 ↩3
-
Microsoft Learn, PathTooLongException Class. 경로가 시스템에서 정의한 최대 길이를 넘었을 때 발생하는 예외라는 점, .NET Framework 4.6.2 이후에서는 “32,767자 초과” 또는 “OS가 오류를 반환한 경우”에 한해 발생한다는 점에 대해. ↩ ↩2
-
Microsoft Learn, Application Manifests. 애플리케이션 매니페스트가 루트 요소 assembly를 갖는 XML이라는 점, application 요소와 windowsSettings에 의한 런타임 설정 선언에 대해. ↩
-
Microsoft Learn, Common MSBuild Project Properties. ApplicationManifest 속성이 매니페스트 파일의 경로를 지정하는 것이며, 대부분의 경우 매니페스트는 실행 파일에 내장된다는 점에 대해. ↩
-
Microsoft Learn, Long Path Support (NuGet CLI). .NET Framework 기반 도구가 긴 경로를 쓰기 위한 실제 구성(Windows 10 1607 이후 또는 1511+.NET Framework 4.6.2, Win32 long paths 정책, longPathAware 매니페스트+UseLegacyPathHandling 비활성화)과, Visual Studio나 msbuild의 restore가 긴 경로를 지원하지 않는다는 점에 대해. ↩ ↩2
-
Microsoft Learn, Path.Join Method. Join이 루트가 붙은 후속 경로를 버리지 않고 연결한다는 점, Combine과의 동작 차이 예시에 대해. ↩
-
Microsoft Learn, Path.GetRelativePath Method. 지원 버전(.NET Core 2.0 이후, .NET Standard 2.1, .NET 5 이후. .NET Framework는 포함되지 않음)과, 비교에 플랫폼 기본 방식(Windows와 macOS는 OrdinalIgnoreCase, Linux는 Ordinal)을 사용한다는 점에 대해. ↩
-
Microsoft Learn, dir. /x 옵션이 8.3 형식이 아닌 이름에 대해 생성된 단축 이름을 표시한다는 점, 표시 형식은 /n과 같으며 단축 이름이 긴 이름 앞에 삽입된다는 점에 대해. ↩
관련 기사
같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.
슬립・최대 절전 모드・Modern Standby와 장시간 가동 앱 ── '한밤중에 멈춰 있었다'를 설계로 막는다
장시간 계속 동작하는 Windows 앱이 '아침에 보니 멈춰 있었다'가 되는 원인을, S3 슬립/최대 절전 모드/Modern Standby의 차이에서 정리합니다. 슬립 중 타이머와 TCP 연결의 동작, SetThreadExecutionState에...
네트워크 드라이브와 UNC 경로의 함정 ── 업무 앱에서 파일 서버(공유 폴더)를 다루는 실무
업무 앱에서 공유 폴더로 출력・감시할 때 자주 발생하는 문제를 정리합니다. 드라이브 문자(Z:)가 서비스에서 보이지 않는 이유, 실행 계정별로 필요한 권한, 에러 1219, FileSystemWatcher의 주의점까지 해설합니다.
C#에서 Win32 API를 안전하게 호출하기 — P/Invoke 실무 가이드(DllImport / LibraryImport / CsWin32)
C#에서 P/Invoke로 Win32 API나 네이티브 DLL을 호출할 때의 실무 포인트를 정리합니다. DllImport와 LibraryImport의 차이, CsWin32를 이용한 시그니처 자동 생성, 문자열 마샬링의 함정, SafeHandle을...
자사 개발 Windows 앱이 바이러스로 오탐되었다면 ── Microsoft Defender 오탐 대응과 성능 영향에 대처하는 법
자사 개발 Windows 앱이 Microsoft Defender에 오탐되었을 때의 정규 대응 방법을 정리합니다. 현대 백신 소프트웨어의 구조, Microsoft에 대한 오탐 신고, 격리로부터의 복원, 제외 설정을 올바르게 넣는 방법과 그 위험까지...
Arm판 Windows에서 업무 앱은 동작하는가 ── x64 에뮬레이션(Prism)과 네이티브 DLL·COM의 현실
「Arm판 Windows에서 업무 앱은 동작하는가」에 개발자·정보시스템 담당자를 위해 답합니다. x64 에뮬레이션(Prism)의 구조, 드라이버 등 동작하지 않는 계층, .NET의 AnyCPU와 P/Invoke 문제, Arm 대응 체크리스트까지 ...
관련 토픽
이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.
Windows 기술 토픽
Windows 개발, 장애 조사, 기존 자산 활용에 관한 KomuraSoft LLC 기사를 모은 토픽 허브입니다.
이 주제와 연결되는 서비스
이 기사는 다음 서비스 페이지로 이어집니다. 가까운 입구부터 확인해 주세요.
Windows 앱 개발
상주 처리, 장비 연동, 운영 로그, 유지 보수 가능한 구조가 필요한 Windows 데스크톱 애플리케이션을 지원합니다.
기술 상담 & 설계 리뷰
설계 방향, 아키텍처 경계, 수명 관리, 기존 Windows 자산 처리 방법을 정리하는 데 도움을 드립니다.
자주 묻는 질문
이 기사 주제에 대해 상담 시 자주 나오는 질문을 모았습니다.
- Windows 경로는 몇 글자까지 사용할 수 있나요?
- Win32 API의 기본값은 MAX_PATH=260자이며, 이는 "드라이브 문자+콜론+백슬래시+256자 분량의 경로 문자열+종료 NUL"을 포함한 길이입니다. NTFS 등의 파일 시스템 자체는 더 긴 경로를 다룰 수 있고, 유니코드 버전 API에 \\?\ 접두사가 붙은 경로를 전달하면 합계 약 32,767자까지 지정할 수 있습니다. 다만 폴더 이름·파일 이름 하나(컴포넌트)는 일반적으로 255자까지이며, 상대 경로는 항상 MAX_PATH까지로 제한됩니다.
- MAX_PATH의 260자 제한은 어떻게 해제할 수 있나요?
- Windows 10 버전 1607 이후에서 레지스트리 값 LongPathsEnabled=1(또는 그룹 정책의 "Win32 긴 경로 사용")과 애플리케이션 매니페스트의 longPathAware 요소를 모두 설정하면, 많은 Win32 파일 함수에서 260자 제한이 풀립니다. 한쪽만으로는 적용되지 않습니다. .NET(Core)/.NET 5 이후의 런타임은 MAX_PATH 검사를 하지 않고 긴 경로를 암묵적으로 다루며, .NET Framework는 4.6.2 이후를 타깃으로 하면 런타임 쪽의 260자 검사가 풀립니다. 다만 Explorer를 포함해 긴 경로를 지원하지 않는 앱은 남기 때문에, 만든 긴 경로를 누가 다루는지까지 포함해 판단이 필요합니다.
- CON이나 NUL이라는 이름의 파일을 만들 수 없는 이유는 무엇인가요?
- CON, PRN, AUX, NUL, COM1~COM9, LPT1~LPT9 등은 MS-DOS 시절부터 이어지는 디바이스의 예약어이며, Windows는 이 이름들을 파일이 아니라 디바이스로 해석하기 때문입니다. NUL.txt처럼 확장자를 붙여도 NUL과 동일하게 취급되므로 피할 수 없습니다. Windows 11에서는 경로 해석 동작이 일부 바뀌었지만, 오래된 OS나 다수의 앱이 기존 해석 그대로이므로 업무 데이터의 파일 이름으로는 계속 피하는 것이 안전합니다.
- 파일 이름의 대소문자는 Windows에서 구분되나요?
- 기본값은 "유지하지만 구분하지 않는다"(case-preserving, case-insensitive)입니다. Readme.txt라는 이름으로 만들면 표시상으로는 그 대소문자가 유지되지만, README.TXT를 열려고 해도 같은 파일에 도달합니다. NTFS는 POSIX적인 대소문자 구분도 지원하며, Windows 10 빌드 17107 이후에서는 fsutil.exe file setCaseSensitiveInfo로 디렉터리 단위의 구분을 활성화할 수 있지만, 대소문자를 구분하지 않는다는 전제의 Windows 앱이 오작동하는 부작용이 있으므로 WSL 연계 등 필요한 상황에 한정해야 합니다.