rm으로 파일을 지우면 디스크에서 데이터가 진짜 사라질까? 파일 이름은 어디에 저장되는 걸까 — inode 안에? 밖에?

파일 시스템은 0과 1로 가득 찬 디스크 위에 "파일"이라는 구조를 만들어주는 체계예요. inode가 뭘 하고, 저널링은 왜 필요하고, ext4는 뭐가 다른지 하나씩 풀어보겠습니다.

파일 시스템이란

디스크는 본질적으로 0과 1의 바이트 덩어리입니다. 그대로 두면 어디에 뭐가 저장됐는지 알 수가 없어요. 파일 시스템은 이 날것의 바이트 위에 구조를 부여 해서, "파일"이라는 논리적 단위로 데이터를 읽고 쓸 수 있게 해주는 체계입니다.

좀 더 구체적으로 말하면, 파일 시스템이 하는 일은 이런 것들이에요.

  • 데이터를 파일 단위로 구분하고, 각 파일에 이름을 붙여요
  • 파일이 디스크 어디에 저장돼 있는지 추적합니다
  • 메타데이터(크기, 권한, 수정 시각 등)를 관리해요
  • 빈 공간을 관리하고, 새 파일을 어디에 배치할지 결정합니다
  • 장애 발생 시 데이터 일관성을 보장해요

리눅스에서 대표적인 파일 시스템으로 ext4, XFS, Btrfs 같은 것들이 있고, Windows는 NTFS를 씁니다. macOS는 APFS예요.


블록과 섹터 — 디스크 I/O의 단위

디스크와 파일 시스템 사이에는 I/O 단위 개념이 있습니다. 이걸 모르면 이후 내용이 좀 뜬구름처럼 느껴질 수 있으니 짚고 넘어가겠습니다.

섹터 (Sector)

디스크 하드웨어가 한 번에 읽고 쓸 수 있는 최소 물리 단위 입니다. 전통적으로 512바이트였고, 최근 디스크들은 4KB 섹터(Advanced Format)를 써요. 이건 하드웨어 레벨의 단위라서 OS가 바꿀 수 있는 게 아닙니다.

블록 (Block)

파일 시스템이 관리하는 논리적 I/O 단위 입니다. 보통 4KB(8개 섹터)로 설정해요. 파일을 저장할 때 블록 단위로 공간을 할당하기 때문에, 1바이트짜리 파일도 최소 4KB의 디스크 공간을 차지합니다. 이걸 내부 단편화 라고 해요.

왜 섹터 단위로 안 하고 블록 단위로 할까요? 512바이트씩 관리하면 관리해야 할 단위 수가 너무 많아집니다. 1TB 디스크에 512바이트 섹터면 약 20억 개를 추적해야 하는데, 4KB 블록이면 약 2.5억 개로 줄어들어요. 관리 오버헤드와 성능 사이의 트레이드오프인 셈이에요.


inode — 파일의 메타데이터 저장소

리눅스 파일 시스템에서 가장 핵심적인 자료구조가 바로 inode(index node) 입니다. 파일 하나당 inode 하나가 대응돼요.

inode에 담기는 정보

PLAINTEXT
inode #29401
├── 파일 타입 (일반 파일 / 디렉토리 / 심볼릭 링크 등)
├── 소유자 (UID), 그룹 (GID)
├── 권한 (rwxr-xr--)
├── 파일 크기
├── 타임스탬프 (atime, mtime, ctime)
├── 하드 링크 카운트
├── 데이터 블록 포인터들
│   ├── 직접 포인터 12개
│   ├── 단일 간접 포인터
│   ├── 이중 간접 포인터
│   └── 삼중 간접 포인터
└── 기타 플래그들

중요한 건, inode에는 파일 이름이 없다 는 점입니다. 파일 이름은 디렉토리 엔트리가 관리하는 것이지, inode 자체에는 포함되지 않아요. 이 분리가 하드 링크를 가능하게 만듭니다.

inode 테이블과 inode 번호

파일 시스템을 생성(mkfs)할 때, 디스크 공간의 일부를 inode 테이블에 미리 할당합니다. 각 inode에는 고유한 inode 번호 가 부여되는데, 같은 파일 시스템 내에서 유일해요.

