infra
Platform

모듈 맵

[Network] curl 명령어로 HTTP 상태 코드 및 헤더 분석하기

0 / 35 완료

펼치기
0 / 35 완료0%

네트워크 트러블슈팅 · 15 / 35

[Network] curl 명령어로 HTTP 상태 코드 및 헤더 분석하기

curl로 HTTP 헤더, 상태 코드, SSL 인증서를 점검합니다

🚨INCIDENT ALERT
HIGH

운영 반영 직후 헬스체크는 실패하지만 브라우저에서는 페이지가 열립니다. 응답 코드, 헤더, 리다이렉트, TLS 정보를 한 번에 확인하지 못해 원인 파악이 늦어집니다.

curl은 단순 다운로드 도구가 아니라 HTTP 장애를 분해하는 가장 빠른 현장 도구입니다.

HTTP 테스트와 상태 코드 점검 (curl)

웹 서버 응답이 이상하다, API가 갑자기 500 에러를 반환한다, SSL 인증서가 만료됐다는 알림이 왔다 — 이런 상황에서 브라우저를 열기 전에 CLI에서 즉시 확인할 수 있는 도구가 curl입니다. 이 챕터에서는 HTTP 메서드와 상태 코드 체계를 이해하고, curl의 핵심 옵션을 활용해 서버 상태를 정확하게 진단하는 방법을 배웁니다.


이번 챕터에서 배울 것
  • 1HTTP 메서드(GET/POST/PUT/PATCH/DELETE/HEAD/OPTIONS)와 멱등성 개념을 설명할 수 있다
  • 2HTTP 상태 코드 체계(2xx 성공, 3xx 리다이렉션, 4xx 클라이언트 오류, 5xx 서버 오류)를 구분할 수 있다
  • 3502 / 503 / 504 Bad Gateway 오류를 구분하고 원인을 분석할 수 있다
  • 4curl 핵심 옵션(-I, -v, -s, -L, -k, -w, -X, -d, -H)을 실전에서 활용할 수 있다
  • 5SSL/TLS 인증서 만료를 점검하고 openssl로 만료일을 확인할 수 있다
  • 6서버 상태 모니터링과 CI/CD 배포 헬스체크 자동화 스크립트를 작성할 수 있다
실습 환경 준비
curl 설치 확인
curl --version
openssl 설치 확인 (SSL 인증서 점검에 필요)
openssl version
테스트용 HTTP 엔드포인트 (httpbin.org 활용 가능)

인터넷 연결이 필요합니다. 오프라인 환경이라면 로컬에서 python3 -m http.server 8000으로 간이 서버를 실행할 수 있습니다.

bc 명령 설치 확인 (응답 시간 임계값 비교 스크립트에 필요)
bc --version || sudo apt-get install -y bc
💡개념

HTTP 메서드와 상태 코드 체계

백엔드 API를 호출했는데 405 오류가 납니다. 또 다른 경우엔 GET으로 데이터를 삭제하는 코드가 있고, POST와 PUT을 혼용해서 클라이언트가 중복 요청을 보내도 서버가 멱등하게 처리되지 않습니다. HTTP 메서드와 상태 코드 체계를 정확히 알지 못하면 API 설계에서 이런 실수가 반복됩니다.

HTTP 메서드와 상태 코드 체계 — 메서드는 GET(조회·안전)·POST(생성)·PUT(전체 수정·멱등)·DELETE(삭제·멱등)로 용도가 정해짐. 상태 코드는 2xx 성공·3xx 리다이렉트·4xx 클라이언트 오류(405 메서드 불허)·5xx 서버 오류. 메서드와 코드를 알아야 멱등성·오류 원인을 정확히 다룸확대

HTTP 메서드 — 행위를 표현하는 동사

HTTP 요청은 어떤 작업을 할지 메서드(동사)로 표현합니다. REST API 설계와 서버 동작을 이해하는 데 필수적입니다.

메서드의미멱등성주요 사용 예
GET리소스 조회O페이지 조회, API 데이터 읽기
POST리소스 생성X회원 가입, 주문 생성
PUT리소스 전체 교체O사용자 정보 전체 수정
PATCH리소스 부분 수정X이메일만 변경
DELETE리소스 삭제O게시글 삭제
HEAD헤더만 요청O파일 크기 확인, 상태 점검
OPTIONS지원 메서드 조회OCORS 사전 요청

멱등성(Idempotency): 같은 요청을 여러 번 보내도 결과가 동일하면 멱등. GET으로 같은 페이지를 100번 요청해도 서버 상태는 변하지 않습니다.

HTTP 상태 코드 — 서버의 응답 의미

상태 코드는 3자리 숫자로, 첫 번째 자리가 대분류를 나타냅니다.

2xx — 성공

코드이름의미
200OK요청 성공, 응답 바디 포함
201CreatedPOST로 리소스 생성 성공
204No Content성공했지만 반환할 바디 없음 (DELETE 응답 등)

3xx — 리다이렉션

코드이름의미
301Moved Permanently영구 이동, 북마크 갱신 필요
302Found임시 이동
304Not Modified캐시 유효, 바디 없음

4xx — 클라이언트 오류

