콘텐츠로 이동

DNS부터 HTTP/3까지 연결은 어떻게 만들어질까

도메인 주소를 찾는 순간부터 암호화된 HTTP 요청을 보내기까지, 서로 다른 프로토콜이 어떤 약속을 차례로 더하는지 설명합니다.

예상 읽기 시간 8분 · 필요한 절부터 읽어도 되는 연결 참고 지도

TCP, TLS, HTTP는 같은 일을 하는 기술이 아니다. IP가 패킷을 목적지로 옮기는 기반이라면 TCP는 빠지거나 순서가 바뀐 데이터를 복구해 순서 있는 바이트 흐름을 만든다. TLS는 그 흐름에서 상대의 신원을 확인하고 내용을 암호화한다. HTTP는 그 위에서 메서드·경로·헤더·본문이 무엇을 뜻하는지 정한다.

장애를 분석할 때 이 층을 나누는 이유가 있다. DNS가 잘못된 주소를 주면 TCP 연결 자체가 다른 서버로 향한다. TCP가 연결돼도 TLS 인증서 이름이 맞지 않으면 안전한 요청을 보낼 수 없다. TLS가 성공해도 프록시가 HTTP 경로를 잘못 전달하면 404가 난다. “API가 안 된다”는 한 증상 안에 실패 위치가 전혀 다른 문제들이 들어 있다.

DNS 조회는 누가 답을 찾아 주는가의 문제다

조회(lookup)는 이름을 주소로 바꾼 최종 결과만 뜻하지 않는다. 캐시를 확인하고, 필요한 DNS 레코드를 질의하고, 별칭을 따라 최종 주소를 얻는 전체 동작이다.

브라우저와 운영체제에 답이 없으면 운영체제의 스텁 리졸버(stub resolver)가 설정된 DNS 서버에 질문한다. 스텁 리졸버는 복잡한 탐색을 직접 하지 않는 작은 요청 창구다. 대신 재귀 리졸버(recursive resolver)가 캐시를 확인하고, 필요하면 루트 이름 서버에서 .com 같은 최상위 도메인 서버의 위치를 얻고, 다시 그 서버에서 example.com권한 있는 이름 서버(authoritative name server)를 찾아간다. 권한 서버는 해당 도메인의 원본 레코드를 제공한다.

flowchart TB
  C["브라우저·운영체제 캐시"] -->|"답이 없으면"| S["스텁 리졸버<br/>운영체제의 DNS 질의 창구"]
  S --> R["재귀 리졸버<br/>답을 대신 찾고 캐시"]
  R -->|"캐시가 비었으면"| ROOT["루트 이름 서버<br/>최상위 도메인 서버 안내"]
  ROOT --> TLD["최상위 도메인 서버<br/>권한 서버 안내"]
  TLD --> AUTH["권한 있는 이름 서버<br/>원본 레코드 제공"]
  AUTH --> N{"CNAME 별칭인가?"}
  N -->|"예"| R
  N -->|"아니요"| IP["A·AAAA 주소 반환"]
  IP --> C

CNAME(Canonical Name)은 한 도메인이 다른 도메인의 별칭임을 나타낸다. 예를 들어 api.example.com이 CDN 사업자의 이름을 가리키면 리졸버는 그 이름의 A 또는 AAAA 레코드를 다시 찾아 실제 IPv4 또는 IPv6 주소를 얻는다. 별칭이 길어지면 캐시가 비었을 때 질의도 늘고, 중간 대상의 설정 오류가 원래 도메인의 장애처럼 보일 수 있다.

TTL(Time To Live)은 이 답을 캐시에 보관해도 되는 시간이다. 짧게 잡으면 주소 변경이 빨리 반영되지만 리졸버와 권한 서버에 질의가 늘어난다. 존재하지 않는 이름이라는 NXDOMAIN 응답도 음수 캐시에 남을 수 있다. 방금 레코드를 만들었는데 일부 네트워크에서 계속 실패한다면 성공 레코드 TTL만이 아니라 이전 실패 응답의 캐시도 확인해야 한다.

TCP는 연결을 만들고 빠진 바이트를 복구한다

새 TCP 연결은 보통 SYN → SYN-ACK → ACK 순서로 시작한다. 양쪽이 통신할 수 있는지 확인하고 각 방향에서 사용할 초기 순서 번호를 맞추는 과정이다. 서버의 업무 코드가 시작되기 전에 최소 한 번의 RTT(Round-Trip Time, 네트워크 왕복 시간)가 필요한 이유다.

연결 뒤 TCP는 보낸 바이트 구간에 순서 번호를 붙인다. 수신 측은 어디까지 받았는지 확인 응답을 보내고, 일정 시간 확인되지 않은 구간은 다시 전송한다. 뒤쪽 데이터가 먼저 도착해도 애플리케이션에는 순서대로 전달한다. 사용하기 편한 대신 앞 세그먼트의 손실이 뒤쪽 전달까지 기다리게 할 수 있다.

