- Authors
- Name
- 들어가며
- 1. HTTP 상태 코드 - 디버깅 관점에서의 재해석
- 2. TLS/SSL 핸드셰이크 트러블슈팅
- 3. 인증서 체인 검증 문제
- 4. curl 디버깅 기법
- 5. 502/503/504 에러 트러블슈팅
- 6. CORS 문제 해결
- 7. 리다이렉트 루프 디버깅
- 8. HTTP/2 및 HTTP/3 디버깅
- 9. 로드밸런서 및 리버스 프록시 트러블슈팅
- 10. 실전 디버깅 시나리오
- 마치며
들어가며
운영 환경에서 HTTP/HTTPS 관련 문제가 발생하면, 단순히 "안 돼요"라는 보고만으로는 원인을 파악하기 어렵다. 네트워크 레이어, TLS 핸드셰이크, 애플리케이션 로직, 로드밸런서 설정 등 여러 계층에서 문제가 발생할 수 있기 때문이다.
이 글에서는 실무에서 자주 만나는 HTTP/HTTPS 문제를 체계적으로 진단하고 해결하는 방법을 다룬다.
1. HTTP 상태 코드 - 디버깅 관점에서의 재해석
1xx (정보 응답)
실무에서 자주 간과되지만 중요한 상태 코드가 있다.
100 Continue → 클라이언트가 큰 요청 본문을 보내기 전, 서버가 수락 의사를 표시
101 Switching → WebSocket 업그레이드 시 반드시 확인해야 하는 코드
103 Early Hints → 브라우저 preload 최적화에 활용
2xx (성공)
200 OK → 가장 기본. 하지만 본문이 비어 있으면 의심해야 한다
201 Created → POST 후 Location 헤더 확인 필수
204 No Content → DELETE 응답으로 적합. 본문이 있으면 버그
206 Partial → Range 요청 시. 대용량 파일 다운로드 디버깅에 핵심
3xx (리다이렉션) - 함정이 많은 영역
301 Moved Permanently → 브라우저가 캐시함. 잘못 설정하면 복구가 어렵다
302 Found → 임시 리다이렉션. SEO 관점에서 301과 혼동 주의
303 See Other → POST 후 GET으로 리다이렉트할 때 사용
307 Temporary Redirect → 메서드를 유지하는 리다이렉트
308 Permanent Redirect → 메서드를 유지하는 영구 리다이렉트
4xx (클라이언트 에러) - 디버깅 핵심
# 400 Bad Request 디버깅
curl -v -X POST https://api.example.com/users \
-H "Content-Type: application/json" \
-d '{"name": "test"' # 잘못된 JSON - 닫는 괄호 누락
# 401 vs 403 차이 이해
# 401: 인증 자체가 안 됨 (토큰 없음/만료)
# 403: 인증은 됐으나 권한이 없음
# 405 Method Not Allowed
curl -v -X PATCH https://api.example.com/users/1
# Allow 헤더에서 허용된 메서드 확인
# 429 Too Many Requests
# Retry-After 헤더와 X-RateLimit-* 헤더 확인
curl -v https://api.example.com/data 2>&1 | grep -i "rate\|retry"
5xx (서버 에러) - 가장 긴급한 상황
500 Internal Server Error → 서버 로그 확인이 최우선
502 Bad Gateway → 업스트림 서버 응답 불가 (프록시 뒤 서버 문제)
503 Service Unavailable → 서버 과부하 또는 점검 중
504 Gateway Timeout → 업스트림 서버 응답 시간 초과
2. TLS/SSL 핸드셰이크 트러블슈팅
TLS 핸드셰이크 과정 이해
Client Server
| |
|--- ClientHello (지원 암호화, SNI) --->|
| |
|<-- ServerHello (선택된 암호화) ------|
|<-- Certificate (인증서 체인) --------|
|<-- ServerKeyExchange ---------------|
|<-- ServerHelloDone -----------------|
| |
|--- ClientKeyExchange -------------->|
|--- ChangeCipherSpec --------------->|
|--- Finished ----------------------->|
| |
|<-- ChangeCipherSpec ----------------|
|<-- Finished ------------------------|
| |
|====== 암호화된 통신 시작 ============|
openssl을 이용한 핸드셰이크 디버깅
# 기본 연결 테스트
openssl s_client -connect example.com:443 -servername example.com
# TLS 버전 명시 테스트
openssl s_client -connect example.com:443 -tls1_2
openssl s_client -connect example.com:443 -tls1_3
# 인증서 체인 전체 출력
openssl s_client -connect example.com:443 -showcerts 2>/dev/null | \
openssl x509 -noout -subject -issuer -dates
# SNI(Server Name Indication) 문제 디버깅
openssl s_client -connect 1.2.3.4:443 -servername mysite.com
# 특정 암호화 스위트 테스트
openssl s_client -connect example.com:443 -cipher ECDHE-RSA-AES256-GCM-SHA384
자주 발생하는 TLS 에러와 해결법
# 에러: SSL_ERROR_HANDSHAKE_FAILURE
# 원인: 클라이언트와 서버 간 지원하는 암호화 스위트가 없음
# 해결: 서버의 cipher suite 확인
nmap --script ssl-enum-ciphers -p 443 example.com
# 에러: CERTIFICATE_VERIFY_FAILED
# 원인: 인증서 체인 불완전 또는 루트 CA 미신뢰
# 진단:
openssl s_client -connect example.com:443 2>&1 | grep "verify"
# 에러: hostname mismatch
# 원인: 인증서의 CN/SAN이 요청 도메인과 불일치
openssl s_client -connect example.com:443 2>/dev/null | \
openssl x509 -noout -text | grep -A1 "Subject Alternative Name"
3. 인증서 체인 검증 문제
인증서 체인 구조
Root CA (자체 서명, OS/브라우저에 내장)
└── Intermediate CA (Root CA가 서명)
└── Server Certificate (Intermediate CA가 서명)
체인 검증 스크립트
#!/bin/bash
# 인증서 체인 검증 스크립트
DOMAIN=$1
echo "=== 인증서 체인 검증: $DOMAIN ==="
# 서버에서 인증서 체인 가져오기
echo | openssl s_client -connect ${DOMAIN}:443 -servername ${DOMAIN} \
-showcerts 2>/dev/null | \
awk '/BEGIN CERTIFICATE/,/END CERTIFICATE/{ print }' > /tmp/chain.pem
# 각 인증서 정보 출력
csplit -f /tmp/cert- -b '%02d.pem' /tmp/chain.pem \
'/BEGIN CERTIFICATE/' '{*}' 2>/dev/null
for cert in /tmp/cert-*.pem; do
[ -s "$cert" ] || continue
echo "--- Certificate: $cert ---"
openssl x509 -in "$cert" -noout \
-subject -issuer -dates -fingerprint 2>/dev/null
echo ""
done
# 체인 검증
openssl verify -verbose -CAfile /etc/ssl/certs/ca-certificates.crt \
/tmp/chain.pem 2>&1
일반적인 인증서 문제
# 1. 인증서 만료 확인
echo | openssl s_client -connect example.com:443 2>/dev/null | \
openssl x509 -noout -dates
# 2. 중간 인증서 누락 확인
# SSL Labs 테스트 (가장 확실한 방법)
# https://www.ssllabs.com/ssltest/
# 3. 자체 서명 인증서 사용 시 curl
curl --cacert /path/to/ca.crt https://internal.example.com
# 4. 인증서 갱신 후 적용 확인
echo | openssl s_client -connect example.com:443 2>/dev/null | \
openssl x509 -noout -serial -fingerprint
4. curl 디버깅 기법
기본 디버깅 옵션
# -v (verbose): 가장 기본적인 디버깅
curl -v https://api.example.com/health
# -vv: 더 상세한 출력
curl -vv https://api.example.com/health
# --trace: 바이트 단위 전체 통신 덤프
curl --trace /tmp/curl-trace.log https://api.example.com/health
# --trace-ascii: 가독성 좋은 ASCII 덤프
curl --trace-ascii /tmp/curl-trace.txt https://api.example.com/health
# --trace-time: 타임스탬프 포함
curl --trace-time --trace-ascii - https://api.example.com/health
고급 디버깅 기법
# DNS 우회 (--resolve): 특정 IP로 요청 보내기
# 배포 전 새 서버 테스트에 유용
curl --resolve api.example.com:443:10.0.1.50 \
https://api.example.com/health
# 연결 시간 측정
curl -o /dev/null -s -w "\
DNS Lookup: %{time_namelookup}s\n\
TCP Connect: %{time_connect}s\n\
TLS Handshake: %{time_appconnect}s\n\
Start Transfer: %{time_starttransfer}s\n\
Total Time: %{time_total}s\n\
HTTP Code: %{http_code}\n\
Download Size: %{size_download} bytes\n" \
https://api.example.com/health
# 리다이렉트 추적
curl -v -L --max-redirs 10 https://example.com 2>&1 | grep "< Location"
# 특정 HTTP 버전 강제
curl --http1.1 https://api.example.com/health
curl --http2 https://api.example.com/health
curl --http3 https://api.example.com/health
# 프록시 경유 디버깅
curl -v --proxy http://proxy.internal:8080 https://api.example.com/health
# 클라이언트 인증서 사용
curl --cert /path/to/client.crt --key /path/to/client.key \
https://mtls.example.com/api
curl 실전 디버깅 원라이너
# 연속 요청으로 응답 시간 모니터링
for i in $(seq 1 10); do
echo -n "요청 $i: "
curl -o /dev/null -s -w "%{http_code} %{time_total}s" \
https://api.example.com/health
echo ""
sleep 1
done
# 헤더만 빠르게 확인
curl -I https://api.example.com/health
# POST 요청 디버깅
curl -v -X POST https://api.example.com/users \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{"name": "test", "email": "test@example.com"}' \
2>&1 | grep -E "^[<>*]"
5. 502/503/504 에러 트러블슈팅
502 Bad Gateway
# 원인: 업스트림 서버가 유효하지 않은 응답을 반환
# 1. 업스트림 서버 직접 확인
curl -v http://upstream-server:8080/health
# 2. Nginx 에러 로그 확인
tail -f /var/log/nginx/error.log | grep "502\|upstream"
# 3. 업스트림 서버 포트 리스닝 확인
ss -tlnp | grep 8080
netstat -tlnp | grep 8080
# 4. Nginx 설정 점검
# proxy_pass에서 업스트림 주소가 올바른지 확인
nginx -T | grep -A5 "upstream"
Nginx 502 해결을 위한 설정 조정:
upstream backend {
server 10.0.1.10:8080 max_fails=3 fail_timeout=30s;
server 10.0.1.11:8080 max_fails=3 fail_timeout=30s;
keepalive 32;
}
server {
location /api/ {
proxy_pass http://backend;
proxy_next_upstream error timeout http_502 http_503;
proxy_connect_timeout 5s;
proxy_read_timeout 60s;
proxy_send_timeout 60s;
# 버퍼 크기 조정 (큰 응답 헤더 처리)
proxy_buffer_size 16k;
proxy_buffers 4 32k;
proxy_busy_buffers_size 64k;
}
}
503 Service Unavailable
# 원인 분석
# 1. 서버 과부하 확인
top -bn1 | head -5
free -m
df -h
# 2. 연결 수 확인
ss -s
ss -tn state established | wc -l
# 3. 프로세스 상태 확인
systemctl status nginx
systemctl status your-app
# 4. 리소스 제한 확인
ulimit -n # 파일 디스크립터 수
cat /proc/sys/net/core/somaxconn # 소켓 백로그
504 Gateway Timeout
# 원인: 업스트림 서버 응답 시간 초과
# 1. 타임아웃 단계별 진단
curl -o /dev/null -s -w "connect: %{time_connect}s\nttfb: %{time_starttransfer}s\ntotal: %{time_total}s\n" \
https://api.example.com/slow-endpoint
# 2. 느린 쿼리 확인 (DB 원인인 경우)
# MySQL
SHOW PROCESSLIST;
# PostgreSQL
SELECT pid, now() - pg_stat_activity.query_start AS duration, query
FROM pg_stat_activity
WHERE state = 'active' AND now() - query_start > interval '5 seconds';
# 3. 타임아웃 설정 조정
# Nginx: proxy_read_timeout 120s;
# HAProxy: timeout server 120s
6. CORS 문제 해결
CORS 작동 원리
브라우저 서버
| |
|--- Preflight (OPTIONS) ------->|
| Origin: https://app.com |
| Access-Control-Request-Method: POST
| |
|<-- 200 OK --------------------|
| Access-Control-Allow-Origin: https://app.com
| Access-Control-Allow-Methods: POST, GET
| Access-Control-Allow-Headers: Content-Type
| Access-Control-Max-Age: 86400
| |
|--- 실제 요청 (POST) ----------->|
| Origin: https://app.com |
| |
|<-- 200 OK --------------------|
| Access-Control-Allow-Origin: https://app.com
CORS 디버깅
# Preflight 요청 시뮬레이션
curl -v -X OPTIONS https://api.example.com/data \
-H "Origin: https://app.example.com" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: Content-Type, Authorization"
# 응답에서 CORS 헤더 확인
curl -v https://api.example.com/data \
-H "Origin: https://app.example.com" \
2>&1 | grep -i "access-control"
Nginx에서 CORS 설정
location /api/ {
# Preflight 요청 처리
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '$http_origin' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type, X-Requested-With' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Access-Control-Max-Age' 86400;
add_header 'Content-Length' 0;
return 204;
}
# 실제 요청에도 CORS 헤더 추가
add_header 'Access-Control-Allow-Origin' '$http_origin' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
proxy_pass http://backend;
}
7. 리다이렉트 루프 디버깅
# 리다이렉트 체인 추적
curl -v -L --max-redirs 20 https://example.com 2>&1 | \
grep -E "^< (HTTP|Location)" | head -30
# 결과 예시 (무한 루프):
# < HTTP/1.1 301 Moved Permanently
# < Location: https://www.example.com/
# < HTTP/1.1 301 Moved Permanently
# < Location: https://example.com/
# < HTTP/1.1 301 Moved Permanently
# < Location: https://www.example.com/ ← 루프!
# 흔한 원인:
# 1. HTTP → HTTPS 리다이렉트 + HTTPS → HTTP 리다이렉트 충돌
# 2. www ↔ non-www 리다이렉트 충돌
# 3. 로드밸런서의 SSL termination + 앱의 HTTPS 강제
# 해결: X-Forwarded-Proto 헤더 활용
# Nginx에서 백엔드로 프로토콜 정보 전달
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
8. HTTP/2 및 HTTP/3 디버깅
HTTP/2 디버깅
# HTTP/2 지원 확인
curl -v --http2 https://example.com 2>&1 | grep "ALPN"
# HTTP/2 프레임 레벨 디버깅 (nghttp2)
nghttp -v https://example.com
# HTTP/2 멀티플렉싱 확인
nghttp -v -m 10 https://example.com/style.css \
https://example.com/script.js \
https://example.com/image.png
# HTTP/2 서버 푸시 확인
nghttp -v --stat https://example.com 2>&1 | grep "push"
# h2c (HTTP/2 Cleartext) 테스트
curl -v --http2-prior-knowledge http://localhost:8080/health
HTTP/3 (QUIC) 디버깅
# HTTP/3 지원 확인
curl --http3 -v https://example.com 2>&1 | head -20
# Alt-Svc 헤더 확인 (HTTP/3 광고)
curl -sI https://example.com | grep -i "alt-svc"
# 예: alt-svc: h3=":443"; ma=86400
# QUIC 연결 디버깅 (quiche 도구)
# 0-RTT 연결 테스트
curl --http3 -v --connect-to example.com:443:server-ip:443 \
https://example.com
HTTP/2와 HTTP/3의 일반적인 문제
# 1. HTTP/2의 HEAD-OF-LINE blocking 확인
# HTTP/2는 TCP 레이어에서 HOL blocking 발생 가능
# 해결: HTTP/3(QUIC) 사용 검토
# 2. HTTP/2 GOAWAY 프레임 디버깅
nghttp -v https://example.com 2>&1 | grep "GOAWAY"
# 3. HPACK 헤더 압축 문제
# 큰 헤더를 가진 요청에서 HPACK 동적 테이블 크기 확인
nghttp -v --header-table-size=65536 https://example.com
# 4. 스트림 동시성 제한
# SETTINGS_MAX_CONCURRENT_STREAMS 확인
nghttp -v https://example.com 2>&1 | grep "MAX_CONCURRENT"
9. 로드밸런서 및 리버스 프록시 트러블슈팅
헬스체크 실패 디버깅
# 1. 직접 헬스체크 엔드포인트 확인
curl -v http://backend-server:8080/health
# 2. 로드밸런서에서 보는 것과 동일한 조건으로 테스트
curl -v -H "Host: api.example.com" \
--resolve api.example.com:80:10.0.1.10 \
http://api.example.com/health
# 3. HAProxy 상태 확인
echo "show stat" | socat stdio /var/run/haproxy/admin.sock | \
awk -F',' '{print $1, $2, $18, $19}'
# 4. Nginx 업스트림 상태 (nginx-module-vts 사용 시)
curl http://localhost/status/format/json | jq '.upstreamZones'
세션 고정(Sticky Session) 문제
# 쿠키 기반 세션 확인
curl -v -c cookies.txt https://app.example.com/login
curl -v -b cookies.txt https://app.example.com/dashboard
# 같은 백엔드로 라우팅되는지 확인
for i in $(seq 1 5); do
curl -s -b cookies.txt https://app.example.com/api/server-id
echo ""
done
X-Forwarded-* 헤더 디버깅
# 프록시 체인 확인
curl -v https://api.example.com/debug-headers 2>&1
# 예상되는 헤더:
# X-Forwarded-For: client-ip, proxy1-ip, proxy2-ip
# X-Forwarded-Proto: https
# X-Forwarded-Host: api.example.com
# X-Real-IP: client-ip
# 프록시가 원본 IP를 올바르게 전달하는지 테스트
curl -H "X-Forwarded-For: 1.2.3.4" https://api.example.com/my-ip
10. 실전 디버깅 시나리오
시나리오 1: 간헐적 502 에러
#!/bin/bash
# 간헐적 502 모니터링 스크립트
URL="https://api.example.com/health"
LOG_FILE="/tmp/502-monitor.log"
echo "=== 502 모니터링 시작: $(date) ===" >> $LOG_FILE
while true; do
RESPONSE=$(curl -s -o /dev/null -w "%{http_code}|%{time_total}|%{time_connect}|%{time_starttransfer}" $URL)
HTTP_CODE=$(echo $RESPONSE | cut -d'|' -f1)
TOTAL_TIME=$(echo $RESPONSE | cut -d'|' -f2)
CONNECT_TIME=$(echo $RESPONSE | cut -d'|' -f3)
TTFB=$(echo $RESPONSE | cut -d'|' -f4)
if [ "$HTTP_CODE" != "200" ]; then
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$TIMESTAMP] HTTP $HTTP_CODE | Total: ${TOTAL_TIME}s | Connect: ${CONNECT_TIME}s | TTFB: ${TTFB}s" >> $LOG_FILE
# 502 발생 시 상세 정보 수집
if [ "$HTTP_CODE" = "502" ]; then
echo " Detailed trace:" >> $LOG_FILE
curl -v $URL 2>> $LOG_FILE
echo "---" >> $LOG_FILE
fi
fi
sleep 5
done
시나리오 2: TLS 인증서 만료 모니터링
#!/bin/bash
# 인증서 만료 모니터링 스크립트
DOMAINS=(
"api.example.com"
"app.example.com"
"admin.example.com"
)
WARNING_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 || date -j -f "%b %d %T %Y %Z" "$EXPIRY" +%s 2>/dev/null)
NOW_EPOCH=$(date +%s)
DAYS_LEFT=$(( (EXPIRY_EPOCH - NOW_EPOCH) / 86400 ))
if [ $DAYS_LEFT -lt 0 ]; then
echo "[CRITICAL] $domain: 인증서 만료됨! ($EXPIRY)"
elif [ $DAYS_LEFT -lt $WARNING_DAYS ]; then
echo "[WARNING] $domain: ${DAYS_LEFT}일 후 만료 ($EXPIRY)"
else
echo "[OK] $domain: ${DAYS_LEFT}일 남음 ($EXPIRY)"
fi
done
시나리오 3: 전체 HTTP 통신 종합 진단
#!/bin/bash
# HTTP 종합 진단 스크립트
URL=${1:-"https://example.com"}
echo "============================================"
echo "HTTP 종합 진단: $URL"
echo "시간: $(date)"
echo "============================================"
# 1. DNS 확인
echo -e "\n[1] DNS 확인"
DOMAIN=$(echo $URL | awk -F[/:] '{print $4}')
dig +short $DOMAIN A
dig +short $DOMAIN AAAA
# 2. 포트 연결 확인
echo -e "\n[2] TCP 연결 확인"
nc -zv $DOMAIN 443 2>&1
# 3. TLS 정보
echo -e "\n[3] TLS 정보"
echo | openssl s_client -connect ${DOMAIN}:443 -servername ${DOMAIN} 2>/dev/null | \
grep -E "Protocol|Cipher|Verify"
# 4. 인증서 정보
echo -e "\n[4] 인증서 정보"
echo | openssl s_client -connect ${DOMAIN}:443 -servername ${DOMAIN} 2>/dev/null | \
openssl x509 -noout -subject -issuer -dates 2>/dev/null
# 5. HTTP 응답
echo -e "\n[5] HTTP 응답"
curl -s -o /dev/null -w "\
HTTP Code: %{http_code}\n\
DNS Lookup: %{time_namelookup}s\n\
TCP Connect: %{time_connect}s\n\
TLS Handshake: %{time_appconnect}s\n\
TTFB: %{time_starttransfer}s\n\
Total Time: %{time_total}s\n\
Download Size: %{size_download} bytes\n\
HTTP Version: %{http_version}\n\
Remote IP: %{remote_ip}\n" $URL
# 6. 응답 헤더
echo -e "\n[6] 주요 응답 헤더"
curl -sI $URL | grep -iE "^(server|x-|content-type|cache-control|strict|location|set-cookie)"
# 7. 보안 헤더 확인
echo -e "\n[7] 보안 헤더 검사"
HEADERS=$(curl -sI $URL)
for header in "Strict-Transport-Security" "X-Content-Type-Options" "X-Frame-Options" "Content-Security-Policy" "X-XSS-Protection"; do
if echo "$HEADERS" | grep -qi "$header"; then
echo " [O] $header"
else
echo " [X] $header (누락)"
fi
done
echo -e "\n============================================"
echo "진단 완료"
echo "============================================"
마치며
HTTP/HTTPS 트러블슈팅은 계층적 접근이 핵심이다. DNS, TCP 연결, TLS 핸드셰이크, HTTP 프로토콜, 애플리케이션 로직 순서로 문제를 좁혀 나가면 대부분의 문제를 효율적으로 해결할 수 있다.
여기서 소개한 도구와 기법을 활용해서 운영 환경에서 발생하는 문제를 체계적으로 진단하는 습관을 기르자. 특히 curl -w 포맷 문자열과 openssl s_client는 반드시 익혀두면 실무에서 큰 도움이 된다.
핵심 정리:
- 상태 코드는 단서일 뿐이다 - 반드시 응답 본문과 헤더를 함께 확인
- TLS 문제는
openssl s_client로 시작 - 타이밍 문제는
curl -w로 각 단계별 시간 측정 - 간헐적 문제는 자동화된 모니터링 스크립트로 포착
- 프록시 관련 문제는 각 홉에서 직접 테스트하여 병목 구간 특정