BASH
$ ls -i /etc/passwd
524289 /etc/passwd

$ stat /etc/passwd
  File: /etc/passwd
  Size: 2765       Blocks: 8    IO Block: 4096   regular file
  Inode: 524289     Links: 1

inode 수는 파일 시스템 생성 시 고정되기 때문에(ext4 기준), 디스크 공간이 남아있어도 inode가 다 차면 파일을 더 만들 수 없습니다. 작은 파일이 엄청나게 많은 환경(메일 서버 같은)에서 실제로 발생하는 문제예요.

데이터 블록 포인터 구조

inode가 데이터 블록 위치를 가리키는 방식이 좀 독특합니다. ext 계열 파일 시스템에서는 전통적으로 이런 구조를 써요.

포인터 종류개수가리키는 대상
직접 포인터12개데이터 블록을 직접 가리킴 (4KB x 12 = 48KB)
** 단일 간접 포인터**1개포인터 블록 → 데이터 블록
** 이중 간접 포인터**1개포인터 블록 → 포인터 블록 → 데이터 블록
** 삼중 간접 포인터**1개포인터 블록 → 포인터 블록 → 포인터 블록 → 데이터 블록

작은 파일은 직접 포인터만으로 충분하고, 큰 파일일수록 간접 포인터를 통해 더 많은 블록을 가리킵니다. 4KB 블록 + 4바이트 포인터 기준으로 단일 간접은 1024개 블록(4MB), 이중 간접은 약 4GB까지 커버돼요.


디렉토리 엔트리 — 이름과 inode의 매핑

디렉토리도 사실은 ** 파일 입니다. 다만 그 내용이 일반 데이터가 아니라 ** 파일 이름 → inode 번호 매핑 테이블이라는 차이가 있을 뿐이에요.

PLAINTEXT
디렉토리 /home/sim/projects
├── "main.c"       → inode 39201
├── "Makefile"     → inode 39202
├── "README.md"    → inode 39205
├── "."            → inode 38100  (자기 자신)
└── ".."           → inode 37000  (상위 디렉토리)

파일을 열 때 OS가 하는 일을 보면 이 구조가 명확해집니다. /home/sim/projects/main.c를 열 때:

  1. 루트 / 디렉토리의 inode를 찾습니다 (inode 번호 2, 고정)
  2. 루트 디렉토리 데이터에서 "home"에 해당하는 inode 번호를 찾아요
  3. 그 inode로 /home 디렉토리 데이터를 읽고, "sim"을 찾습니다
  4. 반복해서 최종 main.c의 inode 번호를 얻어요
  5. 해당 inode의 데이터 블록 포인터를 따라가서 파일 내용을 읽습니다

경로가 깊을수록 디스크 접근이 많아지는 건 이 때문이에요. 물론 실제로는 커널이 dentry 캐시 를 유지해서 매번 디스크까지 가지 않습니다.


하드 링크 vs 심볼릭 링크

inode 구조를 이해하면 링크의 차이가 바로 보입니다.

하드 링크

BASH
$ ln original.txt hardlink.txt

새로운 디렉토리 엔트리를 만들되, 같은 inode 번호 를 가리킵니다. 원본 파일과 하드 링크는 완전히 동등해요 — 사실 뭐가 원본이고 뭐가 링크인지 구분할 방법이 없습니다. 둘 다 같은 inode의 디렉토리 엔트리일 뿐이에요.

PLAINTEXT
"original.txt"  → inode 39201 ← "hardlink.txt"

                  데이터 블록들

inode에는 링크 카운트 가 있어서, 이 inode를 가리키는 디렉토리 엔트리가 몇 개인지 추적합니다. rm으로 파일을 "삭제"하면 실제로는 디렉토리 엔트리 하나를 지우고 링크 카운트를 1 줄이는 것뿐이에요. 링크 카운트가 0이 되어야 비로소 inode와 데이터 블록이 해제됩니다.

