docker.sock이 왜 빠르고 안전한지 이해하려면 OS가 프로세스를 격리하는 방식과 그 격리를 넘는 IPC, 그리고 Unix의 'Everything is a file' 철학까지 알아야 합니다.
docker.sock이 왜 TCP보다 빠른지, 왜 그 파일에 접근하는 것이 root 권한과 동등한지를 제대로 이해하려면 한 단계 아래 레이어부터 봐야 합니다.
“프로세스는 격리되어 있다. 그 격리를 넘어 협력하는 방법이 IPC이고, UDS는 그 방법 중 하나다.
npm run dev로 띄운 하나의 애플리케이션이 하나의 프로세스입니다. OS 관점에서는 실행 중인 프로그램의 인스턴스입니다.
하나의 프로세스는 다음을 독점적으로 소유합니다.
프로세스 A 프로세스 B
┌─────────────────┐ ┌─────────────────┐
│ 자기만의 메모리 │ │ 자기만의 메모리 │
│ 자기만의 변수 │ ✗ │ 자기만의 변수 │
│ 자기만의 파일 │ ←직접접근 │ 자기만의 파일 │
│ 자기만의 핸들 │ 불가능 │ 자기만의 핸들 │
└─────────────────┘ └─────────────────┘
OS가 강제 격리
프로세스가 서로의 메모리에 직접 접근할 수 없도록 OS가 격리합니다. 덕분에 어떤 프로세스가 충돌해도 다른 프로세스는 영향을 받지 않습니다.
RDB에서 트랜잭션이 다른 트랜잭션의 중간 상태를 볼 수 없도록 격리하는 것과 같은 맥락입니다. 격리가 안정성과 보안의 기반입니다.
그런데 이 격리가 새로운 문제를 만들어냅니다. Docker CLI는 어떻게 dockerd에 접근해서 명령할 수 있을까요? 이 질문에 답하는 것이 IPC입니다.
IPC(Inter-Process Communication) 는 격리된 프로세스들이 협력할 수 있게 해주는 메커니즘의 총칭입니다. OS는 다양한 IPC 방식을 제공하며, 각각 특성이 다릅니다.
| 방식 | 비유 | 특징 |
|---|---|---|
| Pipe | 일방향 컨베이어벨트 | 부모-자식 프로세스 간 통신 |
| FIFO | 일방향 우편함 | 무관한 프로세스 간에도 가능 |
| Signal | 알림 종 | 데이터 전송 불가, 신호만 전달 |
| Shared Memory | 공유 화이트보드 | 가장 빠르지만 동기화가 어려움 |
| Message Queue | 우편함 시스템 | 메시지 단위로 전달 |
| UDS | 양방향 메신저 | 가장 강력하고 유연 |
UDS도 이 중 하나입니다. Docker CLI ↔ dockerd 통신에 UDS가 선택된 이유는 이후에 살펴봅니다.
Unix의 가장 유명한 철학 중 하나입니다.
“모든 것은 파일이다. (Everything is a file)
이 철학 아래 프로세스가 다루는 모든 대상이 동일한 인터페이스로 추상화됩니다.
| 대상 | fd 여부 |
|---|---|
| 디스크 파일 | fd |
| 네트워크 소켓 | fd |
| UDS 소켓 | fd |
| 키보드 입력 (stdin) | fd 0 |
| 화면 출력 (stdout) | fd 1 |
| 에러 출력 (stderr) | fd 2 |
| 파이프 | fd |
| 디바이스 (USB, 프린터 등) | fd |
덕분에 모두 동일한 read(), write() 함수로 다룰 수 있습니다.
int file_fd = open("/tmp/data.txt", O_RDONLY);
int sock_fd = socket(AF_INET, SOCK_STREAM, 0);
char buf[100];
read(file_fd, buf, 100); // 파일에서 읽기
read(sock_fd, buf, 100); // 네트워크에서 읽기
// 같은 함수. 같은 인터페이스.
네트워크 소켓이든 파일이든 UDS든 코드 입장에서는 "fd에서 읽는다"는 동작이 동일합니다. 대상이 바뀌어도 코드 구조가 바뀌지 않습니다.
fd는 단순한 정수(0, 1, 2, 3, …)입니다. 각 프로세스는 자신만의 fd 테이블을 갖습니다.
프로세스 A의 fd 테이블
fd 0 → 표준 입력 (stdin)
fd 1 → 표준 출력 (stdout)
fd 2 → 표준 에러 (stderr)
fd 3 → /tmp/data.txt
fd 4 → 네트워크 소켓
fd 5 → UDS 소켓
open(), socket() 같은 함수를 호출하면 OS가 다음 과정을 수행합니다.
1. 실제 객체(파일, 소켓 등) 생성
2. 프로세스 fd 테이블에서 빈 슬롯 탐색
3. 해당 슬롯에 객체 연결
4. 슬롯 번호(fd) 반환
이후 read(fd, ...), write(fd, ...) 를 호출할 때 이 번호를 사용합니다.
fd 번호는 프로세스 단위로 독립적입니다. 프로세스 A의 fd 5와 프로세스 B의 fd 5는 완전히 다른 객체를 가리킵니다. 다른 프로세스에 fd 번호를 전달해봤자 사용할 수 없습니다.
UDS(Unix Domain Socket) 는 같은 컴퓨터 안의 프로세스끼리 통신하기 위한 소켓입니다.
다른 컴퓨터와의 통신을 포함한 범용 소켓입니다.
프로세스 A
↓
TCP/IP 스택
(패킷화 → IP 헤더 → TCP 헤더 → 체크섬 → 라우팅)
↓
네트워크 인터페이스
↓
목적지 (같은 컴퓨터라도 이 경로를 모두 거침)
주소 형식: 192.168.1.1:8080 (IP + Port)
같은 컴퓨터 안에서 Network Socket으로 통신하면, 실제로는 외부 네트워크로 나가지 않더라도 전체 네트워크 스택을 통과합니다.
벤치마크 기준으로 UDS는 localhost TCP 대비 보통 2~5배 빠르고, 레이턴시 차이는 그보다 더 큽니다. 처리량보다 지연 시간 측면에서 이점이 두드러집니다.
프로세스 격리 (OS 보장)
↓ 격리를 넘어 협력이 필요할 때
IPC
↓ 같은 호스트, 양방향, 빠르고 안전해야 할 때
UDS (/var/run/docker.sock)
↓ fd로 추상화되어
read() / write() 로 통신
docker CLI와 dockerd는 같은 호스트 안에 있고, 양방향 통신이 필요하며, 파일 권한으로 접근 제어가 가능해야 합니다. UDS가 이 조건을 모두 만족하는 가장 적합한 선택입니다.