지금까지 4회에 걸쳐 I/O 요청이 어떻게 흐르는지(제1~3회), 캐시가 이를 어떻게 받아들이는지(제4회)를 살펴봤습니다. 요청은 마지막으로 파일 시스템에 도달합니다. 이번에는 그 대표 격인 NTFS 차례입니다.
관점이 바뀝니다. 지금까지는 ‘요청의 흐름’이라는 동적인 이야기였습니다. 이번에는 디스크 위에 데이터가 어떻게 놓이는가라는 정적인 구조의 이야기입니다. 다운로드한 파일에 붙는 보이지 않는 ‘Zone.Identifier’의 정체. 작은 파일 1만 개를 복사하는 것이, 같은 총 용량의 파일 1개를 복사하는 것보다 훨씬 느린 이유. ‘NTFS는 저널링이니까 안심’이라는 말이 어디까지 사실인지──모두 이 구조로부터 설명할 수 있습니다.
연재 「Windows I/O의 심층」의 제5회입니다.
이번 회를 읽기 위한 전제: 제1회에서 다룬 IRP와 디바이스 스택의 기본을 알고 있으면 읽기 쉬워집니다. 다만 단독으로 읽어도 어려움이 없도록, 본문에 등장하는 지난 회까지의 용어를 먼저 정의해 둡니다.
| 용어 | 한 줄로 말하면 | 자세히는 |
|---|---|---|
| IRP(I/O Request Packet) | ReadFile 같은 API 호출이 커널 내부에서 변환되는 ‘I/O 요청 전표’. 드라이버는 이 전표를 받아 처리한다 |
제1회 |
| I/O 관리자와 디바이스 스택 | IRP를 만들고, 목적 디바이스에 이를 때까지 쌓여 있는 드라이버(스택)로 차례로 전달하는 커널의 구성 요소와 그 쌓임 | 제1회 |
| 캐시 관리자 | 파일 내용을 메모리에 유지하고, WriteFile의 내용을 나중에 한꺼번에 디스크로 내보내는 구성 요소. ‘쓴 직후에 디스크에 도달해 있다고는 할 수 없다’의 원흉 |
제4회 |
표 1: 이번 회에서 전제로 삼는 지난 회까지의 용어
하나 더, 제1회에서 다룬 cleanup(마지막 핸들이 닫혔을 때)과 close(커널 내부의 참조가 모두 사라졌을 때)라는 두 단계도, 4장의 파일 삭제 설명에서 사용합니다.
1. 먼저 결론
- NTFS의 중심은 MFT(마스터 파일 테이블)입니다. 모든 파일이 MFT 안의 레코드로 대장 관리되며, 파일에 관한 모든 정보는 ‘MFT 엔트리 안’ 또는 ‘엔트리가 가리키는 MFT 밖의 영역’에 있습니다(2장).1
- 파일의 실체는 ‘속성의 집합’입니다. 작은 파일은 데이터 본체까지 MFT 레코드 안에 들어가고(상주), 큰 파일은 클러스터 열에 대한 참조만을 가집니다(비상주). 소규모 파일을 대량으로 처리할 때의 느림은 여기서 설명할 수 있습니다(2장).1
- 데이터는 여러 개 가질 수 있습니다(복수 데이터 스트림). 평소의 데이터는 ‘이름 없는 스트림’이며,
file.txt:이름형태로 추가 스트림을 가질 수 있습니다. Zone.Identifier(Mark of the Web)의 정체입니다(3장).2 - 이름도 속성입니다. 같은 레코드에 여러 개의 이름을 붙인 것이 하드 링크입니다. 8.3 단축 이름도 ‘또 하나의 이름’으로 함께 존재합니다(4장).34
- 리파스 포인트는 ‘열면 다른 위치로’를 구현하는 공식 장치입니다. 심볼릭 링크, 정션, OneDrive의 파일 온디맨드는 모두 이 태그 붙은 데이터의 응용입니다(5장).56
- 저널은 두 가지입니다.
$LogFile은 메타데이터 정합성 회복용(손상되지 않기 위한 선행 로그)이고, USN 저널은 변경 이력 기록용(무엇이 바뀌었는지 알려주는 대장)입니다. 역할이 완전히 다릅니다(6장).78 - ‘크기’와 ‘디스크상 크기’는 별개입니다. 스파스 파일과 압축이 그 차이를 만들어 냅니다. 제2회에서 살펴본 ‘압축 파일은 비동기가 되지 않는다’는 이야기의 배경도 여기에 있습니다(7장).910
2. 모든 것은 MFT의 레코드다
2.1. 볼륨의 대장
NTFS 볼륨을 포맷하면 MFT(master file table)와 $로 시작하는 일련의 메타데이터 파일이 만들어집니다. MFT에는 볼륨상의 모든 파일에 대해 최소 하나의 엔트리가 있으며, MFT 자신의 엔트리도 포함됩니다.1
flowchart TB
subgraph VOL["NTFS 볼륨"]
MFT["$MFT ── 마스터 파일 테이블<br/>모든 파일 레코드의 대장, 자기 자신도 포함"]
LOG["$LogFile ── 메타데이터 작업의<br/>트랜잭션 로그, 6장"]
BITMAP["$Bitmap ── 클러스터 사용 현황"]
OTH["$Boot / $Secure / $UpCase 등<br/>기타 메타데이터 파일"]
DATA["사용자 데이터 영역<br/>비상주 데이터가 놓이는 곳"]
end
MFT -->|"레코드가 위치를 가리킴"| DATA
그림 1: NTFS 볼륨의 구조. ‘파일 시스템 자신의 관리 정보도 파일로 가진다’는 것이 NTFS의 설계
파일에 관한 정보──크기, 타임스탬프, 접근 권한, 그리고 데이터 내용까지──는 MFT 엔트리 안에 저장되거나, MFT 엔트리가 위치를 기술하는 MFT 밖의 영역에 저장됩니다.1 파일을 삭제하면 엔트리는 ‘빈 자리’로 표시되어 재사용되지만, MFT 자체는 줄어들지 않습니다. 또한 MFT를 연속적으로 유지하기 위해 MFT 존(zone)이라는 영역이 예약되어 있으며, 볼륨이 채워지면 MFT의 단편화가 시작된다──는 수명에 관한 이야기까지 공식 문서에 적혀 있습니다.1
2.2. 파일 = 속성의 집합, 상주와 비상주
파일 레코드의 내용은 속성의 목록입니다. 표준 정보(타임스탬프 등), 파일 이름, 보안, 그리고 데이터. 여기에 중요한 갈림길이 있습니다.
flowchart TB
subgraph REC["MFT 파일 레코드, 파일 1개분의 대장"]
STD["표준 정보 속성<br/>타임스탬프・속성 플래그"]
FN["파일 이름 속성<br/>여러 개 가질 수 있음 ── 4장"]
DATA["데이터 속성"]
end
Q{"데이터가 작은가"}
RES["상주 resident<br/>데이터 본체가 레코드 안에 들어감<br/>읽기는 MFT 접근만으로 완결"]
NONRES["비상주 non-resident<br/>레코드에는 '클러스터 열에 대한 참조'만<br/>실제 데이터는 사용자 데이터 영역에"]
DATA --> Q
Q -->|"수백 바이트 정도까지"| RES
Q -->|"그 이상"| NONRES
그림 2: 파일 레코드는 속성의 집합입니다. 데이터가 작으면 레코드 안에 ‘상주’합니다
같은 레코드의 내용이 상주와 비상주에서 어떻게 달라지는지 나란히 놓고 보면 이해하기 쉬워집니다.
flowchart LR
subgraph RES2["상주 resident ── 작은 파일"]
RA["MFT 파일 레코드, 고정 길이<br/>표준 정보 / 파일 이름 / 보안<br/>─────────────<br/>데이터 속성 = 내용 그 자체<br/>'설정값=1'이 여기에 직접 들어감"]
RB["디스크상에 별도의 저장 위치는 없음<br/>읽기는 MFT 접근만으로 완결"]
RA --> RB
end
subgraph NON2["비상주 non-resident ── 큰 파일"]
NA["MFT 파일 레코드, 고정 길이<br/>표준 정보 / 파일 이름 / 보안<br/>─────────────<br/>데이터 속성 = 데이터 런 표<br/>'어디서부터 몇 클러스터'의 나열"]
NB["사용자 데이터 영역<br/>런1, 연속 클러스터"]
NC["사용자 데이터 영역<br/>런2, 다른 위치의 연속 클러스터"]
NA -->|"위치를 가리킴"| NB
NA -->|"위치를 가리킴"| NC
end
그림 3: 상주와 비상주의 대비. 비상주에서는 레코드가 가지는 것이 ‘실제 데이터가 어디에 얼마나 있는가’를 나타내는 표, 즉 데이터 런뿐입니다
데이터 런의 수가 늘어날수록 파일 하나를 읽기 위해 흩어진 영역을 옮겨 다니게 됩니다. 이것이 다음에 다룰 단편화의 정체입니다.
이 구조로부터 현장에서 마주치는 여러 현상을 설명할 수 있습니다.
- 작은 파일 1만 개를 복사하는 것이 느린 이유. 파일 하나마다 MFT 레코드 생성・이름 등록・보안 설정이라는 메타데이터 작업이 발생합니다. 데이터 전송 자체보다 대장 작업 쪽이 지배적이 되는 것입니다(그리고 그 하나하나가, 제6회에서 살펴볼 필터의 검사 대상이 되기도 합니다).
- 단편화의 정체. 비상주 데이터는 ‘클러스터의 연속 구간(런)의 나열’로 기록됩니다. 연속 영역을 확보하지 못하면 런의 수가 늘어나고, 읽기에 필요한 탐색(seek)이 늘어납니다──이것이 단편화입니다. 런의 실제 배열은
fsutil file layout으로 들여다볼 수 있습니다. - ‘폴더’도 특별하지 않습니다. 디렉터리는 ‘파일 이름에서 MFT 레코드 번호로의 색인(인덱스)을 가진 파일’입니다. 대장 위에서는 모든 것이 같은 구조 위에 놓여 있습니다.
3. 데이터는 ‘스트림’ 중 하나에 불과하다
3.1. 하나의 파일, 여러 개의 바이트 열
NTFS에서는 하나의 파일이 여러 개의 데이터 스트림을 가질 수 있습니다. 평소 ReadFile/WriteFile로 읽고 쓰는 것은 이름 없는 기본 스트림이며, 파일 이름:스트림 이름이라는 구문으로 대체 데이터 스트림(ADS)을 만들 수 있습니다.2
flowchart LR
subgraph F["report.docx라는 파일, MFT 레코드 1개"]
D0["기본 스트림, 무명<br/>= 평소 보는 내용"]
D1["스트림 Zone.Identifier<br/>출처 정보 Mark of the Web"]
D2["스트림 임의의 이름<br/>앱 고유의 부가 정보"]
end
그림 4: 복수 데이터 스트림. 탐색기의 크기 표시에 나타나는 것은 기본 스트림뿐입니다
가장 친숙한 ADS가 Zone.Identifier입니다. 브라우저로 다운로드한 파일에는 이 스트림에 출처(인터넷에서 유래했다는 등)가 기록되며, SmartScreen의 ‘Windows에 의해 PC가 보호되었습니다’나 Office 보호 보기의 판단 근거가 됩니다. 이 구조의 표면은 「Windows에서 ‘Windows에 의해 PC가 보호되었습니다’가 나타나는 이유」에서 다루었습니다──그 이면의 정체는 그저 NTFS 스트림이었던 것입니다.
3.2. 개발자가 빠지는 함정
- 보이지 않는다. 탐색기의 크기에도
dir목록에도 나타나지 않습니다.dir /r이나 Sysinternals의streams로 확인할 수 있습니다.11 - 옮길 수 없다. ADS는 NTFS의 기능이므로, FAT의 USB 메모리나 클라우드 스토리지를 경유한 복사에서는 사라지기 쉽습니다. ‘다운로드 경고가 복사했더니 사라졌다’는 현상이 바로 이것입니다.
- 자신의 앱에서도 열 수 있다.
CreateFile("data.txt:meta", ...)처럼 경로에 콜론을 포함하기만 하면 읽고 쓸 수 있습니다.2 편리하지만, 앞서 말한 ‘옮길 수 없다’는 성질까지 함께 떠안게 되므로 업무 데이터의 본체를 담는 곳으로는 적합하지 않습니다.
4. 이름도 또한 속성이다 ── 하드 링크와 8.3 이름
4.1. 하드 링크 ── 같은 레코드를 향한 여러 개의 이름
그림 2에서 ‘파일 이름 속성은 여러 개 가질 수 있다’고 썼습니다. 같은 볼륨 안에서 여러 개의 경로가 단일 파일을 참조하는 것──이것이 하드 링크입니다(CreateHardLink / mklink /H).3
flowchart TB
subgraph DIR1["C:\app\ 의 인덱스"]
E1["config.json → 레코드#1234"]
end
subgraph DIR2["C:\backup\ 의 인덱스"]
E2["config-link.json → 레코드#1234"]
end
REC["MFT 레코드#1234<br/>데이터 본체, 또는 런에 대한 참조<br/>링크 수 2"]
E1 --> REC
E2 --> REC
그림 5: 하드 링크. 디렉터리의 색인이 같은 MFT 레코드를 가리키고 있을 뿐이며, 둘 다 ‘진짜’입니다
어느 이름을 통해 변경하더라도 같은 파일이므로 내용은 즉시 일치합니다.3 그리고 ‘삭제’의 의미가 달라집니다──DeleteFile은 ‘이름을 하나 떼어내는 것’이며, 마지막 이름이 떨어지고, 열려 있던 핸들이 닫히고, 나아가 메모리 맵 섹션 등 커널 내부의 참조까지 모두 사라졌을 때 비로소 실체가 사라집니다. 제1회에서 본 cleanup(마지막 핸들)과 close(마지막 참조)라는 두 단계가, 삭제의 수명에도 그대로 적용되는 것입니다. 참고로 속성 표시에는 특유의 버릇이 있어서, 어떤 링크를 통해 속성을 바꾸어도 다른 링크의 겉보기 표시는 예전 그대로인 채로 남는 동작이 공식적으로 주석되어 있습니다.3
4.2. 8.3 이름 ── 또 하나의 숨은 이름
역사적 호환성을 위해, NTFS는 긴 파일 이름에 대해 REPORT~1.DOC와 같은 8.3 형식의 단축 이름을 자동으로 생성할 수 있습니다. 이 또한 ‘또 하나의 이름’으로 레코드에 함께 존재합니다. 파일이 대량으로 있는 폴더에서는 단축 이름의 생성・충돌 회피가 비용이 되므로, fsutil 8dot3name으로 생성을 비활성화하거나 기존 단축 이름을 제거할 수 있습니다(레지스트리 경로를 단축 이름으로 기록해 둔 오래된 앱이 있으면 깨질 수 있으므로, strip 전에 검사하는 기능이 있는 것도 실무적으로 중요한 포인트입니다).4
여기서 주의할 점은, 단축 이름이 있는지 여부는 환경에 따라 달라진다는 것입니다. 기본 동작은 레지스트리 값 NtfsDisable8dot3NameCreation으로 결정되며, 0(모든 볼륨에서 생성), 1(모든 볼륨에서 생성 안 함), 2(볼륨 단위로 설정), 3(시스템 볼륨 이외에서는 생성 안 함)의 네 가지가 있습니다.4 2를 선택하면 볼륨 단위로 전환할 수 있으므로, ‘Windows라면 PROGRA~1 같은 단축 이름이 반드시 존재한다’고 단정할 수 없습니다. 단축 이름에 의존하는 코드나 절차를 작성하기 전에, fsutil 8dot3name query C:(볼륨을 생략하면 모든 볼륨 공통의 기본 설정)로 현재 상태를 확인하십시오.
경로와 이름을 둘러싼 함정(MAX_PATH, 예약어, 끝 점)은 「MAX_PATH와 Windows 경로・파일 이름의 함정」에서 자세히 다루고 있습니다. 제1회의 이름 해석(오브젝트 관리자)과 이 장(파일 시스템 내부의 이름)을 합치면, Windows의 ‘이름’에 대한 전체상이 완성됩니다.
5. 리파스 포인트 ── ‘열면 다른 위치로’ 장치
파일이나 디렉터리에는 리파스 포인트를 붙일 수 있습니다. 실체는 ‘태그 + 사용자 정의 데이터‘라는 속성입니다. 파일 시스템이 리파스 포인트가 붙은 파일을 열면, 태그에 따라 처리가 가로채집니다──태그를 이해하는 필터 드라이버가 처리를 넘겨받거나, 이름 재지정 계열 태그라면 링크 대상 경로로 해석이 다시 이루어집니다.5
sequenceDiagram
participant App as 앱
participant IOM as I/O 관리자
participant FS as NTFS
App->>IOM: CreateFile("C:\data\link.txt")
IOM->>FS: IRP_MJ_CREATE(제1회의 세계)
Note over FS: 대상에서 리파스 포인트 발견<br/>태그와 데이터를 반환
alt 심볼릭 링크/정션(이름 재지정)
FS-->>IOM: "진짜 위치는 이쪽"
IOM->>FS: 링크 대상 경로로 해석을 다시 수행
else 필터 관리 태그(클라우드 파일 등)
Note over FS: 태그를 이해하는 필터가<br/>처리를 넘겨받음(제6회)
end
그림 6: 리파스 포인트의 해석. ‘열기’라는 작업에 끼어드는 공식 훅 역할을 합니다
이 하나의 장치 위에 익숙한 기능들이 늘어서 있습니다.
- 심볼릭 링크(
mklink)──링크 대상 경로를 보유하는 표지판. 다른 볼륨이나 UNC 경로도 가리킬 수 있습니다.6 - 정션/마운트 포인트──디렉터리를 다른 로컬 볼륨의 위치로 연결하는 오래된 방식.3
- OneDrive의 파일 온디맨드──실제 데이터가 손에 없는 파일을 리파스 포인트로 표현하고, 열리는 순간 필터가 다운로드해 내용을 내놓습니다. ‘탐색기에서는 보이는데 열면 통신이 발생한다’의 정체입니다(필터 구조 자체는 제6회에서 다룹니다).
실무에서 주의할 점은 하나입니다. ‘경로 끝이 정말로 로컬의 그 위치라는 보장은 없다’는 것입니다. 트리를 재귀적으로 순회하는 도구가 정션에서 무한 루프에 빠지거나, 크기 집계가 중복되거나, 백업이 클라우드 파일의 대량 실체화(hydration)를 유발하거나──리파스 포인트의 존재를 모르는 코드는 이런 문제를 겪습니다. FindFirstFile 계열의 속성 FILE_ATTRIBUTE_REPARSE_POINT를 확인하는 것이 대책의 첫걸음입니다.5
6. 두 가지 저널 ── $LogFile과 USN
‘NTFS는 저널링 파일 시스템’이라는 말을 자주 듣지만, NTFS에는 역할이 다른 두 가지 저널이 있습니다. 혼동하면 보장 범위를 잘못 읽게 됩니다.
flowchart TB
subgraph J1["$LogFile ── 선행 로그, 손상 방지용"]
A1["메타데이터 작업, 레코드 갱신・이름 변경 등을<br/>실행 전에 로그에 기록"]
A2["시스템 장애 후 다음 부팅 시<br/>로그를 재생해 구조의 정합성을 회복"]
A1 --> A2
end
subgraph J2["USN 저널 ── 변경 이력, 무엇이 바뀌었는지 알기 위함"]
B1["파일/디렉터리 변경이 있을 때마다<br/>변경 내용과 이름을 기록"]
B2["백업・검색 인덱스・동기화 도구가<br/>'지난번 이후 무엇이 바뀌었는지'를 전체 스캔 없이 파악"]
B1 --> B2
end
그림 7: 두 가지 저널. $LogFile은 ‘손상시키지 않기’ 위해, USN은 ‘변경을 알기’ 위해 존재합니다
차이를 표로 정리하면 다음과 같습니다.
| 관점 | $LogFile(트랜잭션 로그) |
USN 저널(변경 저널) |
|---|---|---|
| 목적 | 장애 후 파일 시스템의 구조를 정합 상태로 되돌림7 | ‘지난번 이후 무엇이 바뀌었는지’를 나중에 파악8 |
| 기록하는 내용 | 메타데이터 작업(레코드 갱신・이름 변경 등)의 선행 로그. 파일 내용은 대상 외 | 변경이 있을 때마다 변경 내용과 대상 파일/디렉터리의 이름8 |
| 사용 주체 | NTFS 자신. 다음 마운트 시 자동 회복에 사용 | 백업・검색 인덱스・동기화 도구 등의 앱 |
| 얼마나 거슬러 올라갈 수 있는가 | 회복에 필요한 범위뿐. 고정 크기를 재사용하므로 과거 이력을 추적하는 용도로는 사용할 수 없음 | 목표 최대 크기(MaximumSize)를 초과하면 체크포인트 시점에 오래된 레코드부터 잘려 나감. 거슬러 올라갈 수 있는 범위는 크기 설정과 볼륨의 갱신량에 달려 있음12 |
| 참조 방법 | 내용을 읽는 공식 수단은 없음(크기는 chkdsk /L로 확인 가능) |
fsutil usn queryjournal로 상태를, fsutil usn readjournal로 내용을 확인. 프로그램에서는 FSCTL_QUERY_USN_JOURNAL / FSCTL_READ_USN_JOURNAL12 |
| 중지할 수 있는가 | 중지할 수 없음(NTFS의 일부) | 관리자가 삭제・비활성화할 수 있음. 다만 사용 중인 서비스에 전체 스캔을 강요하므로 영향이 큼12 |
표 2: 두 가지 저널의 대비
$LogFile(트랜잭션 로그)는 메타데이터 작업의 선행 로그입니다. 시스템 장애가 발생해도 NTFS는 다음 부팅 시 이 로그와 체크포인트 정보를 바탕으로 파일 시스템의 정합성을 자동으로 회복합니다.7 여기서 지켜지는 것은 구조입니다. 제4회에서 본 것처럼, 캐시상의 더티한 데이터 내용은 전원 차단으로 잃어버릴 수 있습니다──’볼륨은 손상되지 않는다. 그러나 마지막 쓰기는 사라질 수 있다’가 올바른 해석입니다.- USN 저널(변경 저널)은 볼륨 안의 파일이나 디렉터리에 변경이 일어날 때마다 변경 내용과 대상의 이름을 기록해 나가는 대장입니다.8 백업이나 인덱서가 ‘지난번 이후 바뀐 것만’을 전체 스캔 없이 골라내기 위한 구조이며, 장애 후 인덱스 재구축을 피하는 데도 사용됩니다.8 실무에서는
FileSystemWatcher의 누락(「FileSystemWatcher 실무 가이드」)을 보완하는 대조용 대장으로fsutil usn readjournal을 조사에 활용할 수 있다는 점을 기억해 두면 도움이 됩니다.
7. 스파스와 압축 ── ‘크기’가 두 개인 이야기
NTFS에서는 파일의 논리적인 길이와 실제로 할당된 영역이 별도로 관리됩니다. 속성 화면의 ‘크기’와 ‘디스크상 크기’가 바로 이것입니다. 이 차이를 만들어 내는 대표적인 요인이 두 가지 있습니다.
스파스 파일은 0이 이어지는 범위에 실제 영역을 할당하지 않고 ‘구멍’으로 관리합니다.9 논리 크기 42GB의 가상 디스크 파일이 디스크상에서는 500MB밖에 사용하지 않는──일이 흔하게 일어납니다. 구멍을 읽으면 0이 반환되고, 쓰면 그만큼만 할당됩니다.
flowchart LR
subgraph L["논리적인 파일, 크기 1GB"]
R1["데이터 10MB"]
H1["구멍, 0 500MB"]
R2["데이터 5MB"]
H2["구멍, 0 나머지"]
end
subgraph P["디스크상 할당, 15MB+관리 정보"]
A1["런 - R1의 실체"]
A2["런 - R2의 실체"]
end
R1 --> A1
R2 --> A2
그림 8: 스파스 파일. ‘구멍’에는 할당이 없어, 논리 크기와 디스크상 크기가 서로 어긋납니다
NTFS 압축은 데이터를 압축 단위별로 압축하여 저장합니다.10 투명하고 편리하지만, 비용까지 투명한 것은 아닙니다──읽고 쓸 때마다 압축 해제・재압축이 실행되며, 단편화도 진행되기 쉬워집니다. 그리고 제2회 5장에서 본 것처럼, 압축 파일에 대한 접근은 비동기가 되지 않습니다(파일 시스템이 동기로 변환합니다). ‘비동기 I/O로 만들었는데 빨라지지 않는 파일이 있다’고 느낄 때 의심해 볼 곳 중 하나입니다.
할당 기준의 실제 크기는 GetCompressedFileSize로 얻을 수 있습니다. ‘파일 크기 합계’와 ‘디스크 사용량’이 맞지 않는 조사에서는 스파스・압축・ADS(3장)・클러스터 올림, 이 네 가지를 순서대로 의심하는 것이 정석입니다.
8. 직접 눈으로 확인해 보기
이번에도 손 안의 Windows에서 모두 관찰할 수 있습니다(일부는 관리자 권한이 필요합니다). 실행 결과가 맞는지 스스로 판단할 수 있도록, 명령마다 어디를 보면 무엇을 알 수 있는지를 함께 적습니다.
:: 대체 데이터 스트림 확인
dir /r C:\Users\%USERNAME%\Downloads
여기를 본다: 일반 파일 행 아래에, 들여쓰기된 파일 이름:Zone.Identifier:$DATA 형태의 행이 길이와 함께 나열됩니다. 이 행이 있으면 해당 파일에 Mark of the Web이 붙어 있는 상태입니다(3장). 브라우저로 다운로드한 파일에는 붙어 있고, 직접 만든 파일에는 붙어 있지 않습니다. 양쪽 위치에서 실행해 비교해 보면 ADS의 유무가 뚜렷하게 드러납니다.
:: 파일의 MFT상 배치(런)나 속성 확인
fsutil file layout C:\path\to\file.dat
:: 익스텐트만 확인(공식 문서에 기재된 하위 명령)
fsutil file queryextents C:\path\to\file.dat
여기를 본다: layout은 스트림별로 크기・할당 크기와, 비상주라면 익스텐트(VCN・LCN・클러스터 수의 조합)의 목록을 나열합니다. 익스텐트 행이 나오지 않는 아주 작은 파일은 상주(2.2절)이며, 여러 행으로 나뉘어 있으면 단편화된 것입니다. 수 바이트의 텍스트 파일과 수백 MB의 파일로 실행해 비교해 보는 것이, 상주/비상주를 가장 빠르게 체감하는 방법입니다.
:: 8.3 단축 이름 생성 설정과 기존 단축 이름
fsutil 8dot3name query C:
dir /x
여기를 본다: query는 해당 볼륨에서 단축 이름 생성이 활성화되어 있는지 여부를 반환합니다(볼륨을 생략하면 모든 볼륨 공통의 기본 설정).4 dir /x는 긴 이름 옆에 단축 이름 열을 표시하므로, 열이 비어 있으면 단축 이름이 만들어지지 않았다는 뜻입니다. 4.2절의 ‘단축 이름이 반드시 있는 것은 아니다’를 자신의 환경에서 확인할 수 있습니다.
:: USN 저널의 상태
fsutil usn queryjournal C:
여기를 본다: 저널 ID, 유효한 USN 범위(First USN / Next USN), 목표 최대 크기(MaximumSize)와 할당 단위(AllocationDelta)가 표시됩니다.12 파일을 하나 만든 뒤 다시 실행하면 Next USN이 진행되어 있을 것이며, 이것이 ‘변경이 기록되고 있다’는 확인이 됩니다. MaximumSize는 6장에서 언급한 ‘얼마나 거슬러 올라갈 수 있는가’의 기준입니다. 저널이 비활성화된 볼륨에서는 오류가 발생합니다.
:: 리파스 포인트 확인(링크 대상과 태그)
dir /aL C:\Users\%USERNAME%
fsutil reparsepoint query "C:\Users\%USERNAME%\OneDrive"
여기를 본다: dir /aL에 나타난 것이 리파스 포인트(FILE_ATTRIBUTE_REPARSE_POINT가 설정된 것)입니다. 심볼릭 링크나 정션은 <SYMLINKD> <JUNCTION>처럼 종류가 표시된 형태로 나타납니다. fsutil reparsepoint query는 리파스 태그 값과, 이름 재지정 계열이라면 링크 대상 경로를 표시합니다. 리파스 포인트가 아닌 대상을 지정하면 오류가 발생하므로, 오류가 나는 것 자체가 ‘여기는 평범한 폴더’라는 확인이 됩니다.
Procmon으로 파일 작업을 추적하면 이번 회의 등장인물들이 실명으로 흘러가는 것을 볼 수 있습니다($LogFile에 대한 쓰기, 스트림 이름이 붙은 경로, 리파스 처리). 사용법은 「Process Monitor(ProcMon) 실전 가이드」를 참조하십시오.
9. 정리
- NTFS의 중심은 MFT입니다. 모든 파일이 대장의 레코드이며, 정보는 ‘레코드 안’이나 ‘레코드가 가리키는 외부 영역’에 있습니다. 작은 데이터는 상주, 큰 데이터는 런 참조이며, 소규모 파일을 대량으로 처리할 때의 느림이나 단편화는 이 구조의 귀결입니다.1
- 데이터 스트림은 여러 개 가질 수 있습니다. Zone.Identifier(Mark of the Web)는 그저 ADS일 뿐이며,
dir /r로 확인할 수 있고, NTFS 밖으로는 옮겨지지 않습니다.211 - 이름은 속성이며, 여러 개 가질 수 있습니다. 하드 링크는 같은 레코드를 향한 동격의 이름이고, 8.3 이름은 호환용의 또 하나의 이름입니다. ‘삭제 = 이름을 떼어냄’이며, 실체의 소멸은 마지막 이름・핸들・커널 내부 참조(매핑된 섹션 등)가 모두 사라졌을 때입니다.34
- 리파스 포인트는 ‘열기’에 대한 공식 훅으로, 심볼릭 링크도 정션도 파일 온디맨드도 모두 이것의 응용입니다. 트리를 순회하는 코드는
FILE_ATTRIBUTE_REPARSE_POINT를 의식할 필요가 있습니다.56 - 저널은 두 가지입니다.
$LogFile은 구조의 정합성 회복(손상 방지)용이고, USN은 변경 이력(무엇이 바뀌었는가)용입니다. ‘저널링이니까 데이터도 안전하다’는 것은 아닙니다──데이터의 내구성은 제4회에서 다룬 도구로 직접 구현합니다.78 - 논리 크기와 할당은 별개입니다. 스파스・압축・ADS・클러스터 올림이 ‘크기가 맞지 않는다’는 문제의 4대 요인입니다. 압축 파일이 비동기 I/O가 되지 않는 사례까지 포함해, 성능 조사를 위한 도구 상자가 됩니다.910
다음 회는 마지막 회, 제6회 「필터 드라이버와 미니필터 ── Procmon과 백신 검사가 I/O에 개입할 수 있는 이유」입니다. 제1회부터 곳곳에서 등장해 온 ‘사이에 끼어드는 존재들’──백신, Procmon, OneDrive, 암호화──이 어떻게 I/O에 개입하는지. 연재의 총정리로서, 디바이스 스택의 틈새에 서 있는 존재들의 정체를 밝힙니다.
관련 글
- Windows I/O의 심층(제1회) ── 모든 읽기·쓰기는 IRP가 된다: I/O 시스템의 전체상
- Windows I/O의 심층(제2회) ── 동기 I/O와 비동기 I/O: OVERLAPPED의 진짜 의미
- Windows I/O의 심층(제4회) ── 캐시 관리자: 당신의 WriteFile은 언제 디스크에 도달하는가
- Windows에서 ‘Windows에 의해 PC가 보호되었습니다’가 나타나는 이유
- MAX_PATH와 Windows 경로·파일 이름의 함정 ── 260자 제한, 예약어, 끝 점, 대소문자
- FileSystemWatcher 사용법과 주의점 - 누락, 중복 알림, 완료 판정의 함정
- 네트워크 드라이브와 UNC 경로의 함정 ── 업무 앱에서 파일 서버(공유 폴더)를 다루는 실무
- Process Monitor(ProcMon) 실전 가이드 ── 「설정이 반영되지 않는다」「ACCESS DENIED」를 10분 만에 특정하기
관련 상담 영역
합동회사 코무라소프트에서는 파일 크기나 복사 성능의 이해하기 어려운 동작, 링크・스트림과 관련된 결함 등 NTFS의 구조에 뿌리를 둔 Windows 업무 앱의 설계・조사를 다루고 있습니다.
참고 링크
-
Microsoft Learn, Master File Table. NTFS 볼륨상의 모든 파일에 대해 MFT에 최소 하나의 엔트리가 있으며 MFT 자신의 엔트리도 포함된다는 점, 파일 크기・타임스탬프・접근 권한・데이터 내용을 포함한 모든 정보가 MFT 엔트리 안이나 MFT 엔트리가 위치를 기술하는 MFT 밖의 영역에 저장된다는 점, 파일 삭제 시 엔트리가 빈 자리로 표시되어 재사용되지만 MFT의 크기는 줄어들지 않는다는 점, MFT를 연속적으로 유지하기 위한 MFT 존이 예약된다는 점, 할당이 진행됨에 따라 MFT의 단편화가 일어난다는 점에 대해. ↩ ↩2 ↩3 ↩4 ↩5 ↩6
-
Microsoft Learn, File Streams. NTFS의 파일 데이터가 하나 이상의 스트림으로 저장된다는 점, 기본(이름 없는) 데이터 스트림과 이름 붙은 대체 데이터 스트림이 있다는 점, ‘파일 이름:스트림 이름’ 형식으로 스트림을 지정해 CreateFile로 열 수 있다는 점에 대해. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, Hard links and junctions. 하드 링크가 같은 볼륨 안에서 여러 개의 경로가 단일 파일을 참조하는 파일 시스템상의 표현이라는 점, CreateHardLink로 생성한다는 점, 어느 링크를 통한 변경이든 다른 링크에서 즉시 확인된다는 점, 속성 변경이 모든 하드 링크에 전파되는 한편 디렉터리 엔트리상의 표시는 변경을 수행한 링크에서만 갱신된다는 표시상의 특성이 있다는 점, 그리고 정션(디렉터리를 다른 로컬 볼륨으로 연결하는 방식)에 대해. ↩ ↩2 ↩3 ↩4 ↩5 ↩6
-
Microsoft Learn, fsutil 8dot3name. NTFS가 긴 파일 이름에 대해 8.3 형식의 단축 이름을 생성할 수 있다는 점, fsutil 8dot3name으로 단축 이름 생성의 활성화/비활성화 조회와 설정, 기존 단축 이름의 제거(strip), 제거 시 영향을 받는 레지스트리 참조의 스캔을 수행할 수 있다는 점에 대해. ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, Reparse points. 리파스 포인트가 사용자 정의 데이터와 그 데이터 형식을 고유하게 식별하는 리파스 태그의 집합이라는 점, 리파스 포인트가 붙은 파일을 열 때 파일 시스템이 태그에 대응하는 처리(태그를 해석하는 파일 시스템 필터에 의한 처리)를 시도한다는 점, NTFS의 파일 시스템 링크나 원격 스토리지(계층형 저장소)의 구현에 사용된다는 점, FILE_ATTRIBUTE_REPARSE_POINT 속성으로 존재를 확인할 수 있다는 점에 대해. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, Symbolic links. 심볼릭 링크가 다른 파일이나 디렉터리를 가리키는 파일 시스템 객체이며, 링크 대상으로의 투명한 리다이렉트로 작동한다는 점, 절대・상대 링크가 있으며 볼륨을 넘나드는 참조나 원격 경로에 대한 참조가 가능하다는 점에 대해. ↩ ↩2 ↩3
-
Microsoft Learn, NTFS overview. NTFS가 로그 파일과 체크포인트 정보를 사용해 시스템 장애가 발생한 경우 다음 부팅 시 트랜잭션 로그를 재생하여 파일 시스템의 정합성을 자동으로 회복한다는 점, 불량 섹터의 동적 재매핑이나 백그라운드에서 경미한 손상을 복구하는 self-healing NTFS를 갖추고 있다는 점에 대해. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, Change Journals. 볼륨 안의 파일이나 디렉터리에 변경이 가해질 때마다 해당 볼륨의 USN 변경 저널에 변경 내용과 대상 파일/디렉터리의 이름이 기록된다는 점, 볼륨별로 저널이 유지된다는 점, 장애 후 파일 시스템 인덱스의 회복에 사용할 수 있으며 볼륨 전체의 재인덱싱을 피할 수 있다는 점에 대해. ↩ ↩2 ↩3 ↩4 ↩5 ↩6
-
Microsoft Learn, Sparse Files. 스파스 파일에서 0으로 구성된 큰 범위에는 물리적인 디스크 영역을 할당하지 않고 데이터를 포함한 부분에만 영역을 할당한다는 점, 할당되지 않은 범위를 읽으면 0이 반환된다는 점에 대해. ↩ ↩2 ↩3
-
Microsoft Learn, File Compression and Decompression. NTFS의 파일 압축이 투명하게 이루어지며 압축 단위별로 데이터가 압축・저장된다는 점, GetCompressedFileSize로 압축(실제 할당) 후의 크기를 얻을 수 있다는 점, 압축 파일의 읽기/쓰기에 압축 해제・재압축 비용이 수반된다는 점에 대해. ↩ ↩2 ↩3
-
Microsoft Learn, Streams - Sysinternals. Sysinternals의 streams 유틸리티가 NTFS 파일의 대체 데이터 스트림을 나열・삭제할 수 있다는 점에 대해. ↩ ↩2
-
Microsoft Learn, Creating, Modifying, and Deleting a Change Journal 및 fsutil usn. 변경 저널의 MaximumSize가 목표값이며, 크기가 MaximumSize와 AllocationDelta의 합계를 초과하면 NTFS의 체크포인트 시점에 잘려 나간다는 점, AllocationDelta가 저널 끝에 추가하고 앞에서 삭제하는 단위라는 점, fsutil usn queryjournal로 저널의 상태와 용량을, readjournal로 기록 내용을 참조할 수 있다는 점, 프로그램에서는 FSCTL_CREATE_USN_JOURNAL / FSCTL_QUERY_USN_JOURNAL / FSCTL_READ_USN_JOURNAL / FSCTL_DELETE_USN_JOURNAL을 사용한다는 점, 활성 저널의 삭제・비활성화가 MFT 전체의 스캔을 수반하며 저널을 이용 중인 서비스에 볼륨의 재스캔을 강요한다는 점에 대해. ↩ ↩2 ↩3 ↩4
관련 기사
같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.
Windows I/O의 심층(제4회) ── 캐시 관리자: 당신의 WriteFile은 언제 디스크에 도달하는가
Windows의 캐시 관리자를 그림으로 설명하는 연재의 제4회입니다. 파일 매핑으로 구현된 캐시, 선행 읽기와 지연 쓰기, FlushFileBuffers와 FILE_FLAG_NO_BUFFERING의 구분 사용, 전원 차단으로 데이터가 사라지는 조...
Windows I/O의 심층(제6회・최종회) ── 필터 드라이버와 미니필터: Procmon과 백신 검사가 I/O에 개입할 수 있는 이유
Windows의 필터 드라이버와 미니필터를 그림으로 설명하는 연재의 최종회입니다. 필터 매니저와 알티튜드, pre/post 콜백, Procmon과 백신이 모든 I/O를 검사할 수 있는 구조, '그 환경에서만 느리다'의 조사 절차까지 정리합니다.
Windows I/O의 심층(제2회) ── 동기 I/O와 비동기 I/O: OVERLAPPED의 진짜 의미
Windows의 동기 I/O와 비동기 I/O(오버랩 I/O)를 그림으로 설명하는 연재의 제2회입니다. FILE_FLAG_OVERLAPPED의 의미, 완료 통지의 4가지 방식, 비동기인데 동기 완료되는 조건, 취소의 작법, .NET과의 대응까지 정...
Windows I/O의 심층(제1회) ── 모든 읽기·쓰기는 IRP가 된다: I/O 시스템의 전체상
Windows의 I/O 시스템을 밑바닥부터 해설하는 연재의 제1회입니다. 오브젝트 관리자의 이름공간, 드라이버·디바이스·파일이라는 3가지 오브젝트, IRP의 라이프사이클, CloseHandle의 이면까지를 그림으로 정리합니다.
멀티스레드 실무 베스트 프랙티스 .NET 편 ── 스레드를 늘리기 전에 정해둘 것
「스레드를 만들면 가끔 죽거나 멈춘다」를 막는 설계의 정석을 .NET/C# 대상으로 정리합니다. 스레드를 직접 만들지 않고 Task에 올라타기, 공유 가변 상태 줄이기, 락의 규율, CancellationToken을 통한 정지 설계, UI 스레드...
관련 토픽
이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.
Windows 기술 토픽
Windows 개발, 장애 조사, 기존 자산 활용에 관한 KomuraSoft LLC 기사를 모은 토픽 허브입니다.
이 주제와 연결되는 서비스
이 기사는 다음 서비스 페이지로 이어집니다. 가까운 입구부터 확인해 주세요.
Windows 앱 개발
상주 처리, 장비 연동, 운영 로그, 유지 보수 가능한 구조가 필요한 Windows 데스크톱 애플리케이션을 지원합니다.
자주 묻는 질문
이 기사 주제에 대해 상담 시 자주 나오는 질문을 모았습니다.
- MFT(마스터 파일 테이블)란 무엇인가요?
- NTFS 볼륨의 심장부에 해당하는 데이터 구조로, 볼륨상의 모든 파일에 대해 최소 하나의 엔트리(파일 레코드)를 가지는 대장(臺帳)입니다. MFT 자신의 엔트리도 포함됩니다. 파일 크기, 타임스탬프, 접근 권한, 그리고 데이터 본체에 이르기까지 파일에 관한 모든 정보는 MFT 엔트리 안이나, MFT 엔트리가 가리키는 MFT 밖의 영역에 저장됩니다. 작은 파일이라면 데이터 본체까지 MFT 엔트리 안에 들어가며(상주), 큰 파일은 데이터가 놓인 위치(클러스터의 나열)에 대한 참조만 엔트리에 기록됩니다(비상주). 파일을 삭제하면 엔트리는 빈 자리로 표시되어 재사용되지만, MFT 자체의 크기는 줄어들지 않습니다.
- 파일에 붙어 있는 'Zone.Identifier'라는 보이지 않는 데이터는 무엇인가요?
- NTFS의 복수 데이터 스트림(대체 데이터 스트림) 중 하나입니다. NTFS에서는 하나의 파일이 여러 개의 바이트 열(스트림)을 가질 수 있으며, 평소 읽고 쓰는 것은 이름 없는 기본 스트림입니다. 'file.txt:Zone.Identifier'처럼 콜론으로 구분해 지정하는 추가 스트림에, Windows는 파일의 출처(인터넷에서 다운로드했다는 등)를 기록합니다. 이것이 이른바 'Mark of the Web'으로, SmartScreen 경고나 Office 보호 보기의 판단 근거가 됩니다. 대체 스트림은 탐색기의 크기 표시에 나타나지 않으며, dir /r 명령이나 Sysinternals의 streams 도구로 확인할 수 있습니다. NTFS 이외의 파일 시스템(FAT 등)으로 복사하면 보존되지 않는다는 점에도 주의가 필요합니다.
- 하드 링크와 심볼릭 링크는 무엇이 다른가요?
- 하드 링크는 '같은 파일 실체(같은 MFT 레코드)를 가리키는, 동격의 이름이 하나 더 늘어나는' 것입니다. 같은 볼륨 안에서만 만들 수 있으며, 어떤 이름으로 접근하든 같은 파일이므로, 하나의 이름을 삭제해도 다른 이름이 남아 있으면 파일은 사라지지 않습니다. 심볼릭 링크는 '다른 경로로 안내하는 표지판'으로, 리파스 포인트로 구현됩니다. 링크 대상의 경로 문자열을 보유하고 있을 뿐이므로 다른 볼륨이나 원격 위치도 가리킬 수 있지만, 링크 대상이 사라지면 막다른 길이 됩니다. 실무에서는 하드 링크는 실체의 공유(삭제의 의미가 달라짐), 심볼릭 링크는 경로의 재배치(이전이나 리다이렉션)로 구분해 사용하는 것이 기본입니다.
- NTFS는 저널링 파일 시스템이니까 전원이 끊겨도 데이터는 사라지지 않나요?
- 보호되는 범위를 정확히 이해할 필요가 있습니다. NTFS의 트랜잭션 로그($LogFile)가 지키는 것은 파일 시스템의 구조(메타데이터)의 정합성입니다. 시스템 장애가 일어나도 다음 부팅 시 로그를 사용해 정합성을 자동으로 회복하여, '볼륨이 손상되어 읽을 수 없다'는 사태를 막습니다. 하지만 쓰는 도중이었던 파일의 데이터 내용 자체가 복원되는 것은 아닙니다. 연재 제4회에서 본 것처럼, 캐시상의 더티(dirty)한 데이터는 전원 차단으로 사라집니다. 즉 '볼륨은 손상되지 않지만, 마지막 쓰기 내용은 사라질 수 있다'가 올바른 이해이며, 데이터 자체의 내구성이 필요하다면 FlushFileBuffers나 WRITE_THROUGH, 또는 애플리케이션 쪽의 쓰기 설계(임시 파일 + 이름 바꾸기 등)로 직접 구현해야 합니다.
- 파일의 '크기'와 '디스크상 크기'가 다른 이유는 무엇인가요?
- NTFS에서는 파일의 논리적인 길이와 실제로 할당된 디스크 영역이 별도로 관리되기 때문입니다. 일반 파일에서도 클러스터 단위(기본값 4KB)로 올림 처리해 할당하므로 차이가 나지만, 크게 벌어지는 것은 스파스 파일과 압축 파일입니다. 스파스 파일에서는 0이 이어지는 범위에 실제 영역을 할당하지 않고 '구멍'으로 관리하기 때문에, 논리 크기가 수 GB라도 디스크상에서는 수 MB인 경우가 발생합니다. 압축 파일에서는 압축 후의 크기만 할당됩니다. 반대로 '디스크상 크기' 쪽이 더 커 보이는 경우는 클러스터 올림이나 대체 데이터 스트림이 원인인 경우가 있습니다.