TCP에는 서로 다른 두 속도 조절이 있다. 흐름 제어는 수신자의 버퍼가 넘치지 않게 하고, 혼잡 제어는 네트워크 경로가 감당할 수 있는 전송량을 추정한다. 새 연결은 확인 없이 보낼 수 있는 양을 보수적으로 시작해 점차 늘린다. 연결을 자주 새로 만들면 핸드셰이크뿐 아니라 전송 속도를 다시 키우는 비용도 반복된다.

UDP는 메시지 경계를 유지하지만 전달 성공, 순서, 재전송을 기본 보장하지 않는다. 그래서 자동으로 TCP보다 빠른 것이 아니다. 필요한 신뢰성을 애플리케이션이나 QUIC 같은 상위 계층이 구현해야 한다. QUIC은 UDP 위에서 신뢰성·혼잡 제어·암호화·독립적인 여러 스트림을 함께 제공한다.

TLS는 암호화 전에 누구와 연결했는지 확인한다

TLS 핸드셰이크에서 클라이언트는 ClientHello를 보내며 지원하는 암호 조합과 임시 키 재료를 제시한다. SNI(Server Name Indication)는 한 IP에서 여러 도메인을 서비스할 때 어떤 호스트에 접속하려는지 알려 준다. ALPN(Application-Layer Protocol Negotiation)은 HTTP/1.1과 HTTP/2 가운데 어떤 상위 프로토콜을 사용할지 협상한다.

서버는 선택 결과와 인증서 연결을 보낸다. 클라이언트는 인증서 서명이 신뢰하는 인증 기관까지 이어지는지, 유효 기간이 맞는지, 접속한 호스트 이름이 인증서에 포함되는지 확인한다. 인증서 검증을 끄면 내용은 암호화할 수 있어도 연결한 상대가 진짜 서버인지 보장하지 못한다.

공개키 연산을 모든 HTTP 바이트에 사용하는 것은 느리다. 양쪽은 핸드셰이크에서 공유 비밀을 만들고, 여기서 빠른 대칭키를 파생해 실제 통신을 암호화한다. 이전 세션 정보를 안전하게 재사용하는 세션 재개는 일부 왕복과 계산을 줄일 수 있다. 대신 세션 티켓의 수명과 서버 키 교체가 함께 관리돼야 한다.

sequenceDiagram
  participant C as 클라이언트
  participant S as 서버 또는 TLS 종료 프록시
  C->>S: ClientHello<br/>SNI·ALPN·암호 조합·키 재료
  S-->>C: ServerHello<br/>선택 결과·인증서·키 재료
  Note over C: 인증 기관·유효 기간·호스트 이름 검증
  C->>S: Finished<br/>합의한 키로 무결성 확인
  S-->>C: Finished<br/>암호화 연결 준비 완료

HTTP 버전은 같은 요청을 운반하는 방식이 다르다

HTTP/1.1은 연결을 재사용할 수 있지만, 여러 요청을 동시에 처리하려고 도메인마다 여러 TCP 연결을 쓰는 경우가 많다. HTTP/2는 한 TCP 연결 안에 여러 스트림을 함께 보내고 헤더를 압축한다. 요청마다 새 연결을 만드는 비용과 연결 수를 줄일 수 있다. 다만 TCP 레벨에서 패킷 하나가 빠지면 복구될 때까지 같은 연결의 여러 스트림이 함께 영향을 받을 수 있다.

HTTP/3는 QUIC의 독립 스트림 위에서 HTTP를 전달한다. 한 스트림의 손실 복구가 다른 스트림의 애플리케이션 전달까지 직접 막는 범위를 줄이고, 모바일 네트워크가 와이파이에서 셀룰러로 바뀌어도 연결을 이어 갈 수 있다. 하지만 프록시와 네트워크 장비의 지원, 서버 CPU 비용, 관측 도구까지 고려해야 한다. 요청 몇 개와 손실이 거의 없는 짧은 경로에서는 HTTP/2보다 체감 이점이 작을 수 있다.

연결 재사용은 속도와 동시 실행 상한을 함께 정한다

Keep-Alive는 한 HTTP 요청이 끝나도 연결을 잠시 열어 다음 요청에 사용한다. 연결 풀은 이런 연결을 보관한다. 재사용에 성공하면 DNS 캐시와 이미 만들어진 TCP·TLS 상태를 이용해 첫 요청 비용을 줄일 수 있다.

연결 풀은 단순한 속도 장치가 아니라 하위 서버에 동시에 보낼 요청 수를 제한하는 대기열이다. 너무 작으면 풀 대기가 길고, 너무 크면 데이터베이스나 하위 서비스에 감당 못 할 동시 요청을 밀어 넣는다. 클라이언트·프록시·서버의 유휴 연결 제한 시간이 어긋나면 한쪽이 닫은 연결을 다른 쪽이 재사용해 첫 요청에서 연결 초기화 오류가 날 수 있다.