코드이름의미
400Bad Request잘못된 요청 형식
401Unauthorized인증 필요(미로그인)
403Forbidden인증은 됐지만 권한 없음
404Not Found리소스 없음
429Too Many Requests요청 속도 초과(rate limit)

5xx — 서버 오류

코드이름의미
500Internal Server Error서버 내부 오류(코드 버그 등)
502Bad Gateway게이트웨이가 업스트림에서 유효하지 않은 응답 수신
503Service Unavailable서버 과부하 또는 점검 중
504Gateway Timeout게이트웨이가 업스트림 응답 대기 시간 초과

502 vs 503 vs 504 구분

현장에서 자주 혼동되는 5xx 코드입니다.

클라이언트 → [Nginx 프록시] → [백엔드 앱 서버]

502: 백엔드가 응답했지만 내용이 이상함 (프로토콜 오류, 잘못된 응답)
503: 백엔드 서버가 완전히 다운됨 (연결 거부)
504: 백엔드 서버가 타임아웃 내에 응답하지 않음
500: 백엔드 서버 자체에서 오류 발생 (코드 예외, DB 오류 등)

게이트웨이 5xx 오류가 발생하는 위치 — 클라이언트→Nginx 프록시→백엔드 앱 경로에서 503(Service Unavailable)은 백엔드 완전 다운으로 연결 거부, 504(Gateway Timeout)는 백엔드가 타임아웃 내 무응답, 502(Bad Gateway)는 백엔드가 응답했으나 프로토콜·내용 이상, 500(Internal Server Error)은 백엔드 앱 내부 코드 예외·DB 오류로 게이트웨이 문제가 아니다확대


URL 하나를 여는 전체 생애주기 — 파싱·DNS·TCP·TLS·HTTP·재사용 8단계

💡개념

URL 하나를 여는 전체 생애주기 — curl -w의 시계가 어느 단계를 재는가

주소창에 URL을 넣고 Enter를 누르면 페이지가 뜨는 그 1초 안에, 클라이언트는 URL 해석 → DNS → TCP → (HTTPS면) TLS → HTTP 요청 → 서버 처리 → 응답 → 연결 재사용을 순서대로 밟습니다. 이 단계를 알면 curl -w가 찍는 time_namelookup·time_connect·time_appconnect·time_starttransfer가 각각 어느 단계의 시계인지 읽고, "느리다"의 원인 단계를 정확히 짚을 수 있습니다.

TEXT
[클라이언트]  curl https://www.example.com/users?id=1
   │
   ① URL 파싱      scheme(https)·host·port(443)·path·query 분해
   │
   ② DNS 조회      host → IP (stub → 리졸버, 캐시 미스면 위임 체인)     [time_namelookup]
   │
   ③ TCP 연결      IP:443로 3-way handshake(SYN/SYN-ACK/ACK)         [time_connect]
   │
   ④ TLS 핸드셰이크 (https면) ClientHello…인증서 검증…세션키 합의       [time_appconnect]
   │
   ⑤ HTTP 요청     요청라인(GET /users?id=1) + 헤더(Host·User-Agent·Accept) 송신
   │
   ⑥ 서버 처리     라우팅·앱·DB 후 응답 생성 → 첫 바이트 전송          [time_starttransfer]
   │
   ⑦ 응답 수신     상태코드 + 응답 헤더 + 바디                         [time_total]
   │
   ⑧ 연결 재사용   keep-alive면 TCP·TLS 유지 → 다음 요청은 ②③④ 건너뜀
   ▼
[브라우저]  바디 파싱·렌더 (CSS/JS/이미지마다 요청 반복, 대개 연결 재사용)

각 단계에서 무슨 일이 일어나고, 느리거나 막히면 어디를 보나:

단계하는 일느리거나 막히면
① URL 파싱scheme·host·port·path·query로 분해스킴 누락·오타 → 요청이 시작조차 안 됨
② DNS 조회호스트명을 IP로 해석time_namelookup이 큼 → 리졸버·캐시 문제 / (6) Could not resolve host
③ TCP 연결IP:포트로 3-way handshaketime_connect가 크거나 멈춤 → 포트 차단·방화벽 / (7) Connection refused·타임아웃
④ TLS 핸드셰이크인증서 검증·세션키 합의(https만)time_appconnect가 큼 → TLS 협상 / (60) 인증서 에러
⑤ HTTP 요청메서드·경로·헤더(특히 Host) 송신Host가 틀리면 엉뚱한 vhost 응답(404·다른 사이트)
⑥ 서버 처리업스트림·앱·DB 처리 후 첫 바이트 반환time_starttransfer만 큼 → 서버 내부(TTFB) 지연, 네트워크 아님
⑦ 응답상태코드·헤더·바디 반환5xx=서버·업스트림 문제, 4xx=요청(URL·토큰·권한) 문제
⑧ 연결 재사용keep-alive로 TCP·TLS 연결 유지매 요청 새 연결이면 ②③④를 반복 → 지연 누적

