| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 | 31 |
- DDD
- 로그백
- TDD
- 단위테스트
- 커뮤니티서버
- model
- index
- JPA
- 이벤트핸들러this
- 냄새라도
- 공통기술
- 사면초가
- 디스크i/o
- springsecurity
- OOP
- string
- Ajax
- 클린아키텍처
- MVC요청플로우
- Transaction
- SEQUENCE
- JDBC
- AOP
- 공통규약
- SpringLegacy
- 전전긍긍
- 이벤트핸들러등록
- 문제선택근거
- 설정세팅
- MVC
- Today
- Total
개발이군고구마
[리눅스 심화 ②] 패킷은 어떻게 흐르는가: 소켓부터 컨테이너 네트워크까지 본문
0. 들어가며: 네트워크도 결국 파일과 프로세스다
이 시리즈의 관통 주제는 "고급 개념은 리눅스 기초의 조합"임. 네트워크도 예외가 아님.
- 소켓은 결국 파일 디스크립터(FD)임 → [기초편]의 "모든 것은 파일이다"와 연결됨.
- 컨테이너 네트워크는 NET namespace + iptables의 조합임 → [프로세스편]과 연결됨.
과거 스테이징 환경 디버깅에서 정리했던 OSI 계층(L3/L4/L7), 포워드 프록시, SNAT 개념이 이 글에서 하나의 그림으로 합쳐짐. 아래 순서로 진행함.

1. 패킷의 여정: 애플리케이션에서 NIC까지
애플리케이션이 데이터를 보낼 때, 그 데이터는 곧바로 랜선으로 나가지 않음. 커널의 여러 계층을 통과하며 각 계층이 헤더(주소표)를 하나씩 덧붙이는 과정을 거침. 이를 캡슐화(encapsulation)라 함.

수신은 정확히 이 반대 순서로, 각 계층이 헤더를 하나씩 벗겨내며(역캡슐화) 최종적으로 애플리케이션에 데이터를 전달함.
1.1 OSI 계층과 실무 개념의 매핑
과거에 정리했던 OSI 계층을 실무에서 마주치는 장비·개념과 연결하면 아래와 같음. "이 문제는 몇 계층 문제인가"를 판단하는 것이 네트워크 트러블슈팅의 첫걸음임.
| 계층 | 이름 | 다루는 주소 | 대표 장비·개념 |
| L7 | 애플리케이션 | URL, HTTP 헤더 | 프록시(nginx), API Gateway, WAF |
| L4 | 전송 | 포트 번호 | L4 로드밸런서, TCP/UDP, 방화벽 |
| L3 | 네트워크 | IP 주소 | 라우터, NAT, iptables |
| L2 | 데이터링크 | MAC 주소 | 스위치, ARP |
💡 SNAT vs 프록시의 근본 차이가 여기서 명확해짐. SNAT는 L3(IP 헤더)만 바꿔치기하는 것이라 애플리케이션은 눈치채지 못함. 반면 프록시는 L7에서 연결을 한 번 끊고 대신 맺어주는 것이라 연결 자체가 두 개로 나뉨. 같은 "중계"처럼 보여도 동작 계층이 완전히 다름. 이 차이가 스테이징 프록시 디버깅에서 헷갈렸던 핵심이었음.
2. 소켓과 포트: 프로세스가 네트워크에 연결되는 접점
2.1 소켓은 파일 디스크립터다
[기초편]에서 "모든 것은 파일"이라 했음. 네트워크 통신도 예외가 아님. 프로세스가 네트워크로 통신할 때, 커널은 소켓(socket)이라는 특수 파일을 열어주고, 그 소켓에 접근하는 파일 디스크립터(FD)를 프로세스에 발급함.
즉 프로세스 입장에서 네트워크 통신은 "어떤 FD에 데이터를 읽고 쓰는 것"일 뿐임. 디스크 파일에 쓰는 것과 문법상 동일함. 이것이 리눅스 네트워크의 출발점임.

