1. Browser -> Connector (HTTP Request)
이 문서는 open-source/tomcat 실제 소스를 기준으로, 브라우저가 보낸 HTTP 바이트가 Tomcat Connector에 들어와 CoyoteAdapter.service() 직전까지 어떻게 흘러가는지 추적합니다.
이 단계의 끝은 Http11Processor가 getAdapter().service(request, response)를 호출하는 지점이며, 그 순간부터 2번 문서가 시작됩니다.
이번 단계의 역할
1번은 아직 Servlet 컨테이너(Catalina) 내부로 들어가기 전입니다. Tomcat은 먼저 네트워크 연결을 수락하고, NIO Poller에 등록하고, 작업 스레드로 넘긴 다음, HTTP/1.1 Processor가 요청 라인과 헤더를 파싱한 뒤 Adapter 경계로 위임합니다.
즉 이 페이지의 핵심은 다음 한 문장으로 요약할 수 있습니다.
브라우저의 TCP/HTTP 입력을 Tomcat이 “처리 가능한 HTTP 요청”으로 바꿔
CoyoteAdapter에 넘기는 단계
호출 흐름 요약
Acceptor#run()이 새 연결을 받습니다.NioEndpoint#setSocketOptions()가 소켓을 non-blocking으로 바꾸고 Poller에 등록합니다.- Poller가
OPEN_READ이벤트를 감지하면processSocket(..., true)로 작업을 Executor에 디스패치합니다. NioEndpoint.SocketProcessor#doRun()이getHandler().process(...)를 호출합니다.AbstractProtocol.ConnectionHandler#process(...)가 Processor를 재사용하거나 새로 생성합니다.Http11Processor#service(...)가 요청을 파싱한 뒤getAdapter().service(request, response)로 위임합니다.
호출 흐름 다이어그램
핵심 코드
1) 연결 수락 후 소켓을 다음 단계로 넘기는 지점
// Acceptor.java
endpoint.countUpOrAwaitConnection();
socket = endpoint.serverSocketAccept();
if (!endpoint.setSocketOptions(socket)) {
endpoint.closeSocket(socket);
}
Acceptor는 연결을 받은 뒤 직접 HTTP를 해석하지 않습니다. 성공적으로 받은 소켓을 setSocketOptions()로 넘기며, 여기서부터 NIO 기반 처리 흐름이 시작됩니다.
2) non-blocking 전환과 Poller 등록
// NioEndpoint.java
socket.configureBlocking(false);
socketWrapper.setReadTimeout(getConnectionTimeout());
socketWrapper.setWriteTimeout(getConnectionTimeout());
socketWrapper.setKeepAliveLeft(NioEndpoint.this.getMaxKeepAliveRequests());
poller.register(socketWrapper);
여기서 소켓은 블로킹 I/O가 아니라 Selector 기반 감시 대상으로 바뀝니다. 즉 “연결을 받는 단계”와 “읽을 데이터가 준비된 연결을 처리하는 단계”가 분리됩니다.
3) 읽기 이벤트를 작업 스레드로 디스패치
// AbstractEndpoint.java
SocketProcessorBase<S> sc = null;
if (processorCache != null) {
sc = processorCache.pop();
}
if (sc == null) {
sc = createSocketProcessor(socketWrapper, event);
} else {
sc.reset(socketWrapper, event);
}
Executor executor = getExecutor();
if (dispatch && executor != null) {
executor.execute(sc);
} else {
sc.run();
}
Poller는 이벤트 감지까지만 담당하고, 실제 요청 처리는 SocketProcessor를 통해 Executor 스레드에서 수행합니다.
4) Processor가 실제 HTTP 처리를 시작하는 지점
// NioEndpoint.java
state = getHandler().process(socketWrapper,
Objects.requireNonNullElse(event, SocketEvent.OPEN_READ));
// AbstractProtocol.java
if (processor == null) {
processor = recycledProcessors.pop();
}
if (processor == null) {
processor = getProtocol().createProcessor();
register(processor);
}
state = processor.process(wrapper, status);
// AbstractHttp11Protocol.java
@Override
protected Processor createProcessor() {
return new Http11Processor(this, adapter);
}
ConnectionHandler는 요청마다 적절한 Processor를 준비하고, HTTP/1.1인 경우 Http11Processor를 사용합니다.
5) Catalina 진입 직전의 마지막 호출
// Http11Processor.java
if (getErrorState().isIoAllowed()) {
rp.setStage(org.apache.coyote.Constants.STAGE_SERVICE);
getAdapter().service(request, response);
}
이 호출이 1번의 종료 지점입니다. 여기까지는 Coyote 계층이 요청을 읽고 파싱한 단계이고, 이제부터는 Adapter를 통해 Catalina 쪽으로 넘어갑니다.
코드 해설
1) Acceptor는 “연결 수락”까지만 맡습니다
Acceptor#run()은 새 연결을 받아들인 뒤 setSocketOptions()로 넘깁니다. 이 구조 덕분에 accept 루프는 새 연결 유입 제어에 집중하고, 실제 읽기/쓰기 처리 로직은 다른 계층으로 분리됩니다.
2) Poller 등록은 “나중에 읽을 준비가 되면 알려달라”는 의미입니다
socket.configureBlocking(false)와 poller.register(socketWrapper)는 요청을 즉시 다 처리하겠다는 뜻이 아닙니다. Poller가 Selector를 통해 해당 소켓의 읽기 가능 상태를 감지하면, 그때 비로소 처리 단계가 시작됩니다.
3) Poller와 SocketProcessor를 분리한 이유는 지연 전파를 막기 위해서입니다
이벤트 감시 루프가 무거운 HTTP 처리까지 직접 맡으면, 다른 소켓들의 readiness 감지가 늦어집니다. Tomcat은 processSocket(..., dispatch=true)와 executor.execute(sc)를 통해 감지와 실행을 분리합니다.
4) ConnectionHandler는 Processor 선택기이자 재사용 지점입니다
AbstractProtocol.ConnectionHandler#process(...)는 Processor를 바로 새로 만들지 않고 recycledProcessors.pop()부터 확인합니다. 즉 Tomcat은 요청당 객체를 매번 새로 만들기보다 가능한 한 재사용해 GC 비용을 줄이려 합니다.
5) 1번 페이지의 마지막은 “HTTP 파싱 완료 후 Adapter 위임”입니다
Http11Processor#service(...) 안에서 요청 라인, 헤더, keep-alive, 오류 상태 같은 HTTP/1.1 관련 처리가 진행되고, 마지막에 getAdapter().service(request, response)가 호출됩니다. 이 시점부터는 네트워크 프로토콜 처리보다 컨테이너 연결이 핵심이 되므로 다음 문서인 2번으로 넘어갑니다.
설계 의도
1) 왜 Acceptor와 Poller를 분리했는가
Acceptor는 새 연결 수락, Poller는 기존 연결 이벤트 감시를 맡습니다. 이 분리는 새 연결 유입 제어와 기존 연결 처리의 성격이 다르기 때문에 필요합니다.
2) 왜 non-blocking + Selector를 쓰는가
Tomcat은 socket.configureBlocking(false)와 Poller 등록을 통해 소수의 스레드가 다수 연결을 감시하도록 만듭니다. 이 구조는 스레드 하나가 연결 하나에 묶이는 blocking 모델보다 동시성 효율이 높습니다.
3) 왜 Processor와 이벤트 객체를 재사용하는가
processorCache.pop()과 recycledProcessors.pop()가 보여주듯 Tomcat은 고빈도 요청에서 객체 생성과 GC를 줄이는 방향으로 설계되어 있습니다. 요청량이 높을수록 이런 재사용 전략은 처리량과 tail latency에 직접 영향을 줍니다.
4) 왜 Adapter 경계를 두는가
AbstractHttp11Protocol#createProcessor()가 new Http11Processor(this, adapter)를 만들고, Http11Processor는 마지막에 getAdapter().service(...)만 호출합니다. 즉 Coyote는 Adapter 인터페이스까지만 알고, Catalina 내부 구조는 모르게 설계되어 있습니다.
결국 1번의 본질은 “브라우저의 바이트 스트림을 Coyote가 받아서, Catalina가 처리할 수 있는 호출 지점까지 전달하는 것” 입니다.
다음 단계 연결
다음 문서 2번에서는 CoyoteAdapter.service() 내부로 들어가, 왜 org.apache.coyote.Request/Response를 org.apache.catalina.connector.Request/Response로 감싸고 postParseRequest()를 수행해야 하는지 이어서 추적합니다.