부하 분산기와 역방향 프록시는 연결을 둘로 나눈다

부하 분산기는 상태 점검과 분산 규칙에 따라 여러 서버 중 하나를 고른다. 역방향 프록시는 클라이언트를 대신해 상위 서버와 통신하면서 TLS 종료, 인증, 요청률 제한, 경로 선택, 압축, 버퍼링과 재시도를 맡을 수 있다. 실제 제품에서는 두 역할이 겹친다.

중요한 것은 이름보다 연결이 어디서 끝나고 새로 시작되는가다. 클라이언트와 프록시의 연결은 건강해도 프록시의 상위 서버 연결 풀은 고갈될 수 있다. 프록시의 전체 요청 시간과 상위 서버 응답 시간을 함께 기록해야 그 차이를 볼 수 있다.

CDN은 가까운 거점에서 연결과 캐시를 처리한다

CDN(Content Delivery Network)은 여러 지역의 PoP(Point of Presence, 접속 거점)에 서버를 두고 사용자를 가까운 거점으로 보낸다. 정적 파일이 캐시에 있으면 먼 원본 서버까지 가지 않고 바로 응답한다. 동적 API라도 가까운 곳에서 TCP·TLS를 끝내고 최적화된 장거리 연결을 재사용하거나 공격을 흡수할 수 있다.

캐시 가능 여부는 URL 하나만으로 결정되지 않는다. 호스트, 경로, 쿼리 문자열, 선택된 헤더로 만든 캐시 키가 같은 요청을 같은 객체로 볼지를 정한다. 개인을 구분하는 값을 빠뜨리면 다른 사용자의 응답이 섞이는 보안 사고가 나고, 모든 쿠키를 넣으면 매 요청이 달라져 적중률이 사라질 수 있다.

Cache-Control은 저장 가능 여부와 신선도 규칙을 전달하고, ETag나 Last-Modified는 만료된 사본이 여전히 같은지 원본에 가볍게 확인하게 한다. 배포 파일은 내용 해시를 파일명에 넣고 오래 캐시하면 안전하다. 같은 URL의 내용을 바꾸고 삭제 요청에 의존하면 모든 거점에 변경이 퍼지는 동안 이전 버전이 보일 수 있다.

압축은 네트워크 바이트를 줄이고 CPU를 사용한다

JSON, HTML, CSS 같은 텍스트는 압축률이 높아 느린 링크에서 전송 시간을 줄일 수 있다. 대신 서버와 클라이언트 CPU, 압축 버퍼와 지연이 늘어난다. 이미 압축된 이미지나 영상, 아주 작은 응답은 이득이 작다. 같은 압축 문맥에 비밀 값과 공격자가 조종할 값을 넣으면 결과 크기 차이로 비밀을 추측하는 공격 가능성도 확인해야 한다.

스트리밍·WebSocket·SSE는 연결을 오래 유지한다

HTTP 스트리밍은 응답 전체가 준비되기 전에 일부 바이트를 보낸다. 큰 결과나 생성형 AI 응답처럼 첫 결과를 빨리 보여 줄 때 유용하다. WebSocket은 하나의 장기 연결에서 클라이언트와 서버가 양방향 메시지를 보내야 하는 채팅·협업에 맞는다. SSE(Server-Sent Events)는 서버에서 브라우저로 보내는 단방향 이벤트와 자동 재연결이 필요한 경우 더 단순하다.

연결이 오래가면 요청 수가 적어 보여도 동시 연결 수와 메모리가 커진다. 배포 때 기존 연결을 어떻게 빼낼지, 프록시의 유휴 시간 제한을 어떻게 맞출지, 소비자가 느릴 때 버퍼를 무한히 늘리지 않도록 어떻게 역압을 전달할지 정해야 한다. 재연결 뒤 빠진 이벤트를 복원해야 한다면 마지막 이벤트 식별자나 별도 커서도 필요하다.

프로토콜을 바꾸기 전에 확인할 것

첫 요청만 느리면 DNS·연결·TLS와 연결 재사용을 먼저 나눈다. 큰 응답만 멈추면 본문 크기, 압축, 경로 MTU와 프록시 버퍼링을 본다. 특정 지역만 느리면 리졸버 답과 실제 접속 거점, RTT, 패킷 손실을 비교한다. 스트리밍이 한꺼번에 도착하면 애플리케이션보다 중간 프록시가 응답을 모아 보내는지 확인한다.

HTTP/3나 CDN을 도입했다는 사실 자체가 성능 개선은 아니다. 변경 전후에 연결 성공률, 새 연결과 재사용 연결의 지연, 첫 바이트와 전체 전송 시간, 캐시 적중률, 거점에서 원본까지의 시간을 같은 조건으로 비교해야 한다.

참고 자료