왜 핸드셰이크가 병목이 되는가

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 IDSession 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)과 성능 사이의 균형점을 각 서비스의 요청 특성에 맞게 잡아야 한다.