[Tomcat] 클라이언트의 요청이 Tomcat을 거쳐 Spring DispatcherServlet 컴포넌트까지 도달하는 과정
예전에 Spring DispatcherServlet에서 시작해서 Controller까지 요청이 전달되는 글을 간단히 작성한 적이 있었고, 이를 통해서 Spring 서버에 요청이 들어오면 어떻게 처리되는지 이해할 수 있었다. (https://hyos-dev-log.tistory.com/21) 그러던 중 최근에 DispatcherServlet까지 요청이 어떻게 도달하는지에 대해 궁금증이 생겨 Socket부터 Spring DispatcherServlet까지의 요청 과정을 정리해봤다.
가장 먼저 Socket 통신 과정을 살펴보자
클라이언트 - 서버 프로그램이 실행되면, 커널 영역에서는 통신을 위한 Socket을 생성하는데, 아래와 같은 과정을 거치게 된다.
여기서 서버 프로그램의 과정을 확인해보면 Socket 생성 후, bind() 및 listen() 과정을 거친다. 그리고 accept()를 통해 클라이언트로부터 Connection을 받고, 이후에 데이터를 read() / write() 처리한다. 그렇다면 이제 Tomcat 내부의 요청 처리 과정을 알아보면 되는데, 그전에 Java NIO 대해서 조금이나마 이해하고 있어야 Tomcat 내부 동작에서 처리 방식을 알 수 있다.
Java NIO
Java NIO(New Input Output)의 대표적인 특징으로는 Channel, Buffer, Selector 객체가 있다. NIO에서 모든 I/O 는 Channel로 시작되며, 4가지의 타입이 있는데, 여기서 확인할 부분은 다음과 같다.
- SocketChannel : TCP 프로토콜을 사용해서 네트워크를 통해 데이터를 읽고 쓴다.
- ServerSocketChannel : 들어오는 TCP 연결을 수신(listening)할 수 있다. 들어오는 연결마다 SocketChannel 이 만들어진다.
그리고 Selector는 NIO에서 가장 핵심적인 컴포넌트이다. Channel을 등록한 다음에 Channel로 들어오는 이벤트(connect / accept / read / write)를 감지하여, 처리할 수 있도록 한다. 그리고 보통 아래의 코드처럼 작성해서 사용하면 된다.
public class EchoServer {
public static void main(String[] args) {
// Selector 생성
Selector selector = Selector.open();
// Selector 에 생성되어 있는 SocketChannel 을 등록하는데, 감지할 이벤트를 정의한다.
// - SelectionKey.OP_CONNECT
// - SelectionKey.OP_ACCEPT
// - SelectionKey.OP_READ
// - SelectionKey.OP_WRITE
socketChannel.register(selector, SelectionKey.OP_READ);
// select() 메서드를 통해 등록된 이벤트 중에서 하나 이상의 SocketChannel 이 준비될 때까지 Block 된다.
// 준비된 SocketChannel 의 수를 반환한다.
selector.select();
// selectedKeys() 메서드를 통해 준비된 SocketChannel 를 받는다.
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
// 준비된 SocketChannel 의 이벤트를 처리한다.
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if (key.isAcceptable()) {
// a connection was accepted by a ServerSocketChannel.
} else if (key.isConnectable()) {
// a connection was established with a remote server.
} else if (key.isReadable()) {
// a channel is ready for reading
} else if (key.isWritable()) {
// a channel is ready for writing
}
keyIterator.remove();
}
}
}
더 자세한 내용을 알고 싶다면, https://jenkov.com/tutorials/java-nio/nio-vs-io.html의 링크에 들어가서 확인할 수 있으니 참고하면 될 것 같다.
Tomcat 요청을 처리하는 핵심 구조
NioEndpoint, AbstractEndpoint 클래스
우선 커널 영역에 있는 Socket을 통해 데이터가 전달되면 NioEndpoint 클래스의 진입점으로 들어오게 된다. 그리고 해당 클래스는 AbstractEndpoint 클래스를 상속받고 있다. NioEndpoint 클래스의 핵심은 Acceptor, Poller, SocketProcessor(Worker)이다. 이 3개의 객체를 통해 데이터를 전달한다.
Acceptor 클래스
- ServerSocketChannel에 accept() 메서드를 통해서 커넥션을 얻고, 데이터를 송수신할 SocketChannel를 생성한다.
- Runnable 인터페이스를 구현했고, 데몬 스레드로 실행된다.
Poller 클래스
- PollerEvent를 Queue에 담고, 꺼내서 처리한다.
- Selector 컴포넌트로 SocketChannel을 관리한다. (등록 및 이벤트 확인)
- Runnable 인터페이스를 구현했고, 데몬 스레드로 실행된다.
SocketProcessor 클래스 (Worker)
- NioEndpoint 내부에는 우리가 평상시에 application.yml에서 지정하는 tomcat 옵션들이 있는데, 여기서 maxThreads 및 minSpareThreads 값을 확인하여, Thread Pool을 미리 생성하고 확보해두고 있는다.
- Poller에 의해서 처리할 이벤트가 생기면, Thread Pool에서 Thread를 하나 할당하고, 내부의 doRun() 메서드에 의해 이벤트를 처리한다.
- SocketProcessorBase 클래스를 상속받았고, 최상위로 올라가면 Runnable 인터페이스를 구현했다.
위의 3가지 핵심을 간략하게 살펴봤고, 이제 Socket부터 시작해서 Tomcat 내부 구조를 거쳐 Spring의 DispatcherServlet 컴포넌트까지 어떻게 도달하는지 알아보자.
클라이언트 요청이 Socket을 통해 Tomcat을 거쳐 Spring DispatcherServlet까지 도달하는 과정
Acceptor 클래스 내부의 run 메서드
public class Acceptor<U> implements Runnable {
@Override
public void run() {
socket = endpoint.serverSocketAccept();
endpoint.setSocketOptions(socket);
}
}
가장 먼저 endpoint.serverSocketAccept() 메서드를 통해 새롭게 생성된 SocketChannel 반환받는다. 그리고 endpoint.setSocketOptions(socket) 메서드에 의해서 이후의 요청을 처리한다.
NioEndpoint 클래스 내부의 setSocketOptions 메서드
public class NioEndpoint extends AbstractJsseEndpoint<NioChannel, SocketChannel> {
@Override
protected boolean setSocketOptions(SocketChannel socket) {
NioSocketWrapper socketWrapper = null;
NioChannel channel = null;
// 생략...
channel = new NioChannel(bufhandler);
NioSocketWrapper newWrapper = new NioSocketWrapper(channel, this);
channel.reset(socket, newWrapper);
socketWrapper = newWrapper;
poller.register(socketWrapper);
}
}
위의 코드에서 일부 생략한 부분이 있지만, 여기서 핵심은 NioChannel를 생성하고, SocketChannel과 NioChannel를 NioSocketWrapper 객체로 감싼 후, poller.register(socketWrapper) 메서드에 의해서 Poller로 전달된다.
NioEndpoint 클래스 내부에 있는 Poller 클래스의 register 메서드
public class NioEndpoint extends AbstractJsseEndpoint<NioChannel, SocketChannel> {
public class Poller implements Runnable {
public void register(final NioSocketWrapper socketWrapper) {
socketWrapper.interestOps(SelectionKey.OP_READ);//this is what OP_REGISTER turns into.
PollerEvent event = null;
if (eventCache != null) {
event = eventCache.pop();
}
if (event == null) {
// NioSocketWrapper 객체를 생성자 인자로 받아 PollerEvent 객체를 생성한다.
event = new PollerEvent(socketWrapper, OP_REGISTER);
} else {
event.reset(socketWrapper, OP_REGISTER);
}
// PollerEvent 등록
addEvent(event);
}
}
}
중요한 점이 Poller 클래스 내부의 register 메서드에서 NioSocketWrapper 객체를 생성자 인자로 받아 PollerEvent 객체를 생성하고, addEvent(event) 메서드를 통해 PollerEvent를 어딘가에 저장한다.
NioEndpoint 클래스 내부에 있는 Poller 클래스의 addEvent 메서드
public class NioEndpoint extends AbstractJsseEndpoint<NioChannel, SocketChannel> {
public class Poller implements Runnable {
private final SynchronizedQueue<PollerEvent> events = new SynchronizedQueue<>();
private void addEvent(PollerEvent event) {
events.offer(event);
// 로직이 있지만 생략...
}
}
}
위에서 말한 것처럼 addEvent(event) 메서드를 호출하면, 어딘가에 PollerEvent를 저장한다고 했는데, 실제로는 SynchronizedQueue에 PollerEvent를 등록한다.
NioEndpoint 클래스 내부에 있는 Poller 클래스의 run, events 메서드
public class NioEndpoint extends AbstractJsseEndpoint<NioChannel, SocketChannel> {
public class Poller implements Runnable {
private final SynchronizedQueue<PollerEvent> events = new SynchronizedQueue<>();
public boolean events() {
boolean result = false;
PollerEvent pe = null;
for (int i = 0, size = events.size(); i < size && (pe = events.poll()) != null; i++) {
result = true;
NioSocketWrapper socketWrapper = pe.getSocketWrapper();
SocketChannel sc = socketWrapper.getSocket().getIOChannel();
int interestOps = pe.getInterestOps();
if (sc == null) {
// 생략
} else if (interestOps == OP_REGISTER) {
try {
sc.register(getSelector(), SelectionKey.OP_READ, socketWrapper);
} catch (Exception x) {
// 생략
}
} else {
final SelectionKey key = sc.keyFor(getSelector());
if (key == null) {
// 생략
} else {
// 생략
}
}
// 생략
}
return result;
}
@Override
public void run() {
// Loop until destroy() is called
while (true) {
boolean hasEvents = false;
try {
if (!close) {
// events() 메서드 호출
hasEvents = events();
if (wakeupCounter.getAndSet(-1) > 0) {
// selectorNow() 메서드 호출
keyCount = selector.selectNow();
} else {
// selector() 메서드 호출
keyCount = selector.select(selectorTimeout);
}
wakeupCounter.set(0);
}
// 나머지 코드 생략
} catch (Throwable x) {
// 코드 생략
}
Iterator<SelectionKey> iterator =
keyCount > 0 ? selector.selectedKeys().iterator() : null;
while (iterator != null && iterator.hasNext()) {
SelectionKey sk = iterator.next();
iterator.remove();
NioSocketWrapper socketWrapper = (NioSocketWrapper) sk.attachment();
if (socketWrapper != null) {
processKey(sk, socketWrapper);
}
}
// 생략
}
// 생략
}
}
}
가장 처음에 실행되는 events() 메서드는 Queue에서 PollerEvent를 꺼내서 이벤트에 따라 Selector에 등록하거나 Selector에서 Key 들을 가져온다. 그다음에 selectNow() 또는 select(selectorTimeout) 메서드에 의해서 등록된 이벤트 중에서 하나 이상의 SocketChannel 이 준비될 때까지 Block 상태가 되며, 준비된 채널이 있는 경우 채널의 수를 반환한다. 그리고 selector.selectedKeys() 메서드를 통해 실제로 준비된 SocketChannel 들을 받아서
processKey(SelectionKey, NioSocketWrapper) 메서드를 호출한다.
NioEndpoint 클래스 내부에 있는 Poller 클래스의 processKey 메서드
public class NioEndpoint extends AbstractJsseEndpoint<NioChannel, SocketChannel> {
public class Poller implements Runnable {
private final SynchronizedQueue<PollerEvent> events = new SynchronizedQueue<>();
protected void processKey(SelectionKey sk, NioSocketWrapper socketWrapper) {
try {
if (close) {
// 생략
} else if (sk.isValid()) {
if (sk.isReadable() || sk.isWritable()) {
if (socketWrapper.getSendfileData() != null) {
// 생략
} else {
// 생략
boolean closeSocket = false;
if (sk.isReadable()) {
if (socketWrapper.readOperation != null) {
// 생략
} else if (socketWrapper.readBlocking) {
// 생략
}
// processSocket 메서드를 호출하는 부분
else if (!processSocket(socketWrapper, SocketEvent.OPEN_READ, true)) {
closeSocket = true;
}
}
if (!closeSocket && sk.isWritable()) {
if (socketWrapper.writeOperation != null) {
// 생략
} else if (socketWrapper.writeBlocking) {
// 생략
}
// processSocket 메서드를 호출하는 부분
else if (!processSocket(socketWrapper, SocketEvent.OPEN_WRITE, true)) {
closeSocket = true;
}
}
// 생략
}
}
} else {
// 생략
}
} catch (CancelledKeyException ckx) {
// 생략
} catch (Throwable t) {
// 생략
}
}
}
}
processKey 내부에서는 SelectionKey.isReadable() 또는 SelectionKey.isWritable() 조건에 따라 SocketChannel에서 전달된 데이터를 처리하는 processSocket() 메서드를 호출한다.
AbstractEndpoint 클래스의 processSocket 메서드
public abstract class AbstractEndpoint<S, U> {
public boolean processSocket(SocketWrapperBase<S> socketWrapper,
SocketEvent event, boolean dispatch) {
try {
if (socketWrapper == null) {
return false;
}
SocketProcessorBase<S> sc = null;
if (processorCache != null) {
sc = processorCache.pop();
}
if (sc == null) {
// SocketProcessor 객체를 만든다.
sc = createSocketProcessor(socketWrapper, event);
} else {
sc.reset(socketWrapper, event);
}
// Executor 객체를 가져온다.
Executor executor = getExecutor();
if (dispatch && executor != null) {
// Executor 객체를 통해 ThreadPool 에서 Thread 하나를 할당 받아서 실행된다.
executor.execute(sc);
} else {
sc.run();
}
} catch (RejectedExecutionException ree) {
// 생략
return false;
} catch (Throwable t) {
// 생략
return false;
}
return true;
}
}
SocketProcessor 객체가 존재하지 않으면, createSocketProcessor() 메서드를 통해 SocketProcessor를 생성하고, Executor 객체를 통해 ThreadPool에서 하나의 Thread를 할당받아 실행된다.
server:
port: 9000
tomcat:
threads:
min-spare: 20
max: 100
우리가 평상시 application.yml 에 tomcat 옵션을 설정하면, NioEndpoint 객체가 생성될 때 설정한 옵션 값들을 참고해서 ThreadPool을 미리 생성해두고, SocketProcessor 객체가 생성될 때마다 Thread를 하나씩 할당해준다. (Request 당 하나의 Thread를 할당한다.)
NioEndpoint 클래스 내부에 있는 SocketProcessor 클래스의 doRun 메서드
public class NioEndpoint extends AbstractJsseEndpoint<NioChannel, SocketChannel> {
protected class SocketProcessor extends SocketProcessorBase<NioChannel> {
public SocketProcessor(SocketWrapperBase<NioChannel> socketWrapper, SocketEvent event) {
super(socketWrapper, event);
}
@Override
protected void doRun() {
try {
// 나머지 코드 생략
if (handshake == 0) {
SocketState state = SocketState.OPEN;
// Socket 으로부터 요청을 처리한다.
if (event == null) {
state = getHandler().process(socketWrapper, SocketEvent.OPEN_READ);
} else {
state = getHandler().process(socketWrapper, event);
}
if (state == SocketState.CLOSED) {
// 생략
}
} else if (handshake == -1) {
getHandler().process(socketWrapper, SocketEvent.CONNECT_FAIL);
// 생략
} else if (handshake == SelectionKey.OP_READ) {
// 생략
} else if (handshake == SelectionKey.OP_WRITE) {
// 생략
}
} catch (CancelledKeyException cx) {
// 생략
} catch (VirtualMachineError vme) {
// 생략
} catch (Throwable t) {
// 생략
} finally {
// 생략
}
}
}
}
doRun() 메서드에서는 getHandler().process(NioSocketWrapper, SocketEvent) 메서드를 호출하는데,
Socket으로부터 클라이언트 요청을 처리 본격적으로 처리하게 된다. getHandler()에서 반환되는 객체는 coyote 패키지에 있는 AbstractProtocol 클래스 내부에 있는 ConnectionHandler 클래스의 process() 메서드를 호출한다.
AbstractProtocol 클래스 내부에 있는 ConnectionHandler 클래스의 process 메서드
public abstract class AbstractProtocol<S> implements ProtocolHandler, MBeanRegistration {
protected static class ConnectionHandler<S> implements AbstractEndpoint.Handler<S> {
@SuppressWarnings("deprecation")
@Override
public SocketState process(SocketWrapperBase<S> wrapper, SocketEvent status) {
// 생략
Processor processor = (Processor) wrapper.takeCurrentProcessor();
try {
if (processor == null) {
if (negotiatedProtocol != null && negotiatedProtocol.length() > 0) {
UpgradeProtocol upgradeProtocol = getProtocol().getNegotiatedProtocol(negotiatedProtocol);
if (upgradeProtocol != null) {
processor = upgradeProtocol.getProcessor(wrapper, getProtocol().getAdapter());
if (getLog().isDebugEnabled()) {
// 생략
}
} else if (negotiatedProtocol.equals("http/1.1")) {
// 생략
} else {
// 생략
}
}
}
if (processor == null) {
processor = recycledProcessors.pop();
if (getLog().isDebugEnabled()) {
// 생략
}
}
if (processor == null) {
processor = getProtocol().createProcessor();
register(processor);
if (getLog().isDebugEnabled()) {
// 생략
}
}
// 생략
SocketState state = SocketState.CLOSED;
do {
// Processor 구현체의 process 메서드를 호출한다.
state = processor.process(wrapper, status);
} while (state == SocketState.UPGRADING);
} catch (java.io.IOException e) {
// 생략
} catch (ProtocolException e) {
// 생략
} catch (OutOfMemoryError oome) {
// 생략
} catch (Throwable e) {
// 생략
}
}
}
}
조건에 따라 Processor 구현체를 가져오거나, 어딘가에서 꺼내거나 없으면 새롭게 생성한다. 그리고 생성된 Processor 구현체 클래스의 process() 메서드를 호출하는데 이때, SocketWrapper와 SocketEvent 도 역시 같이 인자에 전달한다.
위의 이미지에서 확인할 수 있듯이 Processor의 하위 클래스 중에서 Http11Processor 클래스가 있는데 해당 클래스가 생성될 때 Request, Response 객체를 생성하고, CoyoteAdapter 도 연결한다.
AbstractProcessorLight 클래스의 process 메서드
public abstract class AbstractProcessorLight implements Processor {
@Override
public SocketState process(SocketWrapperBase<?> socketWrapper, SocketEvent status)
throws IOException {
SocketState state = SocketState.CLOSED;
Iterator<DispatchType> dispatches = null;
do {
if (dispatches != null) {
// 생략
} else if (status == SocketEvent.DISCONNECT) {
// 생략
} else if (isAsync() || isUpgrade() || state == SocketState.ASYNC_END) {
// 생략
} else if (status == SocketEvent.OPEN_WRITE) {
// 생략
} else if (status == SocketEvent.OPEN_READ) {
state = service(socketWrapper);
} else if (status == SocketEvent.CONNECT_FAIL) {
// 생략
} else {
// 생략
}
// 생략
} while (state == SocketState.ASYNC_END || dispatches != null && state != SocketState.CLOSED);
return state;
}
}
process() 메서드를 호출하게 되면, 내부의 service() 메서드를 호출한다. 참고로 service() 메서드는 하위 클래스인 Http11Processor 클래스에 속해 있다.
Http11 Processor 클래스의 service 메서드
public class Http11Processor extends AbstractProcessor {
@Override
public SocketState service(SocketWrapperBase<?> socketWrapper)
throws IOException {
// CoyoteAdapter 클래스의 service 메서드 호출한다.
getAdapter().service(request, response);
}
}
service() 메서드 내부에는 CoyoteApdater 클래스의 service() 메서드를 호출하는데, 이때 Http11Processor 가 생성될 때 만들어졌던 Request, Response를 인자로 담아서 보낸다.
CoyoteAdapter 클래스의 service 메서드
public class CoyoteAdapter implements Adapter {
@Override
public void service(org.apache.coyote.Request req, org.apache.coyote.Response res)
throws Exception {
// 핵심 코드
connector
.getService()
.getContainer()
.getPipeline()
.getFirst()
.invoke(request, response);
}
}
내부 로직이 상당히 길어 모든 부분을 생략하고 핵심 부분 하나만 남겨뒀는데, 바로 connector를 통해 Service, Engine(Container)를 호출하는 코드이며, getFirst() 메서드를 통해 Vavle라는 구현체를 가져오고 마지막에 invoke() 메서드를 통해 Request, Response 객체를 인자로 보낸다.
요청 처리시 사용되는 Valve 클래스
- StandardEngineValve
- StandardHostValve
- StandardContextValve
- StandardWrapperValve
Valve는 하나의 특정 컨테이너와 관련된 요청 처리 컴포넌트이며, 위의 4가지 Valve들을 차례대로 거치면서 Request, Response가 전달된다.
StandardEngineValve 클래스의 invoke 메서드
final class StandardEngineValve extends ValveBase {
@Override
public final void invoke(Request request, Response response)
throws IOException, ServletException {
host.getPipeline().getFirst().invoke(request, response);
}
}
getFirst() 메서드를 통해 StandardHostValve의 invoke() 메서드를 호출하여 Request, Response를 전달한다.
StandardHostValve 클래스의 invoke 메서드
final class StandardHostValve extends ValveBase {
@Override
public final void invoke(Request request, Response response)
throws IOException, ServletException {
context.getPipeline().getFirst().invoke(request, response);
}
}
getFirst() 메서드를 통해 StandardContextValve의 invoke() 메서드를 호출하여 Request, Response를 전달한다.
StandardContextValve 클래스의 invoke 메서드
final class StandardContextValve extends ValveBase {
@Override
public final void invoke(Request request, Response response)
throws IOException, ServletException {
wrapper.getPipeline().getFirst().invoke(request, response);
}
}
getFirst() 메서드를 통해 StandardWrapperValve의 invoke() 메서드를 호출하여 Request, Response를 전달한다.
StandardWrapperValve 클래스의 invoke 메서드
final class StandardWrapperValve extends ValveBase {
@Override
public final void invoke(Request request, Response response)
throws IOException, ServletException {
// 생성되어 있는 DispatcherServlet 객체를 servlet 변수에 할당받는다.
servlet = wrapper.allocate();
ApplicationFilterChain filterChain =
ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);
filterChain.doFilter(request.getRequest(), response.getResponse());
}
}
StandardWrapperValve 클래스의 invoke() 메서드는 가장 중요하다. 내부에서 생성되어 있는 DispatcherServlet을 할당받아 Filter를 생성하고, filterChain.doFilter(Request, Response) 메서드를 통해 실질적으로 Spring 환경에서의 Request, Response 처리 작업이 시작된다.
ApplicationFilterChain 클래스의 doFilter, internalDoFilter 메서드
public final class ApplicationFilterChain implements FilterChain {
@Override
public void doFilter(ServletRequest request, ServletResponse response)
throws IOException, ServletException {
internalDoFilter(request, response);
}
private void internalDoFilter(ServletRequest request,
ServletResponse response)
throws IOException, ServletException {
if (pos < n) {
// 생략
filter.doFilter(request, response, this);
// 생략
return;
}
// service 호출
servlet.service(request, response);
}
}
internalDoFilter() 메서드에서는 필터가 여러 개 존재하면, if (pos < n) 조건에 따라 doFilter() 메서드가 호출된다. 필터가 존재하는 만큼 doFilter() 메서드는 재귀 호출되며, 필터가 없으면 return에 의해서 종료된다. 그 이후에 service() 메서드를 통해 Servlet 영역으로 넘어간다.
FrameworkServlet 클래스의 service, processRequest 메서드
public abstract class FrameworkServlet extends HttpServletBean implements ApplicationContextAware {
@Override
protected void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
HttpMethod httpMethod = HttpMethod.resolve(request.getMethod());
if (httpMethod == HttpMethod.PATCH || httpMethod == null) {
processRequest(request, response);
} else {
super.service(request, response);
}
}
protected final void processRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// DispatcherServlet 클래스의 doService 메서드를 호출한다.
doService(request, response);
}
}
이제 마지막으로 FrameworkServlet 클래스를 보면 되는데, 필터에서 요청이 넘어오면 HTTP 메서드가 PATCH 인 경우에는 processRequest() 메서드가 호출되고, 나머지 HTTP 메서드는 상위 클래스인 HttpServlet 클래스의 service() 메서드를 호출하게 된다. (super.service() 메서드를 호출한다.) 그리고 processRequest() 메서드 내부에서는 하위 클래스인 DispatcherServlet 클래스의 doService() 메서드를 호출하면서 Request, Response를 전달함으로써 Socket을 통해 Tomcat을 거쳐 Spring의 DispatcherServlet까지 요청이 전달되는 것을 확인할 수 있다.
정리
NioEndpoint 진입점으로부터 Acceptor 클래스의 ServerSocketChannel.accept() 메서드를 통해 클라이언트 커넥션을 받아 SocketChannel을 생성하고, PollerEvent로 변환하여 PollerEventQueue에 넣고 Poller클래스의 run() 메서드에서는 Queue에서 PollerEvent를 꺼낸 다음 Selector에 의해 SocketChannel들이 관리되도록 한다. 그리고 ThreadPool에서 Thread 하나를 SocketProcessor에 할당하여 Request당 하나의 Thread가 처리될 수 있도록 한다.
AbtractProtocol의 ConnectionHandler 클래스를 통해 AbstractractProcessorLight 및 Http11Processor 클래스에 SocketWrapper를 전달하고, Processor는 CoyoteAdapter 클래스에 Request, Response 객체를 보낸다. CoyoteAdapter 클래스에서는 Connector -> Service -> Engine -> Context -> Wrapper 과정을 거쳐 FrameworkServlet 클래스의 service() 및 processRequest() 메서드를 호출하며, 최종적으로 DispatcherServlet 클래스의 doService() 메서드를 호출한다.