제약이 하나 있어요. 하드 링크는 같은 파일 시스템 내에서만 만들 수 있습니다. inode 번호가 파일 시스템 범위에서만 유일하니까요. 그리고 디렉토리에 대해서는 하드 링크를 만들 수 없습니다 — 순환 참조가 생길 수 있어서요.

심볼릭 링크 (Symbolic Link)

BASH
$ ln -s /home/sim/original.txt symlink.txt

완전히 다른 inode를 생성하고, 그 데이터로 ** 대상 파일의 경로 문자열 **을 저장합니다. 바로가기와 비슷한 개념이에요.

PLAINTEXT
"symlink.txt" → inode 39300 → 데이터: "/home/sim/original.txt"

"original.txt" → inode 39201 → 데이터 블록들

원본을 삭제하면 심볼릭 링크는 dangling link 가 되어 깨집니다. 대신 파일 시스템을 넘어서도 가리킬 수 있고, 디렉토리에 대해서도 만들 수 있어요.

비교 항목하드 링크심볼릭 링크
inode같은 inode 공유별도 inode 생성
원본 삭제 시데이터 유지 (링크 카운트 > 0이면)dangling link — 깨짐
파일 시스템 경계불가가능
디렉토리 링크불가가능
디스크 공간디렉토리 엔트리만 추가새 inode + 경로 데이터

파일 할당 방식

파일 데이터를 디스크 블록에 어떻게 배치하느냐는 파일 시스템 설계의 핵심 문제입니다. 세 가지 방식의 차이를 이해하는 것이 중요해요.

연속 할당 (Contiguous Allocation)

파일을 디스크에 연속된 블록 으로 저장합니다. 시작 블록 번호와 길이만 알면 전체 파일에 접근 가능하니까 단순하고 빨라요. 순차 읽기는 물론이고 랜덤 접근도 O(1)입니다.

문제는 외부 단편화 예요. 파일을 만들고 지우다 보면 빈 공간이 띄엄띄엄 생기고, 충분한 연속 공간을 못 찾으면 파일을 저장할 수 없습니다. 파일 크기가 늘어나는 것도 처리하기 어려워요. CD-ROM처럼 한번 쓰고 변경이 없는 매체에는 괜찮습니다.

연결 할당 (Linked Allocation)

각 블록이 다음 블록의 포인터 를 갖는 연결 리스트 구조입니다. 외부 단편화가 없고 파일 크기를 동적으로 늘릴 수 있어요. 하지만 랜덤 접근이 O(n)이 되고, 포인터 하나가 깨지면 파일 전체를 잃을 수 있습니다. FAT(File Allocation Table) 파일 시스템이 이 방식을 개선한 형태예요.

인덱스 할당 (Indexed Allocation)

인덱스 블록 하나에 해당 파일이 사용하는 모든 데이터 블록의 번호를 모아둡니다. 랜덤 접근이 가능하고 외부 단편화도 없어요. 다만 작은 파일이라도 인덱스 블록 하나를 차지하는 오버헤드가 있습니다.

앞서 본 inode의 포인터 구조가 바로 이 인덱스 할당의 확장 버전이에요. 직접 포인터 + 간접 포인터 구조로 작은 파일부터 큰 파일까지 효율적으로 커버합니다.

방식랜덤 접근외부 단편화파일 확장단점
연속 할당O(1)심함어려움외부 단편화, 크기 고정
연결 할당O(n)없음쉬움느린 랜덤 접근, 포인터 손상 위험
인덱스 할당O(1)없음쉬움인덱스 블록 오버헤드

저널링 (Journaling)

왜 필요한가 — Crash Recovery

파일을 저장하는 작업은 단일 연산이 아닙니다. 데이터 블록 쓰기, inode 업데이트, 비트맵 갱신 등 ** 여러 단계 **로 이뤄져요. 이 도중에 정전이나 커널 패닉이 발생하면? 일부만 반영된 상태가 되어 파일 시스템이 ** 비일관적인 상태(inconsistent state)**에 빠질 수 있습니다.

