The Journey of a Request to the Backend¶
프론트엔드에서 커널을 거쳐 백엔드 프로세스까지
개요¶
백엔드로 요청을 보낼 때 대부분은 요청의 처리(Processing) 단계에만 집중하지만, 사실 그것은 마지막 단계에 불과하다. 요청이 처리되기까지는 총 6단계를 거치며, 각 단계는 이론적으로 별도의 스레드나 프로세스에서 실행될 수 있다. 웹 서버, 프록시, 프레임워크, 데이터베이스 등 거의 모든 백엔드가 이 단계들을 수행하며, 각각 다른 방식으로 구현한다.
1. Accept (연결 수락)¶
클라이언트가 서버의 포트(예: 443)에 접속하면, OS 커널이 3-way handshake를 완료하고 해당 연결을 accept queue(리스너 큐)에 넣는다. 백엔드 애플리케이션은 리스너 소켓에서 accept() 시스템 콜을 호출하여 연결을 나타내는 파일 디스크립터를 생성한다.
주요 포인트:
- 백엔드가 연결 수락을 느리게 하면 병목이 발생하여 큐가 가득 차고 새 연결이 실패할 수 있다.
- 포트를 listen할 때 accept queue의 크기를 backlog 파라미터로 지정할 수 있다.
- 대부분의 백엔드는 연결 수락 전용 스레드를 하나 둔다.
- 단일 스레드로 부족하면 여러 스레드가 수락할 수 있지만, 같은 소켓에서 스레드끼리 블로킹이 발생한다.
- 이를 해결하기 위해
SO_REUSEPORT옵션으로 같은 포트에 여러 리스너 소켓(=여러 accept queue)을 생성할 수 있다. NGINX, HAProxy 등에서 기본 옵션으로 사용된다.
2. Read (데이터 읽기)¶
연결이 수립되면 클라이언트가 요청을 보낸다. 요청은 프로토콜(주로 HTTP)에 의해 시작과 끝이 정의된 바이트 시퀀스다.
클라이언트 측 처리 흐름:
- TLS 사용 시 요청을 암호화
- 요청 압축이 지원되면 본문을 압축
- 데이터 타입(JSON, Protobuf 등)을 on-wire 표현으로 직렬화
- 네트워크 바이트 순서로 원시 바이트 전송
백엔드 측 처리:
- 원시 바이트는 NIC를 통해 OS 커널에 도달하고, 커널이 관리하는 수신 큐(receive queue) 에 들어간다.
- 백엔드 애플리케이션이
read()또는rcv()시스템 콜을 호출하면 데이터가 수신 큐에서 유저 스페이스 메모리로 이동한다. - 이 시점에서 읽은 바이트는 암호화·인코딩된 원시 데이터이며, 10개의 요청일 수도 있고 절반의 요청일 수도 있다. 아직 알 수 없다.
3. Decrypt (복호화)¶
백엔드 프로세스 메모리에 있는 암호화된 원시 바이트를 SSL 라이브러리(OpenSSL 등)를 통해 복호화한다.
주요 포인트:
- 복호화 전까지는 요청의 경계나 프로토콜(HTTP/1.1, HTTP/2, SSH 등)을 알 수 없다.
- 복호화는 CPU 바운드 작업이며, 별도 스레드에서 수행하거나 Read/Accept와 같은 스레드에서 처리할 수 있다.
4. Parse (프로토콜 파싱)¶
복호화된 평문 바이트를 사용하여 합의된 프로토콜에 따라 요청을 파싱한다.
프로토콜별 차이:
- HTTP/1.1: 평문을 읽고 HTTP 스펙(content-length, transfer-encoding 등)에 따라 요청의 시작과 끝을 찾는다.
- HTTP/2, HTTP/3: 바이너리 프로토콜이므로 더 많은 메타데이터를 처리해야 하며, 파싱 비용이 훨씬 크다.
주의사항:
- 읽은 바이트 청크에 완전한 요청이 있을 수도 있고 없을 수도 있다. 프로토콜 시스템 헤더(예: H2의 SETTINGS 프레임)만 있을 수도 있다.
- 파싱은 CPU 사이클을 소비하며, 특히 H2/H3에서 백엔드에 부하를 줄 수 있다.
5. Decode (디코딩/역직렬화)¶
파싱된 요청에 대해 추가 처리를 수행하는 단계다.
주요 작업:
- 역직렬화: JSON이나 Protobuf 바이트를 프로그래밍 언어의 객체/구조체로 변환한다. 이 과정에는 별도의 비용과 메모리가 소요된다. (예: JavaScript에서도
JSON.parse()를 호출해야 한다.) - 문자 인코딩 디코딩: 원시 바이트를 UTF-8 등으로 디코딩한다. UTF-8은 하나의 문자에 최대 4바이트를 사용하므로, 같은 20바이트라도 ASCII와 UTF-8에서 다르게 보일 수 있다.
- 압축 해제: 드물지만 POST로 전송된 대용량 요청 본문이 압축되어 있을 수 있으며, 처리 전에 압축을 해제해야 한다.
6. Process (처리)¶
요청을 완전히 이해한 후 실제로 처리하는 단계다. 데이터베이스 쿼리, 디스크 읽기, 연산 수행 등이 이루어진다.
- 같은 스레드에서 처리할 수 있지만, 전용 워커(worker pool 패턴) 를 두는 것이 권장된다.
요약¶
요청이 처리되기까지 긴 여정을 거친다. 이를 이해하면 백엔드 엔지니어가 각 단계가 병목이 되지 않도록 적절한 아키텍처를 설계할 수 있다.
| 접근 방식 | 설명 |
|---|---|
| 단일 스레드 | 모든 6단계를 하나의 스레드에서 처리 |
| 단계별 전용 스레드 | 각 단계마다 별도 스레드를 할당 |
| 단계 조합 | 일부 단계를 묶어서 스레드에 배분 |
정답은 없으며, 서비스의 특성과 요구사항에 맞게 선택하면 된다.