API 요청은 브라우저에서 서버까지 어떻게 이동할까¶
코드에서는 한 줄인 API 호출이 사용자 화면으로 돌아오기까지 어떤 경계와 대기열을 지나는지 한 번의 흐름으로 따라갑니다.
브라우저가 https://api.example.com/orders/42를 호출했다. 애플리케이션 로그에는 30ms라고 남았는데 사용자는 300ms를 기다렸다. 두 숫자는 서로 틀린 것이 아니다. 측정한 시작점과 끝점이 다르다. 애플리케이션 로그는 컨트롤러 안쪽만 측정했을 수 있고, 사용자는 목적지 조회부터 연결 수립, 프록시 대기, 응답 전송까지 모두 기다린다.
요청 생명주기를 배우는 목적은 네트워크 용어를 암기하는 데 있지 않다. 느린 시간을 서로 다른 책임 구간으로 나누고, 각 구간에서 증거를 찾는 것이다.
요청은 하나가 아니라 여러 연결을 건넌다¶
아래 그림에서 화살표 하나가 언제나 물리 연결 하나를 뜻하는 것은 아니다. CDN이나 부하 분산기가 TLS를 끝내면 클라이언트와 엣지, 엣지와 원본 서버는 서로 다른 연결이다. 앞쪽은 HTTP/2인데 뒤쪽은 HTTP/1.1일 수도 있고, 연결 풀과 시간 제한도 각각 따로 있다.
1. 먼저 도메인이 가리키는 주소를 찾는다¶
클라이언트는 api.example.com이라는 이름만으로 인터넷 패킷을 보낼 수 없다. DNS(Domain Name System, 도메인 이름 시스템) 조회(lookup)를 통해 접속할 IP 주소를 얻는다. 브라우저와 운영체제 캐시에 답이 있으면 네트워크 질의 없이 끝난다. 없으면 운영체제의 작은 질의 창구인 스텁 리졸버가 보통 회사·통신사·공개 DNS의 재귀 리졸버에 답을 찾아 달라고 요청한다.
재귀 리졸버의 캐시에도 답이 없다면 루트, 최상위 도메인, 권한 있는 이름 서버를 따라간다. api.example.com이 CDN 주소의 별칭인 CNAME(Canonical Name)을 가리키면 마지막 A 또는 AAAA 주소가 나올 때까지 조회가 더 이어진다. DNS 시간이 사용자마다 다른 것은 어느 캐시에서 멈췄는지, 어떤 리졸버를 썼는지, 별칭이 몇 번 이어졌는지가 다를 수 있기 때문이다.
DNS의 내부 과정은 DNS부터 HTTP/3까지에서 더 자세히 다룬다.
2. 주소를 얻은 뒤 통신 경로를 만든다¶
HTTP/1.1과 HTTP/2는 보통 TCP 연결을 사용한다. 새 TCP 연결은 클라이언트와 서버가 통신 가능한지 확인하고 처음 사용할 순서 번호를 맞추는 핸드셰이크가 필요하다. 그 위에서 TLS(Transport Layer Security)가 인증서를 검증하고 암호화 키를 합의한다. 네트워크 왕복 시간이 큰 지역에서는 애플리케이션 코드가 시작되기도 전에 이 순차 왕복이 눈에 띄는 지연이 된다.
HTTP/3는 UDP 위에 신뢰성과 TLS를 결합한 QUIC을 사용하지만, 새 연결 준비가 완전히 사라지는 것은 아니다. 어느 버전이든 이미 만든 연결을 안전하게 재사용하면 반복되는 연결 비용을 크게 줄일 수 있다.
첫 요청과 다음 요청의 시간을 따로 봐야 하는 이유가 여기에 있다. 첫 요청은 DNS·연결·TLS 비용을 포함할 수 있고, 다음 요청은 연결 풀의 기존 연결을 바로 쓸 수 있다. 둘을 한 평균으로 섞으면 연결 문제와 서버 처리 문제를 구분하기 어렵다.
3. 엣지와 프록시가 요청을 다시 전달한다¶
클라이언트가 처음 만나는 서버는 애플리케이션이 아닐 수 있다. CDN, 부하 분산기, Nginx 같은 역방향 프록시가 TLS 종료, 인증, 요청률 제한, 경로 선택을 맡고 뒤쪽 애플리케이션 서버로 요청을 전달한다.
이 계층은 공통 정책과 장애 격리를 제공하지만 새 연결 풀과 대기열도 만든다. 상위 서버 연결을 모두 사용 중이면 프록시는 빈 연결이 생길 때까지 기다린다. 상위 서버에서 시간이 초과돼 프록시가 재시도하면 사용자가 본 한 요청 안에서 애플리케이션은 두 번 호출될 수도 있다. 결제나 주문 생성처럼 부작용이 있는 요청은 멱등성 장치 없이 이 재시도를 허용하면 중복 실행이 된다.
4. 서버 안에서도 실행 전에 기다릴 수 있다¶
요청이 애플리케이션 프로세스에 도착했다고 곧바로 업무 코드가 실행되는 것은 아니다. 전통적인 서블릿 서버라면 작업 스레드를, Netty라면 이벤트 루프의 처리 기회를 기다릴 수 있다. 보안 필터와 라우팅을 지난 뒤에도 데이터베이스 연결 풀이 비어 있지 않으면 대기한다.
이 대기 시간을 빼고 컨트롤러 메서드만 측정하면 서버 CPU가 낮은데도 요청이 느린 현상을 설명하지 못한다. 런타임 작업 대기, 데이터베이스 연결 대기, 잠금 대기와 실제 질의 실행을 분리해 기록해야 한다. 풀의 크기를 무조건 늘리면 기다리던 요청을 데이터베이스로 한꺼번에 밀어 넣어 오히려 꼬리 지연이 커질 수 있다.
5. 업무 결과를 만들고 HTTP 응답으로 바꾼다¶
애플리케이션은 캐시와 데이터베이스를 조회하고 업무 규칙을 적용한다. 컨트롤러가 객체를 반환한 뒤에도 작업은 남아 있다. JSON 직렬화기가 객체를 순회하고, 필요한 경우 압축하며, 결과 바이트를 소켓 버퍼에 쓴다. 지연 로딩된 연관 객체가 직렬화 도중 추가 데이터베이스 질의를 만드는 문제도 있다.
응답이 크면 서버의 CPU와 메모리 할당, 네트워크 전송, 클라이언트의 파싱과 화면 업데이트가 함께 늘어난다. “업무 로직은 30ms”라는 측정에는 이 비용이 빠져 있을 수 있다.
6. 첫 바이트와 전체 완료 시간은 다른 질문이다¶
TTFB(Time To First Byte, 최초 바이트 수신 시간)는 요청을 시작한 뒤 응답의 첫 바이트를 받을 때까지의 시간이다. 새 연결이라면 DNS·TCP·TLS를 포함하고, 프록시 대기와 서버가 첫 바이트를 만들기까지의 시간도 포함한다. 그러므로 TTFB를 곧바로 “백엔드 처리 시간”이라고 부르면 안 된다.
첫 바이트가 빨라도 큰 본문을 느린 모바일 네트워크로 받으면 전체 완료는 늦다. 반대로 서버가 응답 전체를 버퍼에 모은 뒤 보내면 본문은 작아도 첫 바이트가 늦을 수 있다. 스트리밍은 일부 결과를 일찍 보낼 수 있지만, 중간 프록시가 버퍼링하면 그 이점이 사라진다.
300ms를 줄일 때는 바깥에서 안쪽으로 좁힌다¶
첫째, 클라이언트에서 DNS·연결·TLS·첫 바이트·전체 시간을 나눠 본다. 둘째, 엣지와 프록시의 전체 시간에서 상위 서버 응답 시간을 빼 대기와 전송 차이를 찾는다. 셋째, 서버에서는 작업 대기·연결 풀 대기·질의·직렬화를 분리한다. 마지막으로 같은 요청을 추적 식별자로 묶어 서로 다른 계층의 시간을 한 흐름으로 맞춘다.
평균 하나만 보지 말고 p50·p95·p99를 함께 본다. 일부 요청만 느리다면 연결 재사용 실패, 특정 데이터 분할, 잠금 충돌, 재시도처럼 조건부 경로가 숨어 있을 수 있다. 최적화는 가장 긴 이름의 기술을 바꾸는 일이 아니라, 사용자가 기다린 시간에서 실제로 큰 구간을 증거로 찾는 일이다.