예를 들어 새 파일을 만드는 과정을 볼게요.

  1. 빈 inode를 할당합니다 (inode 비트맵 갱신)
  2. 빈 데이터 블록을 할당합니다 (블록 비트맵 갱신)
  3. 데이터 블록에 파일 내용을 씁니다
  4. inode에 메타데이터와 블록 포인터를 기록해요
  5. 디렉토리 엔트리에 파일명 → inode 매핑을 추가합니다

2번까지 완료하고 3번에서 크래시가 나면? 비트맵에는 "할당됨"으로 표시됐는데 실제 데이터는 없는, 유령 블록이 생깁니다. 이런 상태를 복구하려면 fsck 같은 도구로 파일 시스템 전체를 스캔해야 하는데, 디스크가 크면 몇 시간이 걸릴 수도 있어요.

저널링의 동작 원리

저널링은 데이터베이스의 WAL(Write-Ahead Logging) 과 같은 원리입니다. 실제 데이터를 기록하기 전에, 무엇을 할 것인지를 저널 영역(로그) 에 먼저 써요.

PLAINTEXT
[1단계] 저널에 트랜잭션 기록
   → "inode 비트맵 변경", "블록 비트맵 변경", "inode 쓰기" 등을 로그에 기록

[2단계] 실제 위치에 데이터 기록
   → 로그에 적은 대로 실제 메타데이터·데이터를 갱신

[3단계] 저널에서 해당 트랜잭션 삭제 (커밋 완료)

크래시가 발생해도 복구가 간단합니다. 부팅할 때 저널을 확인해서, 완료되지 않은 트랜잭션이 있으면 그냥 재실행(redo)하거나 버리면(undo) 돼요. 전체 디스크를 스캔할 필요가 없으므로 복구 시간이 수 초 이내입니다.


ext4

리눅스의 사실상 기본 파일 시스템입니다. ext2 → ext3(저널링 추가) → ext4로 이어져 왔고, 안정성과 호환성이 매우 높아요.

주요 특징

Extents: ext3까지는 간접 포인터 방식으로 블록을 추적했는데, ext4는 Extent 구조를 도입했습니다. Extent는 "시작 블록 번호 + 연속 블록 수"의 쌍으로, 연속된 블록들을 하나의 엔트리로 표현해요. 파일이 연속적으로 저장되어 있다면, 수백 개의 포인터 대신 Extent 하나로 충분합니다. 대용량 파일 처리에서 성능이 크게 개선됐어요.

** 지연 할당 (Delayed Allocation)**: 데이터를 즉시 디스크에 쓰지 않고, 일단 메모리(페이지 캐시)에 모아뒀다가 실제로 flush할 때 한꺼번에 블록을 할당합니다. 이렇게 하면 연속된 블록을 잡기 쉬워져서 단편화가 줄어들고, 불필요한 할당도 방지할 수 있어요. 다만 flush 전에 크래시가 나면 데이터를 잃을 수 있다는 트레이드오프가 있습니다.

** 멀티블록 할당 **: 여러 블록을 한 번의 요청으로 할당합니다. 기존에는 블록 하나씩 할당해서 단편화가 심했어요.

** 최대 파일 크기 16TB, 최대 볼륨 크기 1EB**(Exabyte).

저널링 모드

ext4는 세 가지 저널링 모드를 제공합니다.

모드저널에 기록하는 대상성능안전성
journal메타데이터 + 데이터가장 느림가장 안전
ordered (기본값)메타데이터만 저널링, 데이터는 메타 커밋 전에 먼저 씀중간대부분 안전
writeback메타데이터만 저널링, 데이터 순서 보장 안 함가장 빠름데이터 손실 가능

기본값인 ordered 모드가 핵심입니다. 메타데이터만 저널링하되, 데이터를 메타데이터보다 ** 먼저** 디스크에 쓰는 순서를 보장해요. 이렇게 하면 크래시 후에도 "inode는 새 블록을 가리키는데 블록에는 쓰레기 데이터가 들어있는" 상황을 피할 수 있습니다.


XFS

SGI가 만든 고성능 파일 시스템으로, ** 대용량 파일과 높은 병렬 I/O**에 강합니다. RHEL 7부터 기본 파일 시스템으로 채택되기도 했어요.