"200이 빨리 떴다"는 ①~⑦이 모두 성공하고 신속했다는 뜻이고, 느릴 때는 어느 시계가 큰지가 원인 단계를 그대로 가리킵니다 — time_namelookup이 크면 DNS(②), time_connect가 크면 TCP·방화벽(③), time_appconnect가 크면 TLS(④), time_starttransfertime_connect의 간격이 크면 서버 처리(⑥)입니다. curl -w로 이 네 구간을 한 줄에 찍어 비교하면 "네트워크가 느린가, 서버가 느린가"가 즉시 갈립니다(바로 아래 실습에서 측정합니다). 그리고 keep-alive(⑧)가 살아 있으면 두 번째 요청부터 ②③④가 통째로 사라져 훨씬 빨라지므로, 첫 요청만 느리고 이후가 빠르다면 정상입니다.


💡개념

curl 핵심 옵션과 SSL 인증서 점검

배포 직후 API가 정상인지 확인하려 합니다. 브라우저에서는 되는데 서버에서 내부 API 호출이 안 됩니다. curl을 써서 직접 요청을 던져보면 원인이 바로 드러나는 경우가 많습니다. 헤더를 붙이는 방법, SSL 인증서 오류를 우회하는 방법, 응답 코드만 확인하는 방법 — 이 옵션들을 모르면 curl은 그냥 wget 대체 도구로만 씁니다.

curl 기본 동작 원리

curl(Client URL)은 다양한 프로토콜을 지원하는 CLI 데이터 전송 도구입니다. HTTP/HTTPS 외에도 FTP, SMTP, DNS 등을 지원합니다.

로컬 터미널
# 실습 디렉토리 준비
mkdir -p /tmp/networking/part3/exam_15 && cd /tmp/networking/part3/exam_15

# 기본 GET 요청 — 응답 바디를 표준 출력으로 출력
curl http://example.com

# HTTPS (SSL/TLS 검증 포함)
curl https://example.com
🔍실행 후 확인할 것
  • HTTP 상태코드 — 첫 번째 확인 대상curl -I 출력 첫 줄 HTTP/1.1 200 OK에서 상태코드를 먼저 확인 — 2xx는 성공, 3xx는 리다이렉트(Location 헤더 추적), 4xx는 클라이언트 오류, 5xx는 서버 오류
  • curl -v 출력 읽기 순서> 로 시작하는 줄은 요청 헤더, < 로 시작하는 줄은 응답 헤더 — * 로 시작하는 줄은 curl 자체 로그(TLS 핸드셰이크, 연결 정보)이므로 응답 분석 시 < 줄만 집중
  • Content-Type과 응답 크기 조합 해석Content-Type: application/json이면 API 응답, text/html이면 웹 페이지 — Content-Length가 0이거나 없으면 빈 응답 바디이므로 오류 메시지가 없더라도 실패일 수 있음

주요 옵션 정리

옵션설명
-IHEAD 메서드로 헤더만 수신
-vverbose — 전체 HTTP 흐름 출력
-ssilent — 진행 표시줄 숨김
-o 파일응답 바디를 파일로 저장
-OURL의 파일명으로 저장
-L리다이렉트 자동 따라가기
-kSSL 인증서 검증 건너뜀 (비보안)
-w 형식요청 완료 후 지정 형식으로 정보 출력
-H "헤더"커스텀 헤더 추가
-d "데이터"POST 바디 데이터
-X 메서드HTTP 메서드 지정
-u 사용자:암호Basic 인증
--connect-timeout N연결 타임아웃(초)
--max-time N전체 요청 타임아웃(초)

SSL/TLS 인증서 점검

HTTPS 사이트의 인증서 만료일, 발급자, SANs(Subject Alternative Names) 정보를 확인하는 방법입니다.

로컬 또는 서버
# 인증서 상세 정보 확인
curl -vI https://example.com 2>&1 | grep -E "expire|issuer|subject|SSL"

# openssl을 이용한 만료일 확인 (더 명확)
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -dates

# 출력 예
# notBefore=Jan  1 00:00:00 2024 GMT
# notAfter=Jan  1 00:00:00 2025 GMT   ← 이 날짜 확인

인증서 만료 N일 전 알림을 자동화하는 확인 방법:

로컬 터미널
#!/bin/bash
# SSL 만료까지 남은 일수 확인
DOMAIN="example.com"
EXPIRY=$(echo | openssl s_client -connect "${DOMAIN}:443" -servername "${DOMAIN}" 2>/dev/null \
  | openssl x509 -noout -enddate | cut -d= -f2)

EXPIRY_EPOCH=$(date -d "$EXPIRY" +%s)
NOW_EPOCH=$(date +%s)
DAYS_LEFT=$(( (EXPIRY_EPOCH - NOW_EPOCH) / 86400 ))

echo "${DOMAIN}: 인증서 만료까지 ${DAYS_LEFT}일 남음"

if [ "$DAYS_LEFT" -lt 30 ]; then
  echo "경고: 30일 이내 만료 예정!"
fi

로컬 터미널
mkdir -p /tmp/networking/part3/http-curl && cd /tmp/networking/part3/http-curl
# 로컬 테스트 서버 스크립트 생성
cat > test_server.py << 'EOF'
from http.server import HTTPServer, BaseHTTPRequestHandler
import json

