Table of contents
- 서버에 올라간 뒤 가장 먼저 치는 명령들
- curl — HTTP를 읽고 쓰는 스위스 칼
- wget — 단순 다운로드에 특화된 도구
- ss / netstat — 누가 어떤 포트를 쥐고 있나
- ping — 거기까지 패킷이 도달하는가
- traceroute — 어디서 막히는지
- dig / nslookup — DNS를 들여다본다
- SSH 기초 — 원격 접속의 표준
- 실전 트러블슈팅 순서
서버에 올라간 뒤 가장 먼저 치는 명령들
원격 서버에 막 올라간 개발자가 제일 먼저 하는 일은 대체로 똑같다. “이 서버가 인터넷과 이어져 있나?”, “어떤 포트가 열려 있나?”, “DNS는 잘 도나?”, “API 서버는 응답하나?” 이 네 가지를 확인하려고 친다는 명령들이 이 글의 주제다.
리눅스의 네트워크 도구는 수십 가지가 있지만, 실제로 매일 쓰는 건 10개 안팎이다. curl, wget, ss, netstat, ping, traceroute, dig, nslookup, ssh. 이 도구들이 서로 어떻게 겹치고 어떻게 다른지만 잡아두면 트러블슈팅 속도가 달라진다.
아래는 도구들을 “어느 계층에서 무엇을 확인하는가”로 묶어본 지도다.
flowchart TB
subgraph L7["애플리케이션 계층"]
CURL["curl / wget<br/>HTTP 요청·다운로드"]
SSH["ssh<br/>원격 쉘·파일전송"]
end
subgraph L4["전송 계층 / 소켓"]
SS["ss / netstat<br/>열린 포트·연결 상태"]
end
subgraph L3["네트워크 계층"]
PING["ping<br/>도달 가능성"]
TR["traceroute<br/>경로 추적"]
end
subgraph DNS["이름 해결"]
DIG["dig / nslookup<br/>DNS 레코드"]
end
CURL --> DIG
CURL --> SS
SSH --> SS
PING --> DIG
TR --> PING
글 전체 흐름도 이 순서로 간다. 상위 계층(HTTP)부터 차례대로 내려가며, 각각 왜 필요한지와 어떤 함정이 있는지를 살펴본다.
curl — HTTP를 읽고 쓰는 스위스 칼
curl은 URL 하나를 받아서 거의 모든 프로토콜로 요청을 보내는 도구다. HTTP, HTTPS, FTP, SMTP, SCP, LDAP 등등. 실전에서는 95% 이상을 HTTP 디버깅에 쓴다.
가장 단순한 사용법은 GET 요청이다.
curl https://httpbin.org/get
httpbin.org은 HTTP 디버깅 전용으로 만들어진 공개 서버다. 요청을 보낸 쪽의 헤더·바디를 그대로 JSON으로 돌려준다. curl을 처음 익힐 때 연습 상대로 좋다.
헤더까지 보고 싶으면 -i(include) 또는 -I(HEAD 요청만) 옵션을 쓴다.
curl -i https://example.com
# HTTP/2 200
# content-type: text/html; charset=UTF-8
# ...본문...
curl -I https://example.com
# HEAD만 보내서 응답 헤더만 확인
POST는 -X POST와 -d를 묶어서 쓴다. JSON 바디를 보낼 때는 Content-Type 헤더를 명시해야 한다.
curl -X POST https://httpbin.org/post \
-H "Content-Type: application/json" \
-d '{"name":"ioob","lang":"ko"}'
자주 놓치는 실전 옵션 몇 가지를 정리해둔다.
-L: 301·302 리다이렉트를 자동으로 따라감-k/--insecure: TLS 인증서 검증 생략 (사내 자체 서명 인증서 디버깅용. 프로덕션에서 상시 쓰면 사고 난다)-v: 요청·응답 헤더 전체 출력. TLS 핸드셰이크 로그까지 보여준다-o <파일>: 응답 본문을 파일로 저장.-O는 URL의 파일명 그대로 저장-w "%{http_code}\n": 바디 대신 상태 코드만 출력. 헬스체크 스크립트에 자주 쓴다-m <초>: 전체 타임아웃
특히 curl -v는 디버깅의 기본기다. TLS handshake가 실패했는지, 어떤 헤더가 주고받아졌는지, 응답 바디 크기가 얼마인지까지 한 번에 보여준다.
curl -v https://api.example.com/health
# * Trying 10.0.0.1:443...
# * Connected to api.example.com
# * TLSv1.3 (OUT), TLS handshake, Client hello (1):
# > GET /health HTTP/2
# > Host: api.example.com
# > User-Agent: curl/8.4.0
# < HTTP/2 200
# < content-type: application/json
“API가 안 된다”는 제보를 받으면 가장 먼저 치는 게 curl -v다. 클라이언트 코드의 문제인지, 네트워크의 문제인지, 서버의 문제인지를 한 화면으로 가른다.
wget — 단순 다운로드에 특화된 도구
wget은 이름 그대로 “웹에서 가져온다”는 도구다. curl과 기능이 겹치지만 철학이 다르다. curl은 일회성 요청 클라이언트에 가깝고, wget은 대량 다운로드·미러링에 특화되어 있다.
wget https://example.com/archive.tar.gz
# 파일을 현재 디렉토리에 저장
자주 쓰는 옵션은 다음과 같다.
-c: 중단된 다운로드를 이어받기 (Continue)-r: 재귀 다운로드 (사이트 미러링)-np/--no-parent: 재귀 시 상위 디렉토리로 올라가지 않음-O <파일>: 출력 파일명 지정--limit-rate=200k: 대역폭 제한
“curl로 충분하지 않나?” 싶을 수 있다. 실전에서는 용도가 갈린다. API 호출·스크립트 자동화는 curl, 큰 파일을 받거나 멈춘 다운로드를 이어받을 때는 wget이 더 간결하다. 컨테이너 이미지의 RUN 명령에 어느 쪽을 쓸지는 팀 컨벤션 따라가면 된다.
ss / netstat — 누가 어떤 포트를 쥐고 있나
서버에 올라가서 “이 포트는 누가 쓰고 있지?”를 확인하는 순간이 반드시 온다. 8080에서 앱이 떠야 하는데 Address already in use가 나거나, 외부에서 포트가 안 열려서 접근이 막힐 때다.
netstat은 전통의 도구인데 오래되어 net-tools 패키지째로 deprecated 됐다. 현대 리눅스는 iproute2 패키지의 ss(socket statistics)를 권장한다. 대부분의 최신 배포판에서 ss가 기본으로 깔려 있다.
가장 자주 쓰는 조합이다.
ss -tlnp
# -t: TCP
# -l: LISTEN 상태
# -n: 이름 대신 숫자 (DNS/서비스 이름 조회 스킵)
# -p: 프로세스 정보
#
# State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
# LISTEN 0 4096 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=1234,fd=3))
# LISTEN 0 511 127.0.0.1:8080 0.0.0.0:* users:(("node",pid=5678,fd=20))
이 출력이 말해주는 건 뚜렷하다. 22번 포트는 sshd가 모든 인터페이스(0.0.0.0)에서 듣고 있고, 8080은 Node 프로세스가 로컬호스트에서만(127.0.0.1) 듣고 있다. 8080을 외부에서 못 접근하는 이유가 방화벽 때문이 아니라 앱이 로컬 바인딩을 하고 있어서라는 사실이 한 줄에 드러난다.
자주 쓰는 변형은 아래와 같다.
# 확립된 TCP 연결만 보기
ss -tnp state established
# UDP 리스너만 보기
ss -ulnp
# 특정 포트만 보기
ss -tlnp sport = :443
# 요약 통계
ss -s
netstat에 익숙하면 다음 매핑표가 도움이 된다.
| netstat | ss |
|---|---|
netstat -tlnp | ss -tlnp |
netstat -tnp | ss -tnp |
netstat -r | ip route |
netstat -i | ip -s link |
netstat을 계속 쓰는 레거시 스크립트를 유지할 게 아니라면, 새 글과 새 도커 이미지에는 ss 기준으로 적어두는 쪽을 권한다.
ping — 거기까지 패킷이 도달하는가
ping은 원리가 단순하다. 대상에게 ICMP Echo Request를 보내고, 돌아오는 Echo Reply를 재서 응답 시간을 찍는다. “서버가 살아있나?”가 아니라 “내 호스트에서 거기까지 패킷이 도달하나?”를 확인하는 도구다.
ping -c 4 8.8.8.8
# PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
# 64 bytes from 8.8.8.8: icmp_seq=1 ttl=117 time=3.42 ms
# ...
# --- 8.8.8.8 ping statistics ---
# 4 packets transmitted, 4 received, 0% packet loss, time 3005ms
# rtt min/avg/max/mdev = 3.42/3.51/3.60/0.07 ms
-c 4는 4번 보내고 끝낸다는 뜻이다. 지정 안 하면 Ctrl+C 전까지 계속 보낸다.
유용한 옵션 몇 개를 더 추가하자.
-i <초>: 인터벌 조정 (기본 1초)-s <바이트>: 패킷 크기. MTU 문제를 진단할 때 쓴다-W <초>: 응답 대기 타임아웃-4/-6: IPv4/IPv6 강제
다만 주의할 점이 하나 있다. ICMP는 보통 방화벽에서 차단되곤 한다. AWS EC2 인스턴스나 GCP VM에선 기본 보안 그룹이 ICMP를 허용하지 않는다. “ping이 안 된다”고 해서 반드시 서버가 죽은 건 아니다. HTTP/TCP로 확인해야 할 때가 더 많다.
traceroute — 어디서 막히는지
ping이 “도달하나?”를 묻는다면, traceroute는 “도중에 어디를 거치나? 어디서 막히나?”를 묻는다. 각 홉의 라우터까지의 RTT를 측정해서 경로를 그린다.
traceroute www.google.com
# traceroute to www.google.com (142.250.76.100), 30 hops max
# 1 _gateway (192.168.1.1) 1.2 ms 1.1 ms 1.1 ms
# 2 10.0.0.1 (10.0.0.1) 8.3 ms 8.1 ms 8.2 ms
# 3 * * *
# 4 72.14.218.11 (72.14.218.11) 18.2 ms 17.5 ms 17.9 ms
# ...
# 10 142.250.76.100 25.0 ms 24.8 ms 25.1 ms
* * *로 찍힌 홉은 “응답이 없는 라우터”를 뜻한다. 반드시 장애는 아니다 — 많은 라우터가 traceroute용 ICMP/UDP에 대해 응답을 막아둔다. “패킷이 실제로 버려진다”와 “단순히 응답을 안 한다”를 구분하려면 그다음 홉이 응답하는지로 판단한다.
모던 배포판에서는 tracepath, mtr(traceroute + ping)을 대신 쓰기도 한다. mtr은 특히 실시간으로 홉마다의 패킷 손실률을 보여줘서 네트워크 품질 진단에 탁월하다.
mtr www.google.com
# HOST: myhost Loss% Snt Last Avg Best Wrst
# 1. _gateway 0.0% 10 1.2 1.2 1.1 1.4
# 2. 10.0.0.1 0.0% 10 8.3 8.2 8.0 8.5
# 3. 72.14.218.11 20.0% 10 18.1 18.3 17.5 19.2
3번째 홉의 Loss%가 20%로 찍히면 그 구간에 문제가 있다고 좁혀 말할 수 있다.
dig / nslookup — DNS를 들여다본다
“curl로 API를 부르는데 Could not resolve host가 뜬다.” 이때 필요한 게 DNS 진단이다.
dig(Domain Information Groper)는 BIND 패키지에서 나온 DNS 조회 도구다. 출력 포맷이 기계적이라 스크립트에도 쓰기 좋다.
dig example.com
# ;; ANSWER SECTION:
# example.com. 86400 IN A 93.184.216.34
특정 레코드 타입을 지정할 수도 있다.
dig MX gmail.com # 메일 레코드
dig AAAA google.com # IPv6
dig NS example.com # 네임서버
dig TXT example.com # TXT (SPF, DKIM 등)
+short 옵션을 붙이면 값만 나와서 스크립트에 바로 넘기기 좋다.
dig +short example.com
# 93.184.216.34
특정 DNS 서버에 직접 물어볼 수도 있다. “내 머신의 DNS는 이상한데, 공용 DNS로는 정상인가?”를 확인할 때 쓴다.
dig @8.8.8.8 example.com
nslookup도 비슷한 도구인데, 좀 더 오래됐고 출력이 사람 친화적이다.
nslookup example.com
# Server: 8.8.8.8
# Address: 8.8.8.8#53
#
# Non-authoritative answer:
# Name: example.com
# Address: 93.184.216.34
현대 리눅스에선 dig를 권하지만, nslookup밖에 없는 컨테이너 이미지도 여전히 많다. 두 도구 다 머릿속에 넣어두면 편하다.
DNS 캐시 디버깅 팁: 같은 도메인을 계속 치는데 답이 바뀌지 않는다면, 리졸버 캐시 또는 systemd-resolved가 값을 붙들고 있을 수 있다. resolvectl flush-caches(systemd 기반) 또는 sudo systemd-resolve --flush-caches로 캐시를 비운다.
SSH 기초 — 원격 접속의 표준
마지막은 SSH다. 리눅스 서버를 쓰는 이상 SSH는 피할 수 없다. 클라우드 VM, 사내 서버, 홈서버 — 거의 모든 원격 접속이 SSH다.
가장 단순한 형태다.
ssh ioob@10.0.0.5
# 패스워드를 묻거나, 키가 등록되어 있으면 바로 접속
키 기반 인증 — 패스워드는 잊는다
실무에서 패스워드 인증은 거의 안 쓴다. SSH 키 쌍(공개키/개인키)을 만들어 공개키를 서버에, 개인키를 로컬에 둔다. 접속할 때 서버가 “네가 개인키 가진 그 사람 맞냐”를 챌린지-응답으로 확인한다.
키 생성부터 등록까지의 흐름은 이렇다.
sequenceDiagram
autonumber
participant Me as 내 머신
participant Srv as 원격 서버
Me->>Me: ssh-keygen (id_ed25519, id_ed25519.pub 생성)
Me->>Srv: ssh-copy-id user@host<br/>(공개키를 ~/.ssh/authorized_keys에 추가)
Me->>Srv: ssh user@host
Srv->>Me: 공개키로 챌린지
Me->>Srv: 개인키로 서명한 응답
Srv->>Me: 인증 성공 → 쉘 제공
실제 명령은 다음과 같다.
# 1) 키 생성 (Ed25519 권장. RSA보다 짧고 빠르다)
ssh-keygen -t ed25519 -C "ioob@laptop"
# → ~/.ssh/id_ed25519 (개인키), ~/.ssh/id_ed25519.pub (공개키)
# 2) 공개키를 서버에 등록
ssh-copy-id ioob@10.0.0.5
# 3) 이후로는 패스워드 없이 접속
ssh ioob@10.0.0.5
ssh-copy-id가 없는 환경이라면 수동으로 넣어도 된다.
cat ~/.ssh/id_ed25519.pub | ssh ioob@10.0.0.5 "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys"
개인키는 절대 공유하지 않는다. 깃 커밋, 슬랙 메시지, 노션 문서 어디에도 넣지 않는다. 공개키(.pub)만 돌아다니게 한다.
~/.ssh/config — 매번 타이핑하지 않는다
서버가 늘어나면 접속 정보가 중복된다. ~/.ssh/config에 별칭을 넣어두면 한 줄로 접속할 수 있다.
# ~/.ssh/config
Host prod-api
HostName 10.0.0.5
User ioob
Port 22
IdentityFile ~/.ssh/id_ed25519_work
Host bastion
HostName bastion.example.com
User ioob
Host prod-db
HostName 10.0.2.10
User ioob
ProxyJump bastion
이제 ssh prod-api 한 줄로 끝이고, ssh prod-db는 자동으로 bastion을 거쳐 들어간다. ProxyJump는 점프 호스트(bastion)를 거치는 다단 접속을 한 번에 해결해준다.
포트 포워딩 기초 — 터널 하나로 로컬과 원격을 연결
SSH는 그저 쉘 접속 도구가 아니다. 암호화된 TCP 터널을 임의로 파는 도구이기도 하다. 로컬 포트 포워딩이 가장 대표적이다.
“원격 서버의 DB(5432)에 로컬 IDE로 붙고 싶다. 그런데 DB는 외부에 포트를 열고 싶지 않다.” 이 상황을 SSH 터널로 해결한다.
ssh -L 5432:localhost:5432 ioob@prod-api
이 명령의 의미는 이렇다. 내 로컬의 5432 포트로 오는 트래픽을, SSH 연결 너머의 prod-api 서버에서 localhost:5432로 포워딩한다. 로컬 IDE는 jdbc:postgresql://localhost:5432/db로 접근하면 되고, 트래픽은 SSH 암호화 채널을 통해 원격 DB에 닿는다.
flowchart LR
IDE["로컬 IDE<br/>localhost:5432"]
SSH["SSH 터널<br/>암호화된 TCP"]
SRV["prod-api<br/>localhost:5432"]
DB[("PostgreSQL")]
IDE -->|"connect"| SSH
SSH --> SRV
SRV -->|"loopback"| DB
반대 방향, 즉 원격에서 로컬로 포워딩하는 것도 있다(-R). 원격 서버가 내 개발 머신의 특정 포트로 접근하게 할 때 쓴다. 자주 쓰는 구문 몇 개를 정리해두면 좋다.
# 로컬 포워딩: 로컬 L포트 → 원격 타깃
ssh -L 8080:internal-service:80 bastion
# 원격 포워딩: 원격 R포트 → 로컬 타깃
ssh -R 9000:localhost:3000 remote-host
# SOCKS 프록시 (동적 포워딩)
ssh -D 1080 bastion
# → 로컬 1080을 SOCKS5 프록시로 쓸 수 있음
포트 포워딩을 조직 정책에 맞게 써야 한다는 점은 주의할 점이다. 프로덕션 DB에 IDE로 직접 붙는 건 감사·거버넌스 관점에서 문제가 될 수 있다. 팀 정책을 확인한다.
파일 전송 — scp와 rsync
SSH 채널 위에서 파일을 옮기는 도구도 흔하게 쓴다.
# 로컬 → 원격
scp ./deploy.sh ioob@prod-api:/tmp/
# 원격 → 로컬
scp ioob@prod-api:/var/log/app.log ./
# 디렉토리째
scp -r ./dist ioob@prod-api:/var/www/
변경분만 효율적으로 동기화하고 싶을 땐 rsync가 훨씬 빠르다.
rsync -avz -e ssh ./dist/ ioob@prod-api:/var/www/dist/
# -a: 재귀+권한 유지 등
# -v: verbose
# -z: 전송 시 압축
# -e ssh: SSH 채널 사용 (기본값)
scp는 전체를 통째로 복사하지만 rsync는 바뀐 블록만 전송한다. 배포 스크립트에선 rsync가 더 흔하다.
실전 트러블슈팅 순서
“API가 안 된다”는 상황에서 이 글의 도구들을 어떤 순서로 꺼내는지 한 번 짚고 마무리한다.
flowchart TB
A["1. curl -v 로 요청해본다"] --> B{"응답이 오나?"}
B -- "Yes" --> C["200 이 아니면 서버 앱 문제"]
B -- "No" --> D{"Could not resolve host?"}
D -- "Yes" --> E["dig 로 DNS 확인"]
D -- "No" --> F{"Connection refused?"}
F -- "Yes" --> G["ss -tlnp 로 대상 포트 확인"]
F -- "No" --> H{"Timed out?"}
H -- "Yes" --> I["ping / traceroute 로 경로 확인<br/>방화벽·보안그룹 확인"]
이 사고 흐름이 몸에 붙기 시작하면 “왜 안 되지?”로 한참 헤매는 시간이 눈에 띄게 줄어든다. 각 단계에서 필요한 도구를 바로 꺼낼 수 있기 때문이다.
다음 편에서는 systemd와 서비스 관리를 다룬다. 리눅스 서버의 프로세스는 대부분 systemd 서비스로 돌아간다. systemctl로 상태를 다루고, 유닛 파일 구조를 이해하고, journalctl로 로그를 조회하는 방법까지 살펴본다.




Loading comments...