2.2 포트: 하나의 IP 안에서 프로세스를 구분하는 번호
IP 주소는 "어느 서버인가"를 식별함(L3). 하지만 서버 하나에는 웹 서버, DB, SSH 등 여러 프로세스가 동시에 통신함. 이들을 구분하는 것이 포트(port)임(L4).
- IP:Port 조합이 통신의 최종 종착지를 결정함. (예:
10.0.1.5:8080) - 하나의 연결은 출발지 IP:Port ↔ 도착지 IP:Port의 4가지 값 조합으로 유일하게 식별됨.
💡 [프로세스편]에서 "컨테이너마다 80 포트를 각자 쓸 수 있다"고 한 것이 이제 명확해짐. 각 컨테이너가 독립된 NET namespace를 가져 자기만의 IP:Port 공간을 갖기 때문임.
3. ss로 TCP 상태 읽기: LISTEN, ESTABLISHED, TIME_WAIT
소켓의 상태를 읽는 도구가 ss임(구형 netstat의 후계). TCP 연결이 지금 어떤 상태인지를 보여줌.
3.1 자주 쓰는 ss 명령어
ss -tnlp # TCP, 숫자표시, LISTEN 중인 것, 프로세스명 → "무슨 포트가 열려있나"
ss -tnp # 현재 맺어진 연결 확인 → "누가 나에게 붙어있나"
ss -s # 전체 소켓 통계 요약
ss state time-wait # 특정 상태만 필터링
3.2 TCP 연결의 생성과 종료
TCP 상태를 이해하려면 연결의 시작(3-way handshake)과 끝(4-way termination)을 알아야 함.

3.3 실무에서 반드시 구분해야 할 두 상태
| 상태 | 의미 | 많이 쌓이면? |
LISTEN |
연결을 기다리는 서버 소켓 | 정상 (서비스가 떠 있다는 뜻) |
ESTABLISHED |
현재 데이터를 주고받는 연결 | 정상 (동시 접속 수 지표) |
TIME_WAIT |
연결을 먼저 끊은 쪽이 잔여 패킷 처리를 위해 잠시 대기 | 단기 연결이 폭증하는 신호 |
CLOSE_WAIT |
상대는 끊었는데 내 애플리케이션이 소켓을 안 닫음 | 애플리케이션 버그 |
이 두 가지는 반드시 구분해야 함. 원인 주체가 정반대이기 때문임.
TIME_WAIT폭증: 대개 정상 동작의 부작용임. 짧은 연결을 대량으로 맺고 끊을 때(예: 커넥션 풀 없이 매 요청마다 새 연결) 발생함. 커널 파라미터 튜닝이나 커넥션 재사용(keep-alive, 커넥션 풀)로 완화함.CLOSE_WAIT폭증: 거의 항상 애플리케이션 버그임. 상대방이 FIN을 보냈는데 내 코드가 소켓을close()하지 않아 생김. 방치하면 FD가 고갈되어 "Too many open files" 에러로 이어짐. 코드에서 소켓·커넥션을 제대로 닫는지 점검해야 함.
💡 [기초편]에서 lsof로 프로세스가 연 FD를 봤던 것을 기억할 것. CLOSE_WAIT이 쌓이는 건 결국 FD 누수임. "Too many open files"와 CLOSE_WAIT은 같은 문제의 다른 얼굴임.
4. iptables와 NAT: 컨테이너가 외부와 통신하는 원리
4.1 iptables란
iptables는 리눅스 커널의 방화벽·패킷 처리 프레임워크임. 패킷이 커널을 통과하는 지점마다 규칙(rule)을 걸어 허용/차단/변조함. 크게 두 가지 용도로 씀.
- filter 테이블: 패킷을 허용하거나 막음(방화벽).
INPUT,OUTPUT,FORWARD체인. - nat 테이블: 패킷의 IP·포트를 바꿔치기함(주소 변환).
PREROUTING,POSTROUTING체인.
4.2 NAT: 주소를 바꿔치기하는 L3 기술
NAT(Network Address Translation)는 패킷의 IP 주소를 변환하는 기술임. 방향에 따라 두 종류로 나뉨.
| 종류 | 변환 대상 | 용도 | 방향 |
| SNAT (Source NAT) | 출발지 IP를 바꿈 | 내부 사설 IP → 공인 IP (밖으로 나갈 때) | 아웃바운드 |
| DNAT (Destination NAT) | 도착지 IP를 바꿈 | 공인 IP:Port → 내부 서버 (안으로 들어올 때) | 인바운드 |

💡 SNAT는 프록시가 아님(재확인). SNAT는 L3에서 IP 헤더의 출발지 주소만 갈아끼울 뿐, 연결 자체는 끊지 않고 그대로 통과시킴. 그래서 애플리케이션은 자신의 IP가 바뀐 줄 모름. 이것이 프록시(L7에서 연결을 새로 맺음)와의 결정적 차이임. docker run -p 8080:80이 바로 DNAT 규칙을 iptables에 추가하는 동작임.
5. DNS 조회 흐름: 이름이 IP로 바뀌는 과정
애플리케이션은 api.example.com 같은 이름으로 통신하지만, 실제 패킷은 IP 주소가 있어야 나감(L3). 이 이름→IP 변환이 DNS 조회(resolution)임.
5.1 조회 순서
리눅스는 정해진 순서로 이름을 찾음. 이 순서를 모르면 "코드는 맞는데 왜 특정 서버에서만 접속이 안 되는가" 같은 문제를 못 풂.