class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path == '/health':
            self.send_response(200)
            self.send_header('Content-Type', 'application/json')
            self.end_headers()
            self.wfile.write(json.dumps({'status':'ok'}).encode())
        elif self.path == '/api/users':
            self.send_response(200)
            self.send_header('Content-Type', 'application/json')
            self.end_headers()
            data = [{'id':1,'name':'alice'},{'id':2,'name':'bob'}]
            self.wfile.write(json.dumps(data).encode())
        else:
            self.send_response(404)
            self.end_headers()
    def do_POST(self):
        length = int(self.headers.get('Content-Length', 0))
        body = self.rfile.read(length)
        self.send_response(201)
        self.send_header('Content-Type', 'application/json')
        self.end_headers()
        self.wfile.write(json.dumps({'created': True, 'data': body.decode()}).encode())
    def log_message(self, *args): pass

HTTPServer(('127.0.0.1', 8765), Handler).serve_forever()
EOF
echo "# 테스트 서버 실행 (백그라운드)"
echo "# python3 test_server.py &"
HTTP 응답 판단 — 응답시간·상태코드가 무엇을 가리키나
응답 시간 몇 초부터 느린가사용자 체감: 0.1s 즉각, ~1s 매끄러움, 1~3s 인내, 3s↑ 이탈 위험. API 내부 호출은 보통 p95 300ms·p99 1s를 SLO로. 'time_total'을 -w로 측정해 임계 비교
time_namelookup이 큼DNS 해석 지연 — 수백 ms↑면 리졸버·DNS 캐시 문제. curl -w로 단계별 분해(namelookup→connect→starttransfer)해 '어디서 느린지' 특정
time_starttransfer만 큼 (연결은 빠름)서버 처리(TTFB) 지연 — 앱/DB가 느린 것. 네트워크가 아니라 백엔드. '연결은 금방인데 응답이 늦다=서버 내부'
502 vs 503 vs 504502=업스트림이 잘못된 응답(앱 크래시), 503=과부하/점검(일시적), 504=업스트림 타임아웃(느린 백엔드). LB 로그와 함께 보면 원인 계층이 갈린다
타임아웃을 어떻게 거나--connect-timeout 3 --max-time 10 처럼 연결·전체를 분리 지정. 모니터링 스크립트는 반드시 둘 다 — 없으면 한 요청이 영원히 매달려 점검이 멈춘다
간헐적 실패를 잡으려면단발 curl로는 못 봄 — 반복 측정(loop+ -w time_total)으로 p95/p99 분포를 본다. '평균은 정상인데 가끔 느림'은 분위수로만 보인다
curl로 HTTP 헤더와 상태 코드 확인

curl -I 헤더 확인

로컬 또는 서버
# 기본 헤더 확인 (HEAD 메서드)
curl -I https://www.google.com

# 출력 예시
# HTTP/2 200
# content-type: text/html; charset=UTF-8
# date: Mon, 27 Jan 2025 01:00:00 GMT
# expires: Mon, 27 Jan 2025 01:00:00 GMT
# cache-control: private, max-age=0
# server: gws
# x-xss-protection: 0
# x-frame-options: SAMEORIGIN

확인 포인트:

  • HTTP/2 200 — 프로토콜 버전과 상태 코드
  • content-type — 응답 형식 (text/html, application/json 등)
  • server — 웹 서버 소프트웨어 (Nginx, Apache, gws 등)
  • cache-control — 캐싱 정책
로컬 또는 서버
# 리다이렉트를 따라가며 최종 목적지 확인
curl -I -L http://naver.com

# HTTP/1.1 301 Moved Permanently      ← 첫 번째 응답
# location: https://www.naver.com/    ← 리다이렉트 대상
# ...
# HTTP/2 200                          ← 최종 목적지 응답

상태 코드만 빠르게 확인

로컬 또는 서버
# 상태 코드만 숫자로 출력
curl -s -o /dev/null -w "%{http_code}" https://www.google.com
# 출력: 200

# 줄바꿈 추가
curl -s -o /dev/null -w "%{http_code}\n" https://www.google.com

# 여러 사이트 일괄 점검
for url in https://google.com https://naver.com https://example.com; do
  code=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout 5 "$url")
  echo "$url → $code"
done

응답 시간 측정

로컬 또는 서버
# 상세한 타이밍 정보 출력
curl -o /dev/null -s -w "
DNS 조회:      %{time_namelookup}s
TCP 연결:      %{time_connect}s
TLS 핸드셰이크: %{time_appconnect}s
첫 바이트까지:  %{time_starttransfer}s
전체 시간:     %{time_total}s
전송 크기:     %{size_download} bytes
" https://www.google.com

출력 항목 설명:

  • time_namelookup: DNS 조회 시간
  • time_connect: TCP 3-way handshake 완료까지
  • time_appconnect: TLS 핸드셰이크 완료까지 (HTTPS)
  • time_starttransfer: 서버가 첫 번째 바이트를 보낼 때까지
  • time_total: 전체 완료 시간

curl -v verbose로 전체 HTTP 흐름 디버깅

curl -v 출력 구조 이해

로컬 또는 서버
curl -v https://httpbin.org/get

출력에서 각 접두사의 의미:

