프롤로그 - "왜 용량이 부족하지?"
개발자 초년생 시절, 저는 머신러닝 데이터셋을 관리하고 있었습니다.
300GB짜리 이미지 데이터셋(dataset_v1)이 있었는데, 실험을 위해 폴더 구조를 조금 바꿔야 했습니다.
원본을 건드리기 무서워서 복사(Copy)를 했습니다 (cp -r dataset_v1 dataset_v2).
순식간에 제 맥북의 디스크 용량은 바닥났고, "Disk Full" 경고와 함께 빌드가 멈췄습니다. 선배가 지나가면서 한마디 툭 던지더군요. "야, 그걸 통째로 복사하면 어떡해? 심볼릭 링크(Symbolic Link) 걸어야지."
"네? 윈도우 바로가기 같은 건가요?" "비슷한데 달라. 아, 그리고 데이터 중요하면 하드 링크(Hard Link) 써야 할 수도 있고."
그날 저는 ln -s 명령어를 처음 배웠고, 파일 시스템의 심연인 Inode의 세계로 입문했습니다.
오늘은 저처럼 무지성 복사 붙여넣기로 디스크를 괴롭히는 분들을 위해, 리눅스 파일 시스템의 비밀을 파헤쳐 봅니다.
1. 파일이란 무엇인가? (The Truth about Files)
우리는 파일 탐색기에서 파일 아이콘을 보면 "아, 저게 파일이구나"라고 생각합니다. 하지만 운영체제(Linux/Unix/macOS) 입장에서 파일은 두 부분으로 나뉩니다.
- Inode (Index Node): 파일의 실체.
- 파일의 메타데이터(크기, 권한, 소유자, 생성 시간 등)와 디스크 상의 실제 데이터 위치(Block Address)가 담겨 있는 고유한 식별자입니다.
- 주민등록초본이나 DNA 같은 존재입니다.
- Filename (파일 이름): 파일의 껍데기.
- 우리가 보는
report.txt라는 이름은 단지 특정 Inode 번호를 가리키는 포인터(링크)일 뿐입니다. - 디렉토리(폴더)는 사실 "파일명"과 "Inode 번호"의 매핑 테이블을 담고 있는 특수한 파일입니다.
- 우리가 보는
충격적 사실: 우리가 파일을 지우는
rm file.txt명령어는, 사실 파일 데이터를 지우는 게 아닙니다. 단지 파일 이름(껍데기)과 Inode(실체)의 연결을 끊는 것(Unlink)입니다. 연결된 이름이 0개가 되면(Reference Count = 0), 그때 비로소 OS가 "아, 이 Inode는 아무도 안 쓰는구나" 하고 디스크 공간을 회수합니다.
2. Hard Link (하드 링크) - "분신술"
"하나의 Inode에 여러 개의 이름표를 붙이는 것"
- 명령어:
ln target.txt hardlink.txt - 비유: "한 사람(Inode)이 '김철수'라는 본명과 '제임스'라는 예명을 동시에 쓰는 것"
- 김철수가 살을 빼면 제임스도 살이 빠집니다 (같은 몸이니까).
- 김철수라는 이름표를 떼어내도(삭제), 제임스라는 이름표가 남아있으면 그 사람은 사라지지 않습니다.
- 특징:
target.txt와hardlink.txt는 완전히 동일한 파일입니다. (Inode 번호가 같음)ls -i명령어로 확인해보면 Inode 번호가 똑같이 나옵니다.- 파일 내용을 수정하면 양쪽 다 반영됩니다.
- 용량을 차지하지 않습니다! (디렉토리 엔트리에 이름만 하나 추가될 뿐).
진짜 삭제되지 않는 파일의 비밀
하드 링크 파일 중 원본(target.txt)을 지워도 hardlink.txt는 멀쩡히 살아있습니다.
데이터가 복사된 게 아닌데 어떻게 가능할까요?
Inode에는 Link Count라는 숫자가 있습니다. 하드 링크를 만들면 이 숫자가 1에서 2로 올라갑니다.
rm target.txt를 하면 카운트가 2 -> 1로 줄어들 뿐, 0이 아니므로 데이터는 유지됩니다.
마지막 rm hardlink.txt를 해야 카운트가 0이 되어 진짜로 삭제됩니다.
언제 쓸까? (Real World)
- 백업 시스템: Apple의 Time Machine 백업이 이 방식을 씁니다.
- 오늘 백업할 때, 어제와 내용이 똑같은 파일은 복사하지 않고 하드 링크만 겁니다.
- 그래서 수백 GB를 매일 백업해도 디스크가 터지지 않는 겁니다. 사용자는 "어제 폴더"와 "오늘 폴더"에 각각 온전한 파일이 있는 것처럼 보입니다.
- Git: Git 내부적으로 객체(Blob)를 저장할 때 중복을 줄이기 위해 비슷한 메커니즘을 씁니다.
3. Symbolic Link (심볼릭 링크/Soft Link) - "바로가기"
"원본 파일의 '경로(Path)'를 가리키는 별도의 파일"
- 명령어:
ln -s target.txt symlink.txt - 비유: "바탕화면의 '롤 바로가기' 아이콘"
- 아이콘을 더블클릭하면
C:\Program Files\LoL\LeagueClient.exe를 실행해 줍니다.
- 아이콘을 더블클릭하면
- 특징:
symlink.txt는 자기만의 새로운 Inode를 가집니다. (원본과 다름).- 이 파일의 내용물은 원본 파일의 "주소 텍스트(/usr/bin/python)" 그 자체입니다.
- 파일 크기가 아주 작습니다 (몇 바이트 수준).
- 원본(
target.txt)을 지우면? 심볼릭 링크는 "깨진 링크(Broken Link)"가 됩니다. 클릭해도No such file에러가 뜹니다.
언제 쓸까? (Real World)
- 버전 관리:
/usr/bin/python->python3.9- 파이썬 버전을 3.10으로 업데이트해도, 심볼릭 링크만
python3.10을 가리키도록 갱신하면 됩니다. - 스크립트들은 여전히
/usr/bin/python만 호출하면 되므로, 코드 수정 없이 버전을 갈아끼울 수 있습니다.
- 파이썬 버전을 3.10으로 업데이트해도, 심볼릭 링크만
- 설정 파일(Dotfiles) 관리:
- 제 맥북의 환경 설정(
.zshrc,.vimrc)은 실제로는 Dropbox 폴더에 있고, 홈 디렉토리(~/)에는 심볼릭 링크만 둡니다. - 이렇게 하면 포맷을 해도 설정 파일은 클라우드에 안전하게 남습니다.
- 제 맥북의 환경 설정(
4. 결정적 차이 - 왜 하드 링크는 잘 안 쓸까?
하드 링크가 더 안전해 보이고(원본 지워도 살아남음) 빠르지만, 치명적인 단점 2가지 때문에 평소엔 잘 안 씁니다.
- 디렉토리(폴더)에는 하드 링크 불가:
- 만약 폴더 A 안에 폴더 B가 있고, 폴더 B 안에 다시 폴더 A를 하드 링크로 넣는다면?
- 무한 루프(Cycle)가 생깁니다.
find같은 탐색 프로그램이 영원히 돌다가 뻗어버립니다. - 그래서 OS 차원에서 디렉토리 하드 링크 생성을 막아놨습니다. (Superuser도 못 합니다).
- 다른 파일 시스템 간 연결 불가:
- 하드 링크는 Inode 번호를 공유한다고 했죠?
- 하지만 Inode 번호는 파일 시스템(파티션)마다 따로 매겨집니다.
- 내 하드디스크(
C:)의 Inode 100번과 USB 드라이브(D:)의 Inode 100번은 전혀 다른 겁니다. - 그래서 드라이브를 넘나드는 하드 링크는 불가능합니다.
반면 심볼릭 링크는:
- 폴더도 링크 가능.
- 다른 드라이브, 네트워크 드라이브(NAS)도 링크 가능. 그래서 우리가 접하는 99%의 링크는 심볼릭 링크입니다.
5. 개발자를 위한 심화: npm vs pnpm
Node.js 개발자라면 node_modules 폴더가 블랙홀처럼 무겁다는 걸 알 겁니다. 왜 그럴까요?
npm의 방식 (Copy & Paste)
A 프로젝트와 B 프로젝트가 둘 다 React v18을 쓴다면?
npm은 각각의 node_modules 폴더에 React 파일을 물리적으로 따로 저장(복사)합니다.
프로젝트가 100개면 React도 100번 저장됩니다. 디스크 용량 낭비가 심하고, 설치 속도(I/O)도 느립니다.
pnpm의 혁신 (Hard Link)
pnpm(Performant NPM)은 하드 링크를 적극적으로 활용합니다.
- 모든 패키지를
~/.pnpm-store라는 글로벌 저장소에 딱 한 번만 다운로드합니다. - 각 프로젝트의
node_modules에는 그 저장소 파일을 가리키는 하드 링크를 생성합니다.
결과:
- 용량 절약: 프로젝트가 1,000개라도 React 파일은 디스크에 딱 하나만 존재합니다.
- 설치 속도: 파일 복사보다 하드 링크 생성이 훨씬 빠릅니다. 거의 순식간입니다.
이 원리를 알고 난 뒤, 저는 모든 프로젝트를 npm에서 pnpm으로 마이그레이션했습니다. 맥북 용량이 50GB는 늘어났습니다.
6. Docker와 CI 캐싱의 비밀
하드 링크는 Docker 이미지 레이어와 CI/CD 캐싱에서도 핵심적인 역할을 합니다.
Docker Layer Caching
Docker 이미지는 여러 레이어(Layer)로 구성됩니다.
COPY . . 명령어를 실행할 때, Docker는 변경된 파일만 새로운 레이어로 만듭니다.
하지만 내부적으로는 OverlayFS 같은 Union File System을 사용하는데, 이들은 하드 링크와 유사한 메커니즘으로 동일한 데이터를 공유합니다.
만약 모든 레이어를 매번 복사했다면, Docker 이미지는 수십 GB가 되었을 겁니다.
CI/CD Caching (GitHub Actions)
GitHub Actions의 actions/cache도 비슷합니다.
node_modules를 캐시에서 복원할 때, 단순히 압축을 푸는 것보다 하드 링크를 활용하면 복원 속도가 비약적으로 빨라집니다.
특히 Monorepo 환경(Turborepo, Nx)에서는 패키지 간의 의존성을 하드 링크로 연결하여 빌드 시간을 단축시킵니다.
요약 - 한 눈에 비교하기
| 특징 | Hard Link (하드 링크) | Symbolic Link (심볼릭 링크) |
|---|---|---|
| 정체 | Inode에 붙은 여분의 이름표 | 원본 주소를 적어둔 별도의 파일 |
| Inode 번호 | 원본과 같음 | 원본과 다름 (새 파일) |
| 원본 삭제 시 | 파일은 살아있음 (데이터 유지) | 링크 파일은 깨짐 (데드 링크) |
| 대상 | 파일만 가능 (디렉토리 불가) | 파일 & 디렉토리 모두 가능 |
| 범위 | 같은 드라이브(파티션) 내에서만 | 다른 드라이브/네트워크 가능 |
| 비유 | 예명 (본캐/부캐 둘 다 나임) | 바로가기 아이콘 |
마치며 - rm -rf /의 공포를 넘어서
이제 rm 명령어가 덜 무섭습니다.
"아, 내가 지우는 건 파일 데이터가 아니라, 단지 연결(Link)을 끊는 거구나."
하지만 역설적으로 rm -rf /는 더 무섭게 다가옵니다.
이건 모든 디렉토리의 연결 고리를 끊어서, OS가 "어? 이 파일들 아무도 안 쓰네?" 하고 모조리 Garbage Collection 해버리게 만드는 주문이니까요. (복구 불가).
여러분의 소중한 데이터를 위해, 무작정 복사(cp)하기 전에 링크(ln)를 고려해보세요.
특히 Node.js를 쓴다면 pnpm은 선택이 아니라 필수입니다. 디스크가 여러분에게 감사할 겁니다.