Tomcat 은 어떻게 동작할까? - Spring 과의 연동을 중점으로 (3)

정원식·2023년 12월 16일
0

23년 5월에 작성한 글입니다.

개요

  • 본 시리즈에서는 사용자의 요청이 톰캣에서 어떻게 처리되어 우리가 작성한 비즈니스 로직에 도달하는지에 대해 다룹니다.
  • 세번째 편에서는 Tomcat 에서 어떤 과정을 거쳐 우리에게 요청이 넘어오는지 살펴봅니다.
    • 두번째 편의 Tomcat 설정에 기반합니다.

사용한 버전

  • Servlet: 4.0.1
  • Tomcat: 9.0.60
  • Spring Boot: 2.6.6
  • Spring WebMvc: 5.3.18

톰캣에서 DispatcherServlet 으로 요청이 오기까지

  • 사용자의 요청이 톰캣을 거쳐 DispatcherServlet 으로 오기까지 아래의 단계를 거칩니다.
  1. 커넥션 생성
    • 클라이언트와 커넥션 (SocketChannel) 을 생성하고 해당 커넥션에 대한 프로세싱 (SocketProcessor) 을 스레드풀에서 실행합니다.
  2. Processor -> Valve
    • SocketChannelHttpServletRequest, HttpServletResponse 객체로 변환 하여 StandardEngineValve 에 넘겨줍니다.
  3. Valve -> Filter
    • 여러 밸브를 거치다가 필터링 단계에 도달합니다.
    • 밸브는 특정 컨테이너(Engine, Host, Context, Wrapper) 내에서 요청을 처리합니다.
  4. Filter -> Servlet
    • 필터링 단계를 거쳐 DispatcherServlet 에 처리를 위임합니다.

1. 커넥션 생성

org.apache.tomcat.util.net.Acceptor

  • 커넥션 생성
  • Poller 의 이벤트 큐 (SynchronizedQueue) 에 소켓을 래핑하여 등록
public class Acceptor<U> implements Runnable {

    public void run() {
        ...
        while (!stopCalled) {
            ...
            // 커넥션 생성
            socket = endpoint.serverSocketAccept();

            ...
            // 커넥션 프로세싱
            if (!endpoint.setSocketOptions(socket)) {
                // 프로세싱 실패시, 커넥션 종료
                endpoint.closeSocket(socket);
            }
        }
    }
}

public class NioEndpoint extends AbstractJsseEndpoint<NioChannel,SocketChannel> {

    protected boolean setSocketOptions(SocketChannel socket) {
        ...
        channel = new NioChannel(bufhandler);

        ...
        // socker 을 socketWrapper 로 변환
        NioSocketWrapper newWrapper = new NioSocketWrapper(channel, this);
        channel.reset(socket, newWrapper);
        connections.put(socket, newWrapper);
        socketWrapper = newWrapper;

        ...
        poller.register(socketWrapper);     // poller 에 소켓 이벤트 등록
    }
}

org.apache.tomcat.util.net.NioEndpoint$Poller

  1. 이벤트 핸들링
    • 이벤트 큐에서 이벤트 확인
    • Selector 에 이벤트에 래핑된 소켓을 등록
  2. Selector 에서 polling
    • 확인된 키에 등록된 attachment (SockerProcessor) 실행
public class Poller implements Runnable {

    public void run() {
        while (true) {

            ...
            // 앞서 등록된 소켓 이벤트 확인 및 Selector 에 소켓(Channel) 등록
            hasEvents = events();

            ...
            // Selector 에서 연산이 필요한 SelectionKey 조회
            // SelectionKey 는 Channel 과 Attachment 를 지님
            Iterator<SelectionKey> iterator = keyCount > 0 ? selector.selectedKeys().iterator() : null;

            // 연산이 필요한 SelectionKey 프로세싱
            while (iterator != null && iterator.hasNext()) {
                SelectionKey sk = iterator.next();
                iterator.remove();
                NioSocketWrapper socketWrapper = (NioSocketWrapper) sk.attachment();

                if (socketWrapper != null) {
                    // 프로세싱
                    processKey(sk, socketWrapper);
                }
            }
        }
    }

    public boolean events() {
        // 등록된 이벤트 핸들링
        for (int i = 0, size = events.size(); i < size && (pe = events.poll()) != null; i++ ) {
            NioSocketWrapper socketWrapper = pe.getSocketWrapper();
            SocketChannel sc = socketWrapper.getSocket().getIOChannel();

            ...
            // Selector 에 새로 등록이 필요한 이벤트라면
            else if (interestOps == OP_REGISTER) {
                try {
                    // Selector 에 읽기 연산이 필요한것으로 등록
                    sc.register(getSelector(), SelectionKey.OP_READ, socketWrapper);
                } catch (Exception x) {
                    log.error(sm.getString("endpoint.nio.registerFail"), x);
                }
            }
        }
    }