특징

  • **B+Tree 기반 구조 **: inode 할당, 여유 공간 관리, Extent 매핑 등 내부 구조 대부분이 B+Tree로 되어 있어요. 검색·삽입·삭제가 O(log n)이라 파일 수가 많아져도 성능 저하가 적습니다
  • Allocation Groups: 파일 시스템을 여러 AG(Allocation Group)로 나누어 각 AG가 독립적으로 메타데이터를 관리합니다. 서로 다른 AG에 대한 작업은 병렬 처리가 가능해서 멀티스레드 I/O에 유리해요
  • ** 지연 할당 지원 **: ext4와 마찬가지입니다
  • ** 온라인 확장 가능 **: 마운트된 상태에서 파일 시스템 크기를 늘릴 수 있어요. 단, ** 축소는 불가능 **합니다

XFS는 대용량 파일을 순차적으로 읽고 쓰는 워크로드(로그, 미디어, 빅데이터 등)에서 ext4보다 좋은 성능을 보여줍니다. 반면 작은 파일이 많은 환경에서는 ext4가 유리한 경우가 있어요.


ext4 vs XFS vs Btrfs 비교

항목ext4XFSBtrfs
** 구조**Extent 기반B+Tree 기반CoW B-Tree
** 최대 파일 크기**16TB8EB16EB
** 최대 볼륨 크기**1EB8EB16EB
** 저널링**메타데이터(+데이터)메타데이터CoW로 대체
** 스냅샷**미지원미지원네이티브 지원
** 체크섬**메타데이터만메타데이터만데이터 + 메타데이터
** 축소**가능불가능가능
** 디스크 Defrag**e4defragxfs_fsr자동
** 적합한 환경**범용, 안정성 우선대용량 파일, 고병렬 I/O스냅샷 필요, 데이터 무결성
** 안정성**매우 높음높음발전 중

Btrfs 는 좀 특이합니다. Copy-on-Write(CoW) 방식을 써서 데이터를 덮어쓰지 않고 항상 새 위치에 기록하기 때문에, 저널링 없이도 일관성을 보장할 수 있어요. 스냅샷, 서브볼륨, 데이터 체크섬 같은 현대적 기능을 내장하고 있지만, 아직 프로덕션에서 100% 신뢰하기엔 불안하다는 의견이 있습니다. SUSE에서는 기본으로 쓰고 있고, Facebook(Meta)도 내부적으로 사용한다고 알려져 있어요.


가상 파일 시스템 (VFS)

리눅스 커널은 VFS(Virtual File System) 라는 추상화 계층을 제공합니다. 유저 프로그램이 open(), read(), write() 같은 시스템 콜을 호출하면, VFS가 중간에서 받아서 실제 파일 시스템(ext4, XFS, NFS 등)의 구현체에 넘겨줘요.

PLAINTEXT
사용자 프로세스

    │ open(), read(), write(), stat()

 ┌──────────────────────────┐
 │     VFS (Virtual FS)     │   ← 공통 인터페이스
 └──────────────────────────┘
    │          │          │
    ▼          ▼          ▼
  ext4       XFS       NFS(네트워크)

VFS 덕분에 프로그램은 파일이 ext4에 있든, NFS 서버에 있든, /proc 같은 가상 파일 시스템에 있든 신경 쓸 필요가 없습니다. 그냥 같은 시스템 콜을 쓰면 돼요. 이건 객체지향의 다형성과 비슷한 구조로, VFS가 인터페이스를 정의하고 각 파일 시스템이 그걸 구현하는 형태입니다.

VFS의 핵심 객체는 네 가지예요.

객체역할
superblock파일 시스템 전체 정보 (블록 크기, inode 수, 마운트 정보 등)
inode개별 파일의 메타데이터
dentry디렉토리 엔트리 (경로 → inode 매핑, 캐싱)
file열린 파일 상태 (현재 오프셋, 접근 모드 등)

주의할 점

"파일을 삭제하면 데이터가 실제로 지워지나요?"