5.2 핵심 설정 파일
| 파일 | 역할 |
/etc/hosts |
이름-IP를 직접 매핑(정적). DNS보다 먼저 조회됨 |
/etc/resolv.conf |
질의할 DNS 서버(nameserver) 목록 |
/etc/nsswitch.conf |
조회 순서(files → dns)를 정의 |
💡 실무 활용: 테스트 시 특정 도메인을 강제로 특정 IP로 보내려면 /etc/hosts에 한 줄 추가하면 됨(DNS보다 우선하므로). 반대로 "hosts엔 없는데 조회가 이상하다"면 /etc/resolv.conf의 DNS 서버가 잘못됐거나 응답이 느린 것임. Docker 컨테이너는 보통 /etc/resolv.conf가 내장 DNS(127.0.0.11)를 가리켜 서비스명으로 컨테이너 간 통신이 됨.
6. 종합: 컨테이너 네트워크는 어떻게 완성되는가
이제 모든 조각을 합침. [프로세스편]에서 "컨테이너 = namespace + cgroup + 파일시스템"이라 했음. 그 중 네트워크 격리(NET namespace)가 실제로 어떻게 외부 통신까지 이어지는지를, 이 글의 개념들로 완성함.
컨테이너가 외부와 통신하려면 세 가지 부품이 조합됨.
- NET namespace — 컨테이너에 독립된 네트워크 스택(자기만의 IP, 포트, 라우팅 테이블)을 줌.
- veth pair (가상 랜선) — 한쪽 끝은 컨테이너 안에, 다른 쪽 끝은 호스트에 꽂히는 가상의 랜선 한 쌍. 격리된 namespace를 호스트와 연결함.
- bridge (가상 스위치) + iptables NAT — 호스트의 가상 스위치(
docker0)가 컨테이너들을 묶고, SNAT(아웃바운드)·DNAT(포트 포워딩)로 외부와 연결함.

흐름 정리: 컨테이너 A가 외부로 요청을 보내면 → veth를 타고 docker0 브리지에 도달 → iptables가 SNAT로 출발지를 호스트 IP로 바꿈 → NIC를 통해 외부로 나감. 외부에서 컨테이너로 들어올 땐 반대로 DNAT로 도착지를 컨테이너 IP로 바꿈. 이 모든 게 앞에서 다룬 namespace·veth·NAT의 조합임.
한 줄 정리: 컨테이너 네트워크는 마법이 아니라 "격리된 NET namespace를 veth로 호스트에 잇고, iptables NAT로 외부와 연결한 것"임.
7. 실무 트러블슈팅 체크리스트
| 증상 | 확인 명령어 | 원인·해석 |
| 서비스 접속 안 됨 | ss -tnlp | grep <포트> |
프로세스가 해당 포트를 LISTEN 중인지 확인 |
| "Too many open files" | ss -tnp state close-wait |
CLOSE_WAIT 누수 — 앱이 소켓을 안 닫음 |
| 순간적 커넥션 폭증 | ss -s / ss state time-wait |
TIME_WAIT 확인 — 커넥션 풀·keep-alive 검토 |
| 특정 도메인만 접속 실패 | cat /etc/resolv.conf, nslookup <도메인> |
DNS 조회 실패 또는 /etc/hosts 오설정 |
| 컨테이너에서 외부 통신 불가 | iptables -t nat -L -n |
SNAT(MASQUERADE) 규칙 존재 여부 확인 |
| 방화벽 차단 의심 | iptables -L -n |
filter 테이블의 INPUT/FORWARD 규칙 확인 |
| 어느 계층 문제인지 모름 | ping(L3) → telnet IP 포트(L4) → curl(L7) |
계층을 순서대로 좁혀가며 원인 격리 |
💡 마지막 행이 핵심임. 문제를 만나면 낮은 계층부터 올라가며 원인을 좁힘. ping으로 L3(IP 도달) 확인 → telnet/nc로 L4(포트 열림) 확인 → curl로 L7(HTTP 응답) 확인. 어느 단계에서 막히는지가 곧 문제의 계층임.