왜 핸드셰이크가 병목이 되는가
TLS 연결마다 전체(full) 핸드셰이크가 발생하면, 인증서 검증과 키 교환에 필요한 왕복(RTT)과 비대칭 암호 연산이 매번 반복된다. TLS 1.2 full 핸드셰이크는 2-RTT, TLS 1.3도 첫 연결은 1-RTT가 필요하다. API 게이트웨이나 마이크로서비스 간 통신처럼 짧은 요청이 대량으로 오가는 환경에서는 이 지연과 CPU 비용이 그대로 p99 latency와 서버 부하로 드러난다.
핵심 최적화는 두 가지다. 첫째, 이미 협상한 세션을 재사용해 왕복과 키 연산을 생략하는 것. 둘째, 애초에 연결을 끊지 않고 keep-alive로 재사용하는 것이다.
세션 재사용의 두 가지 방식
TLS 세션 재개(resumption)에는 서버가 세션 상태를 저장하는 Session ID 방식과, 상태를 암호화해 클라이언트에게 넘기는 Session Ticket 방식이 있다. TLS 1.3에서는 이 둘이 통합되어 PSK(Pre-Shared Key) 기반 재개로 일원화되었고, 0-RTT 조기 데이터까지 가능하다.
| 구분 | Session ID | Session Ticket / PSK |
|---|---|---|
| 상태 저장 위치 | 서버 메모리 | 클라이언트 |
| 서버 확장성 | 낮음(캐시 공유 필요) | 높음 |
| 다중 노드 환경 | 캐시 동기화 부담 | 티켓 키만 공유 |
| 비용 | 서버 메모리 | 키 관리 필요 |
Nginx에서 세션 캐시 설정하기
Nginx는 공유 메모리 캐시와 티켓을 함께 지원한다. 다중 워커·다중 서버 환경에서 재개율을 높이려면 캐시를 공유(shared)로 두는 것이 중요하다.
http {
ssl_session_cache shared:SSL:50m; # 워커 간 공유, 약 20만 세션
ssl_session_timeout 1h; # 재사용 유효 시간
ssl_session_tickets on;
ssl_session_ticket_key /etc/nginx/ticket.key; # 노드 간 동일 키 공유
ssl_protocols TLSv1.2 TLSv1.3;
keepalive_timeout 65s; # 연결 재사용
}
여러 서버가 로드밸런서 뒤에 있다면 ssl_session_ticket_key를 모든 노드에 동일하게 배포해야 한다. 그렇지 않으면 A노드가 발급한 티켓을 B노드가 복호화하지 못해 full 핸드셰이크로 떨어진다.
클라이언트 측 재사용 확인
서버 설정만으로는 부족하다. 클라이언트가 커넥션 풀을 유지하고 세션을 캐싱해야 실제 재개가 일어난다. Python requests는 Session 객체로 커넥션 풀을 재사용한다.
import requests
s = requests.Session() # 커넥션 풀 + TLS 세션 재사용
for _ in range(100):
r = s.get("https://api.example.com/health")
# 매 요청마다 requests.get()을 쓰면 풀이 재생성되어 핸드셰이크 반복
재개 여부는 openssl로 직접 검증한다. 두 번째 연결에서 Reused가 뜨는지 확인한다.
openssl s_client -connect api.example.com:443 -reconnect 2>&1 \
| grep -E "New|Reused"
# 출력 예:
# New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384
# Reused, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384
0-RTT의 함정
TLS 1.3의 0-RTT 조기 데이터는 재연결 시 왕복 없이 요청을 보내 지연을 크게 줄인다. 다만 재전송 공격(replay)에 취약하다. 공격자가 캡처한 0-RTT 데이터를 다시 보내도 서버가 이를 구별하기 어렵기 때문이다.
따라서 0-RTT 구간에는 멱등(idempotent)한 요청만 허용해야 한다. 결제·주문 생성 같은 상태 변경 POST는 0-RTT로 처리하지 않도록 애플리케이션 레벨에서 차단하는 것이 안전하다.
운영 시 주의점
- 티켓 키 로테이션: 티켓 키는 장기 비밀이다. 유출되면 과거 세션이 복호화될 수 있으니 주기적으로 교체하되, 이전 키도 잠시 유지해 재개 실패를 막는다.
- Forward Secrecy 유지: 세션 재개는 편의를 주지만 티켓 유효기간이 길수록 전방향 안전성이 약해진다.
ssl_session_timeout은 무한정 늘리지 말고 수십 분~1시간 수준으로 둔다. - 재개율 모니터링: 실제로 재개가 일어나는지 지표로 봐야 한다. 재개율이 낮다면 로드밸런서가 매번 다른 노드로 분산하거나 티켓 키가 불일치할 가능성이 높다.
정리
핸드셰이크 비용은 커넥션 재사용(keep-alive), 세션 재개(ticket/PSK), 그리고 신중한 0-RTT 적용의 조합으로 줄인다. 설정을 켜는 것으로 끝내지 말고 openssl과 재개율 지표로 실제 효과를 검증하는 습관이 핵심이다. 보안(전방향 안전성·replay)과 성능 사이의 균형점을 각 서비스의 요청 특성에 맞게 잡아야 한다.