아닙니다. rm 명령은 디렉토리 엔트리를 제거하고 inode의 링크 카운트를 줄일 뿐이에요. 링크 카운트가 0이 되면 inode와 데이터 블록이 "사용 가능"으로 표시되지만, ** 실제 데이터는 디스크에 그대로 남아있습니다 **. 새 파일이 해당 블록을 덮어쓰기 전까지는 복구 도구로 살릴 수 있어요. 이래서 보안이 중요한 환경에서는 shred 같은 명령으로 블록을 의미 없는 데이터로 덮어써야 합니다.

다만 SSD는 이야기가 달라요. TRIM 커맨드가 블록을 컨트롤러에 알려주면, SSD 내부적으로 해당 블록을 소거할 수 있습니다. 이 경우 복구가 훨씬 어려워요.

"디스크 단편화가 왜 발생하고, 리눅스에서는 문제 안 되나요?"

파일을 생성·삭제·수정하다 보면 빈 공간이 파편화되고, 새로운 파일이 불연속적인 블록에 나뉘어 저장되는 게 단편화입니다. HDD에서는 헤드가 여기저기 이동해야 해서 성능이 떨어져요.

리눅스 파일 시스템들은 지연 할당이나 선할당(preallocation) 기법으로 단편화를 최소화합니다. 그래서 Windows NTFS처럼 주기적으로 조각 모음을 해야 할 일은 드물어요. 하지만 "절대 안 생긴다"는 건 아닙니다. 디스크 사용률이 90% 이상이 되면 연속 공간을 잡기 어려워져서 단편화가 급격히 증가해요.

SSD는 랜덤 접근 성능이 좋아서 단편화가 성능에 미치는 영향이 미미합니다. 다만 SSD 조각 모음은 의미가 없을 뿐 아니라 수명을 깎으니 하면 안 돼요.

"RAID 레벨 설명해주세요 — 0, 1, 5, 10"

파일 시스템 이야기를 하다 보면 RAID까지 꼬리가 뻗는 경우가 있어요.

RAID 레벨구성특징최소 디스크
RAID 0스트라이핑데이터를 분산 저장. 속도 2배, 하나 죽으면 전체 데이터 소실2개
RAID 1미러링동일 데이터를 두 디스크에 복제. 읽기 빠름, 용량 절반2개
RAID 5스트라이핑 + 패리티분산 패리티로 1개 디스크 장애 복구 가능. 쓰기 시 패리티 계산 오버헤드3개
RAID 10미러링 + 스트라이핑 (RAID 1+0)미러 쌍을 스트라이핑. 성능과 안정성 모두 확보, 비쌈4개

실무에서 DB 서버에는 RAID 10, 로그 서버에는 RAID 5를 많이 씁니다. RAID 0은 데이터를 잃어도 되는 캐시 서버 정도에만 쓸 법해요.


관련 개념 연결

  • ** 가상 메모리 **: 페이지 캐시를 통해 파일 시스템 I/O와 직결됩니다. mmap()은 파일을 가상 메모리에 매핑해서 읽는 방식이에요
  • ** 리눅스 기본 **: 파일 권한(chmod, chown), FHS 디렉토리 구조는 리눅스 기본편에서 정리했습니다
  • DB 인덱스(B+Tree): XFS가 내부적으로 사용하는 B+Tree는 DB 인덱스의 동작 원리와 동일한 자료구조예요. 디스크 기반 검색에서 B+Tree가 선택되는 이유가 같습니다

정리

파일 시스템을 설명할 때는, 계층별로 구분하면 깔끔합니다.

  1. ** 물리 계층 **: 섹터, 블록 — 디스크 I/O 단위
  2. ** 구조 계층 **: inode, 디렉토리 엔트리 — 메타데이터 관리
  3. ** 할당 전략 **: 연속·연결·인덱스 — 블록 배치 방법
  4. ** 일관성 **: 저널링, CoW — 크래시 복구
  5. ** 추상화 **: VFS — 다양한 파일 시스템을 통일된 인터페이스로

각 계층에서 어떤 문제를 해결하는지를 중심으로 이야기하면 꼬리 질문이 와도 자연스럽게 연결할 수 있어요.

댓글 로딩 중...