    protected void processKey(SelectionKey sk, NioSocketWrapper socketWrapper) {

        ...
        // 읽기 연산이 필요한경우
        if (sk.isReadable()) {
            // 프로세싱
            } else if (!processSocket(socketWrapper, SocketEvent.OPEN_READ, true)) {
                closeSocket = true;
            }
        }
    }

    public boolean processSocket(SocketWrapperBase<S> socketWrapper, SocketEvent event, boolean dispatch) {

        ...
        // SockerProcessor 생성
        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();
        }
    }
}

org.apache.tomcat.util.net.NioEndpoint$SocketProcessor

  • Runnable 을 상속하여 스레드풀 내에서 실행됨

2. Processor -> Valve

org.apache.tomcat.util.net.NioEndpoint$SocketProcessor

protected class SocketProcessor extends SocketProcessorBase<NioChannel> {
    protected void doRun() {

        ...
        if (socketWrapper.getSocket().isHandshakeComplete()) {
            // TLS handshake 가 필요하지 않다면
            handshake = 0;
        }

        ...
        if (handshake == 0) {
            SocketState state = SocketState.OPEN;

            if (event == null) {
                state = getHandler().process(socketWrapper, SocketEvent.OPEN_READ);
            } else {
                // 핸들러를 통해 프로세싱
                state = getHandler().process(socketWrapper, event);
            }
        }
    }
}

org.apache.coyote.AbstractProtocol$ConnectionHandler

protected static class ConnectionHandler<S> implements AbstractEndpoint.Handler<S> {
    public SocketState process(SocketWrapperBase<S> wrapper, SocketEvent status) {

        ...
        // Http11Processor 호출
        state = processor.process(wrapper, status);
    }
}

org.apache.coyote.http11$Http11Processor

public class Http11Processor extends AbstractProcessor {

    public SocketState service(SocketWrapperBase<?> socketWrapper) throws IOException {

        ...
        // HTTP 프로토콜 확인 (0.9, 1.0, 1.1)
        prepareRequestProtocol();

        ...
        // Request 헤더 파싱
        prepareRequest();

        ...
        // KeepAlive 처리
        int maxKeepAliveRequests = protocol.getMaxKeepAliveRequests();
        if (maxKeepAliveRequests == 1) {
            keepAlive = false;
        } else if (maxKeepAliveRequests > 0 &&
                socketWrapper.decrementKeepAlive() <= 0) {
            keepAlive = false;
        }

        ...
        // CoyoteAdapter 호출
        getAdapter().service(request, response);
    }
}

org.apache.catalina.connector$CoyoteAdapter

public class CoyoteAdapter implements Adapter {

    public void service(org.apache.coyote.Request req, org.apache.coyote.Response res) throws Exception {

        ...
        // StandardEngineValve 호출
        connector.getService().getContainer().getPipeline().getFirst().invoke(request, response);
    }
}

3. Valve -> Filter

  • 여러 Valve 를 거쳐 ApplicationFilterChain 에 도달합니다.
  • Spring Boot 에서는 Filter or Interceptor 로 WAS 에 관계없이 처리가 가능하므로
    • 공통 프로세싱을 Filter 와 Interceptor 에서 처리하고 최소한의 Valve 만 설정한것으로 보입니다.
    • 다양한 Valve 목록
  • 이름과는 다르게 StandardHostValve 에서 에러페이지를 처리합니다. (Whitelabel Error Page)

4. Filter -> Servlet

org.apache.catalina.core.ApplicationFilterChain

  • 필터링 후 Serlvet 을 호출
public final class ApplicationFilterChain implements FilterChain {

    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) {
            ApplicationFilterConfig filterConfig = filters[pos++];
            try {
                Filter filter = filterConfig.getFilter();

                ...
                // 다음 필터에서 필터링 수행
                filter.doFilter(request, response, this);
            }
        }

        ...
        // 필터링이 끝났다면 Servlet 호출
        servlet.service(request, response);
}

결론

  • Tomcat (HTTP/1.1) 에서 사용자의 요청을 크게 4단계를 거쳐 DispatcherServlet 에 전달합니다.

Reference

profile
매일매일 성장하고 싶은 백엔드 개발자입니다.

0개의 댓글