1. Browser -> Connector (HTTP Request)
이 문서는 open-source/tomcat 실제 코드 기준으로, 브라우저 요청이 Connector 경계에 도달해 Coyote 레이어에서 처리되는 흐름을 추적합니다.
핵심은 “accept -> poller -> socket processor -> protocol processor -> adapter” 호출 체인입니다.
시퀀스 흐름
코드 기준 결론
- 소켓 수락은
org.apache.tomcat.util.net.Acceptor#run()에서 수행되고, 실제accept()호출은NioEndpoint#serverSocketAccept()가 담당합니다. - 수락된 소켓은
NioEndpoint#setSocketOptions()에서 non-blocking으로 전환된 뒤poller.register(socketWrapper)로 Selector에 등록됩니다. - Poller가
OP_READ이벤트를 받으면processSocket(..., SocketEvent.OPEN_READ, true)로 작업을 Executor에 디스패치합니다. - Executor 스레드의
NioEndpoint.SocketProcessor#doRun()이getHandler().process(...)를 호출합니다. AbstractProtocol.ConnectionHandler#process(...)는 Processor를 선택/생성하고processor.process(...)를 호출합니다.- HTTP/1.1에서는
AbstractHttp11Protocol#createProcessor()가new Http11Processor(this, adapter)를 만들고,Http11Processor#service(...)에서 최종적으로getAdapter().service(request, response)가 호출됩니다. - 이
Adapter는Connector#initInternal()에서new CoyoteAdapter(this)로 주입됩니다. 즉 1번의 끝지점이 2번(CoyoteAdapter.service)의 시작지점입니다.
오픈 소스 호출 흐름 (NIO + HTTP/1.1)
- 브라우저가 TCP 연결을 생성하고 HTTP 바이트를 전송합니다.
Acceptor#run()이endpoint.serverSocketAccept()를 호출해 연결을 수락합니다.- NIO 구현에서는
NioEndpoint#serverSocketAccept()가serverSock.accept()를 실행합니다. Acceptor#run()은 이어서endpoint.setSocketOptions(socket)를 호출합니다.NioEndpoint#setSocketOptions()는socket.configureBlocking(false)후poller.register(socketWrapper)를 수행합니다.Poller#events()가 등록 이벤트를 처리하며SelectionKey.OP_READ로 Selector에 등록합니다.Poller#run()이 준비된 키를 순회하고processKey(...)에서 읽기 이벤트 시processSocket(socketWrapper, SocketEvent.OPEN_READ, true)를 호출합니다.AbstractEndpoint#processSocket()는createSocketProcessor(...)로 작업 객체를 만들고executor.execute(sc)로 실행합니다.NioEndpoint.SocketProcessor#doRun()이getHandler().process(socketWrapper, event)를 호출합니다.AbstractProtocol.ConnectionHandler#process(...)가 Processor를 선택/생성한 뒤processor.process(wrapper, status)를 호출합니다.- HTTP/1.1의 경우
AbstractHttp11Protocol#createProcessor()에서 생성된Http11Processor가service(...)를 실행합니다. Http11Processor#service(...)내부에서 요청라인/헤더 파싱 후getAdapter().service(request, response)로 컨테이너 어댑터에 위임합니다.
왜 이런 코드 구조인가 (설계 의도 분석)
1) 왜 Acceptor와 Poller를 분리했는가
Acceptor#run()은 연결 수락 자체에만 집중하고, 실제 I/O 처리는 Poller/Processor로 넘깁니다.
Acceptor#run()주석과 코드에서countUpOrAwaitConnection()으로 최대 연결 수를 먼저 제어합니다.setSocketOptions()주석에 “성공하면 적절한 processor에게 handed off”라고 명시되어 있습니다.- pause 상태에서 tight loop -> 짧은 sleep으로 전환하는 로직이 있어, 종료/일시정지 시 CPU 과점을 줄이려는 의도가 드러납니다.
즉, accept 단계는 “새 연결 유입 제어”, poll 단계는 “기존 연결 이벤트 처리”로 역할을 분리해 확장성과 운영 안정성을 동시에 확보합니다.
2) 왜 non-blocking + Selector를 쓰는가
NioEndpoint#setSocketOptions()에서 socket.configureBlocking(false)를 강제하고 poller.register(socketWrapper)로 등록합니다.
그리고 Poller#events()는 SelectionKey.OP_READ로 소켓을 Selector에 등록합니다.
이 구조는 스레드 하나당 소켓 하나를 블로킹으로 잡아두지 않고, 소수의 스레드가 다수 소켓 이벤트를 multiplexing 하게 만들어 동시성 효율을 높입니다.
3) 왜 Poller 이벤트를 즉시 처리하지 않고 Executor로 디스패치하는가
Poller#processKey(...)는 바로 비즈니스 처리를 하지 않고 processSocket(..., dispatch=true)를 호출합니다.
AbstractEndpoint#processSocket(...)는 dispatch가 true이면 executor.execute(sc)로 별도 컨테이너 스레드에서 실행합니다.
이 설계의 이유는 Selector 루프가 무거운 처리를 직접 수행하면 전체 소켓 감시 지연이 커지기 때문입니다. 즉, Poller는 “이벤트 감지”, SocketProcessor는 “실제 처리”로 분리해 지연 전파를 막습니다.
4) 왜 캐시/재사용 코드가 많은가
Tomcat은 고빈도 요청에서 GC 비용을 줄이기 위해 객체 재사용을 적극 사용합니다.
NioEndpoint.PollerEvent주석: “cacheable object … avoid GC”AbstractEndpoint#processSocket(...):processorCache.pop()후 재사용AbstractProtocol.ConnectionHandler#process(...):recycledProcessors.pop()재사용
요청당 객체를 매번 새로 만들면 지연이 튀기 쉬워서, 이벤트 객체/프로세서를 풀링해 처리량과 지연 안정성을 확보합니다.
5) 왜 ConnectionHandler에서 timeout을 NO-OP 처리하는 분기가 있는가
AbstractProtocol.ConnectionHandler#process(...)에는 timeout 이벤트가 이미 불필요해진 경우 즉시 SocketState.OPEN으로 반환하는 분기가 있습니다.
코드 주석도 “dispatch 지연 때문에 timeout이 더 이상 필요 없을 수 있으니 불필요한 처리 회피”를 명시합니다.
이유는 timeout 스레드와 실제 처리 스레드가 분리되어 있기 때문입니다. 늦게 도착한 timeout 이벤트를 그대로 처리하면 정상 요청까지 불필요하게 닫을 수 있습니다.
6) 왜 Adapter 경계를 두는가
Adapter 인터페이스 주석은 “coyote-based servlet container entry point”라고 명확히 정의합니다.
또한 Connector#initInternal()에서 new CoyoteAdapter(this)를 생성해 protocolHandler.setAdapter(adapter)로 주입합니다.
즉, Coyote(프로토콜 처리)는 Adapter 인터페이스까지만 알고, Catalina(서블릿 컨테이너) 구현 세부는 모르게 설계되었습니다. 프로토콜 처리와 컨테이너 처리를 분리해 교체 가능성과 유지보수성을 높이려는 목적입니다.
7) 왜 Http11Processor#service(...)가 이렇게 많은 분기를 가지는가
Http11Processor#service(...)에는 실제 운영에서 필수인 분기가 함께 들어 있습니다.
- 부분 요청 읽기(
parseHeaders()실패 시 open socket 유지) - 업그레이드(
Connection: upgrade) 처리 - keep-alive 요청 수 제한
- 파싱 실패 시 400/500/503 상태 설정
- 마지막에
getAdapter().service(...)위임
이 메서드는 단순 파서가 아니라 “HTTP/1.1 상태머신 + 커넥션 수명 제어 + 컨테이너 진입 게이트” 역할을 동시에 수행하도록 작성되어 있습니다.
왜 이게 1번의 본질인가
1번은 아직 Catalina 파이프라인에 들어가기 전 단계입니다.
Http11Processor가 바이트를 HTTP 요청으로 파싱하고 Adapter.service(...)를 부르는 순간까지가 정확히 1번입니다.
그 다음 2번 문서에서 다루는 CoyoteAdapter.service(...)가 시작됩니다.
다음 탐구
다음 문서 2번에서 CoyoteAdapter.service() 내부를 보며,
왜 Request/Response 래핑과 postParseRequest()가 필요한지 이어서 파고들면 전체 흐름이 연결됩니다.