*   Trying 54.208.105.16:443...         ← DNS/TCP 정보 (* = curl 내부 정보)
* Connected to httpbin.org port 443     ← TCP 연결 성공
* SSL connection using TLSv1.3          ← TLS 버전
* Server certificate:                   ← 인증서 정보 시작
*  subject: CN=httpbin.org
*  start date: Jan  1 00:00:00 2024 GMT
*  expire date: Jan  1 00:00:00 2025 GMT
*  SSL certificate verify ok.           ← 인증서 검증 성공
>                                       ← > = 내가 보낸 요청 헤더
> GET /get HTTP/2
> Host: httpbin.org
> User-Agent: curl/7.88.1
> Accept: */*
>
< HTTP/2 200                            ← < = 서버의 응답 헤더
< date: Mon, 27 Jan 2025 01:00:00 GMT
< content-type: application/json
< content-length: 305
<
{                                       ← 응답 바디 (접두사 없음)
  "headers": { ... }
}

POST 요청으로 API 테스트

로컬 또는 서버
# JSON 바디로 POST 요청
curl -v -X POST https://httpbin.org/post \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer mytoken123" \
  -d '{"username": "testuser", "action": "login"}'

# Form 데이터로 POST
curl -v -X POST https://httpbin.org/post \
  -F "username=testuser" \
  -F "password=secret"

# PUT 요청
curl -X PUT https://httpbin.org/put \
  -H "Content-Type: application/json" \
  -d '{"name": "updated name"}'

# DELETE 요청
curl -X DELETE https://httpbin.org/delete \
  -H "Authorization: Bearer mytoken123"

SSL 인증서 오류 디버깅

로컬 또는 서버
# 인증서 검증 실패 시 상세 정보
curl -v https://expired.badssl.com/ 2>&1

# 주요 에러 메시지
# * SSL certificate problem: certificate has expired     ← 만료
# * SSL certificate problem: self-signed certificate     ← 자체 서명
# * SSL certificate problem: unable to get local issuer certificate ← 체인 불완전

# 임시 우회 (운영 환경에서는 절대 사용 금지!)
curl -k https://expired.badssl.com/

# 특정 CA 인증서 지정
curl --cacert /path/to/company-ca.crt https://internal.example.com/

타임아웃 설정

로컬 또는 서버
# 연결 타임아웃 3초, 전체 응답 타임아웃 10초
curl --connect-timeout 3 --max-time 10 https://example.com

# 응답 없는 서버 빠르게 탐지
curl -s -o /dev/null -w "%{http_code}" \
  --connect-timeout 3 --max-time 5 \
  http://192.168.1.200/health
# 출력: 000 (연결 실패 시)

서버 상태 모니터링 스크립트 작성

기본 헬스체크 스크립트

로컬 터미널
#!/bin/bash
# /usr/local/bin/http-healthcheck.sh

TARGETS=(
  "https://www.example.com|200"
  "https://api.example.com/health|200"
  "https://www.google.com|200"
)

FAILED=0

for entry in "${TARGETS[@]}"; do
  url=$(echo "$entry" | cut -d'|' -f1)
  expected=$(echo "$entry" | cut -d'|' -f2)

  actual=$(curl -s -o /dev/null -w "%{http_code}" \
    --connect-timeout 5 --max-time 10 "$url")

  if [ "$actual" = "$expected" ]; then
    echo "[OK]   $url → $actual"
  else
    echo "[FAIL] $url → 예상: $expected, 실제: $actual"
    FAILED=$((FAILED + 1))
  fi
done

exit $FAILED

응답 시간 임계값 모니터링

로컬 터미널
#!/bin/bash
# 응답 시간이 2초를 초과하면 경고

URL="https://api.example.com/v1/status"
THRESHOLD=2.0

TIMING=$(curl -o /dev/null -s -w "%{time_total}" \
  --connect-timeout 5 --max-time 10 "$URL")

echo "응답 시간: ${TIMING}s"

# bc로 부동소수점 비교
if (( $(echo "$TIMING > $THRESHOLD" | bc -l) )); then
  echo "경고: 응답 시간 임계값(${THRESHOLD}s) 초과!"
  exit 1
fi
echo "정상"

SSL 인증서 만료 일괄 점검

로컬 터미널
#!/bin/bash
# 여러 도메인의 SSL 만료일 일괄 점검

DOMAINS=("www.example.com" "api.example.com" "static.example.com")
WARN_DAYS=30

for domain in "${DOMAINS[@]}"; do
  EXPIRY=$(echo | openssl s_client -connect "${domain}:443" \
    -servername "${domain}" 2>/dev/null \
    | openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2)

  if [ -z "$EXPIRY" ]; then
    echo "[ERROR] $domain: 인증서 조회 실패"
    continue
  fi

  EXPIRY_EPOCH=$(date -d "$EXPIRY" +%s 2>/dev/null)
  NOW_EPOCH=$(date +%s)
  DAYS_LEFT=$(( (EXPIRY_EPOCH - NOW_EPOCH) / 86400 ))

  if [ "$DAYS_LEFT" -lt 0 ]; then
    echo "[EXPIRED] $domain: 이미 만료됨"
  elif [ "$DAYS_LEFT" -lt "$WARN_DAYS" ]; then
    echo "[WARN]    $domain: ${DAYS_LEFT}일 후 만료 (${EXPIRY})"
  else
    echo "[OK]      $domain: ${DAYS_LEFT}일 남음"
  fi
done

cron에 등록하여 매일 점검:

로컬 터미널
# crontab -e
0 9 * * * /usr/local/bin/ssl-check.sh | mail -s "SSL 만료 점검 결과" admin@example.com

증상

로컬 터미널
$ curl -I https://api.example.com/v1/users
HTTP/1.1 502 Bad Gateway
Server: nginx/1.24.0
Date: Mon, 27 Jan 2025 01:00:00 GMT
Content-Type: text/html

원인 분석과 점검 순서

단계 1 — 백엔드 서버 포트 리스닝 확인

로컬 터미널
# Nginx 프록시가 8080으로 백엔드를 바라본다고 가정
ss -antp | grep 8080
# 출력이 없으면 백엔드 서버가 다운된 것

단계 2 — 백엔드 직접 접근 테스트

로컬 또는 서버
# Nginx를 우회하고 백엔드 직접 호출
curl -v http://127.0.0.1:8080/v1/users

# 연결 거부 시
# curl: (7) Failed to connect to 127.0.0.1 port 8080: Connection refused
# → 백엔드 프로세스 다운

# 응답이 오면 Nginx 설정 문제

단계 3 — Nginx 에러 로그 확인

로컬 터미널
# Nginx 에러 로그에서 upstream 관련 메시지 확인
tail -50 /var/log/nginx/error.log | grep upstream

# 예시 에러 메시지
# *1 connect() failed (111: Connection refused) while connecting to upstream
# → 백엔드 서비스 다운
# *1 upstream timed out (110: Connection timed out) while reading response header
# → 백엔드 응답 타임아웃

단계 4 — 백엔드 서비스 재시작

위험 명령어

이 명령은 실행 중인 서비스 상태를 바꿔 순간적인 중단이나 설정 반영 실패를 만들 수 있습니다. 운영 트래픽 영향과 재시작 후 확인 명령을 먼저 준비하세요.

서버 터미널
# 애플리케이션 서비스 상태 확인 및 재시작
systemctl status myapp
systemctl restart myapp

# Docker 컨테이너인 경우
docker ps | grep myapp
docker restart myapp-container

502 유형별 Nginx 설정 해결책

Nginx
# proxy_connect_timeout: 백엔드 연결 타임아웃 (기본 60s)
# proxy_read_timeout: 응답 대기 타임아웃 (기본 60s)
# 느린 API의 경우 늘려줄 수 있음

upstream backend {
    server 127.0.0.1:8080;
    keepalive 32;
}

server {
    location /api/ {
        proxy_pass http://backend;
        proxy_connect_timeout 5s;
        proxy_read_timeout 30s;
        proxy_send_timeout 30s;
    }
}

증상

로컬 터미널
$ curl https://api.example.com/health
curl: (60) SSL certificate problem: certificate has expired
More details here: https://curl.se/docs/sslcerts.html

curl failed to verify the legitimacy of the server and therefore
could not establish a secure connection to it.

브라우저에서는 "NET::ERR_CERT_DATE_INVALID" 오류로 표시됩니다.

즉각적인 현황 파악

로컬 터미널
# 1. 만료일 확인
echo | openssl s_client -connect api.example.com:443 -servername api.example.com 2>/dev/null \
  | openssl x509 -noout -dates
# notAfter=Dec 31 23:59:59 2024 GMT  ← 만료됨

# 2. 발급자 확인 (Let's Encrypt인지, 상용 CA인지)
echo | openssl s_client -connect api.example.com:443 2>/dev/null \
  | openssl x509 -noout -issuer -subject

# 3. 임시 우회로 서비스 동작 확인 (장애 범위 파악)
curl -k https://api.example.com/health
# 200이면 인증서만 문제, 서비스는 살아있음

인증서 종류별 갱신 방법

Let's Encrypt (Certbot)

위험 명령어

이 명령은 실행 중인 서비스 상태를 바꿔 순간적인 중단이나 설정 반영 실패를 만들 수 있습니다. 운영 트래픽 영향과 재시작 후 확인 명령을 먼저 준비하세요.

로컬 터미널
# 상태 확인
certbot certificates

# 수동 갱신
certbot renew --cert-name api.example.com

# 강제 갱신 (만료 전이라도)
certbot renew --force-renewal --cert-name api.example.com

# Nginx 리로드
systemctl reload nginx

상용 CA (DigiCert, Sectigo 등)

  1. CA 포털에서 새 인증서 신청/다운로드
  2. 기존 인증서 파일 교체:
    로컬 터미널
    cp new-cert.pem /etc/nginx/ssl/api.example.com.crt
    cp new-key.pem /etc/nginx/ssl/api.example.com.key
    nginx -t && systemctl reload nginx
    

재발 방지 — 자동 갱신 설정

위험 명령어

이 명령은 실행 중인 서비스 상태를 바꿔 순간적인 중단이나 설정 반영 실패를 만들 수 있습니다. 운영 트래픽 영향과 재시작 후 확인 명령을 먼저 준비하세요.

서버 터미널
# Certbot 자동 갱신 타이머 확인
systemctl status certbot.timer

# 없으면 cron으로 설정
# crontab -e
0 3 * * * certbot renew --quiet && systemctl reload nginx

심화 — 같은 IP 뒤에 여러 사이트가 산다

💡개념

심화: DNS·SNI·Host 헤더 — 누가 응답할지 정하는 3인조

curl로 IP는 분명히 맞게 찾아갔는데 엉뚱한 인증서나 다른 사이트의 응답을 받는 일이 있습니다. 원인은 하나의 IP 뒤에 여러 가상 호스트(vhost)가 살고, 그중 누가 답할지를 세 값이 단계별로 나눠 정하기 때문입니다. 이 셋의 역할을 구분하지 못하면 curl 테스트가 '되는데도' 실제 서비스는 실패하는 착시에 빠집니다.

  • DNS는 어느 IP로 갈지만 정한다: A/AAAA 레코드는 목적지 IP를 알려줄 뿐, 그 IP에서 어떤 사이트가 응답할지는 관여하지 않습니다.
  • SNI는 어느 인증서를 줄지 정한다: TLS 핸드셰이크의 ClientHello에 담기는 SNI(Server Name Indication)가 서버에게 어느 도메인의 인증서를 달라고 알립니다. SNI가 틀리면 다른 사이트의 인증서를 받거나 기본 vhost로 떨어집니다.
  • Host 헤더는 어느 vhost가 본문을 처리할지 정한다: 암호화가 끝난 뒤 전달되는 HTTP Host 헤더로 실제 응답 vhost가 갈립니다. 인증서 선택보다 한 박자 늦습니다.
  • 테스트 핀 고정의 함정: curl --resolve 도메인:443:IP는 DNS만 바꿔치기해 SNI·Host를 그대로 둔 채 특정 서버로 정확히 핀 고정합니다. 반면 -H "Host: ..."만 바꾸면 SNI는 원래 값이라 인증서·vhost가 어긋날 수 있습니다.

그래서 '공유 IP·CDN·SNI 라우팅 로드밸런서' 뒤를 디버깅할 땐, IP뿐 아니라 SNI와 Host를 각각 무엇으로 보냈는지까지 봐야 합니다.

상황

블루-그린 배포로 새 서버를 띄우고 curl --resolve로 그 서버에 핀 고정해 테스트하면 200 정상입니다. 그런데 실제 도메인으로 접속하면 404가 나거나 옛 사이트가 응답합니다. 테스트는 통과했는데 실사용은 실패하는 상황이라 원인을 못 잡습니다.

원인

공유 IP 뒤 로드밸런서가 SNI를 기준으로 vhost를 고르는 구조였습니다. 테스트할 때 -H "Host: ..."만 바꾸고 SNI는 원래 값을 보냈거나, DNS 전환이 아직 옛 IP를 가리키고 있었습니다. Host만 맞춘 요청이 우연히 통과해 '된다'고 착각하게 만든 것입니다.

진단

로컬 또는 서버
# curl -v로 실제 받은 인증서와 보낸 SNI 확인
curl -v --resolve api.example.com:443:203.0.113.10 https://api.example.com/health 2>&1 \
  | grep -E 'subject:|SSL connection|ALPN'

# SNI별로 어떤 인증서가 오는지 비교
echo | openssl s_client -servername api.example.com -connect 203.0.113.10:443 2>/dev/null \
  | openssl x509 -noout -subject

# 실제 DNS가 새 IP로 전환됐는지
dig +short api.example.com

해결

테스트는 항상 실제 SNI(=도메인)로 핀 고정합니다 — curl --resolve 도메인:443:새IP를 쓰고 -H "Host: ..."만 바꾸는 방식은 피합니다. DNS 전환이 미완이면 TTL을 낮추고 전파를 확인하며, 로드밸런서가 SNI 라우팅이면 새 도메인의 SNI·인증서를 vhost에 등록해야 실제 트래픽이 새 서버로 갑니다.


💼
실무 맥락
현업 패턴

실무에서 curl이 필수인 이유

curl은 브라우저 없이 HTTP 통신의 모든 계층을 CLI에서 직접 제어하고 검사할 수 있는 도구입니다. GUI가 없는 서버 환경, 자동화 스크립트, CI/CD 파이프라인에서 없어서는 안 됩니다.

개발자 관점

로컬 또는 서버
# API 개발 중 빠른 테스트
curl -X POST http://localhost:8080/api/login \
  -H "Content-Type: application/json" \
  -d '{"email":"test@example.com","password":"1234"}'

# 응답 헤더에서 토큰 추출
TOKEN=$(curl -s -X POST http://localhost:8080/api/login \
  -H "Content-Type: application/json" \
  -d '{"email":"test@example.com","password":"1234"}' \
  | python3 -c "import sys,json; print(json.load(sys.stdin)['token'])")

# 추출한 토큰으로 인증 API 테스트
curl -H "Authorization: Bearer $TOKEN" http://localhost:8080/api/users/me

인프라/운영 관점

로컬 터미널
# 로드밸런서 뒤의 각 백엔드 서버가 올바른 응답을 하는지 확인
# (Server 헤더나 X-Served-By 헤더로 어느 백엔드가 응답했는지 파악)
for i in {1..10}; do
  curl -s -o /dev/null -w "%{http_code} - %header{x-served-by}\n" \
    https://api.example.com/health
done

# Prometheus/Grafana 없는 환경에서 수동 모니터링
watch -n 5 'curl -s -o /dev/null -w "Status: %{http_code} | Time: %{time_total}s\n" \
  https://api.example.com/health'

CI/CD 파이프라인에서의 활용

로컬 터미널
# 배포 후 헬스체크 (배포 스크립트에 포함)
deploy_app() {
  # ... 배포 로직 ...

  # 배포 후 최대 60초간 헬스체크 재시도
  for i in {1..12}; do
    STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
      --connect-timeout 3 --max-time 5 \
      https://api.example.com/health)

    if [ "$STATUS" = "200" ]; then
      echo "배포 성공: 서비스 정상 응답"
      return 0
    fi

    echo "시도 $i/12: 상태 코드 $STATUS, 5초 후 재시도..."
    sleep 5
  done

  echo "배포 실패: 헬스체크 타임아웃"
  return 1
}

자격증 및 면접 포인트

  • HTTP 상태 코드 외우기: 200/201/204/301/302/400/401/403/404/429/500/502/503/504는 반드시 설명할 수 있어야 합니다.
  • curl -v 출력 해석: TLS 핸드셰이크, 요청/응답 헤더 흐름을 설명할 수 있어야 합니다.
  • 502 vs 503 차이: Nginx/Apache 프록시 구성 면접에서 자주 나오는 질문입니다.
  • SSL 갱신 자동화: Let's Encrypt + Certbot 자동 갱신 설정은 실무 기초 역량입니다.

핵심 명령어 정리

명령설명
curl -I URLHEAD 요청으로 헤더만 확인
curl -v URL전체 HTTP 흐름 verbose 출력
curl -s -o /dev/null -w "%{http_code}" URL상태 코드만 출력
curl -L URL리다이렉트 자동 추적
curl -o 파일 URL응답을 파일로 저장
curl -k URLSSL 검증 건너뜀 (임시 테스트용)
curl -X POST -d '...' -H 'Content-Type: application/json' URLJSON POST 요청
curl --connect-timeout 3 --max-time 10 URL타임아웃 설정

정리

  • HTTP 메서드(GET/POST/PUT/DELETE)는 행위를, 상태 코드는 결과를 표현합니다.
  • curl -I로 빠르게 헤더와 상태 코드를 확인하고, curl -v로 전체 흐름을 디버깅합니다.
  • curl -s -o /dev/null -w "%{http_code}"는 자동화 스크립트에서 상태 코드만 추출하는 표준 패턴입니다.
  • SSL 인증서 만료 전 자동 알림과 certbot 자동 갱신을 설정하면 장애를 예방할 수 있습니다.
  • 502는 게이트웨이의 업스트림 문제, 503은 서비스 완전 다운, 504는 타임아웃으로 구분합니다.

관련 모듈로 더 깊이:

다음 모듈에서는 HTTPS/TLS 작동 원리와 인증서 에러, 443 포트 장애를 디버깅하는 방법을 다룹니다.

지식 확인

퀴즈 — 8문제

Q1

Nginx 프록시 뒤에 있는 Node.js 앱이 배포 직후부터 502 Bad Gateway를 반환합니다. 빠른 원인 확인 순서는?

Q2

헬스체크 스크립트에서 서버 응답 바디는 필요 없고 HTTP 상태 코드만 빠르게 확인하고 싶습니다. 적절한 curl 명령은?

Q3

curl -s -o /dev/null -w "%{http_code}" URL 명령의 목적은 무엇입니까?

Q4

SSL 인증서 만료로 인한 curl 에러 메시지로 올바른 것은 무엇입니까?

Q5

curl -v 옵션으로 확인할 수 없는 정보는 무엇입니까?

Q6

HTTP 응답 상태 코드를 2xx·3xx·4xx·5xx 범주로 나눠 이해하면 좋은 이유는?

Q7

[심화] 하나의 IP 뒤에 여러 HTTPS 사이트가 있을 때, curl이 IP는 맞게 찾아갔는데도 엉뚱한 인증서를 받는다. 그 IP에서 돌려줄 인증서를 결정하는 값은?

Q8

[심화] 새 서버로 curl --resolve와 -H Host를 맞춰 테스트하면 200인데, 실제 도메인으로 접속하면 404거나 다른 사이트가 나온다. 무엇을 먼저 확인해야 하는가?

0 / 8 답변

🧪 실습으로 확인하기

HTTP 연결 타임아웃 — 구간별 시간 분해로 병목 특정

중급

"느려요/타임아웃"을 막연히 보지 않고 curl의 타이밍 분해(DNS→connect→TLS→TTFB→total)로 어느 구간이 느린지 가른다. connect 타임아웃(네트워크/방화벽)과 read 타임아웃(서버 처리 지연)을 구분하고, DNS 지연·TLS 핸드셰이크·서버 TTFB 중 범인을 특정해 올바른 쪽(네트워크 vs 서버)을 고친다.

40📋 3단계💻 직접 환경
실습 시작하기 →

이것도 배워보세요