Tomcat, Embedded Tomcat

appti·2024년 5월 5일
2

분석

목록 보기
22/25

서론

Tomcat의 컴포넌트나 구조에 대해 살펴보고, 실제 스프링 부트에 사용되는 구현체나 세팅 과정 등에 대한 흐름에 대해 살펴보고자 합니다.

요약

내용이 길기 때문에, 본론에 앞서 글을 간단히 요약했습니다.

컴포넌트

Tomcat의 컴포넌트 중 핵심은 다음과 같습니다.

  • catalina
    • 톰캣의 서블릿 컨테이너 역할을 수행하는 컴포넌트 입니다.
      • 톰캣 서버를 가동시키는 경우 catalina 컴포넌트를 가동한 것이라고 볼 수 있습니다.
  • coyote
    • TCP 기반으로 톰캣의 프로토콜을 지원합니다.
    • 클라이언트의 요청을 받고, 응답을 반환하는 역할을 수행합니다.
  • tomcat
    • Tomcat 웹 서버 역할을 수행하는 컴포넌트입니다.

Embed Tomcat

Embed Tomcat의 경우 Tomcat의 기능을 최적화해 스프링 부트 애플리케이션에 적합하게 만들기 위해 Tomcat의 컴포넌트를 분리해 별도의 라이브러리로 만든 것입니다.

MVC 기반의 스프링 부트 애플리케이션을 사용하는 경우 다음과 같은 라이브러리를 의존합니다.

  • Tomcat Embed Core
    • 이름 그대로 Tomcat의 핵심 기능을 제공하는 라이브러리입니다.
    • Tomcat이 WAS의 역할에 충실할 수 있도록 하는 라이브러리입니다.
  • Tomcat Embed EL
    • 이름 그대로 EL(Expression Language)에 대한 내용만이 존재합니다.
  • Tomcat Embed WebSocket
    • 이름 그대로 웹 소켓에 대한 내용만이 존재합니다.

구조

Tomcat의 구조는 위와 같습니다.

특이 사항은 다음과 같습니다.

  • Engine부터 Tomcat의 Container라고 표현합니다.
  • Realm의 경우 인증/인가에 사용되지만, Tomcat에서 인증/인가 처리를 하지 않으므로 사실상 의미가 없습니다.
  • Logger의 경우 Slf4j의 Slf4j Bridge를 통해 치환됩니다.
  • Container는 하나의 Pipeline과 여러 Valve를 가지며, Valve를 통해 요청이 라우팅됩니다.

요청 처리

위 과정을 통해 요청 데이터를 파싱합니다.

위 과정을 통해 Servlet.service()를 호출해 클라이언트의 요청을 처리할 수 있는 비즈니스 로직을 수행합니다.


컴포넌트

Tomcat

톰캣의 경우 위와 같이 7개의 컴포넌트를 가지고 있습니다.

  • catalina
    • 톰캣의 서블릿 컨테이너 역할을 수행하는 컴포넌트 입니다.
      • 톰캣 서버를 가동시키는 경우 catalina 컴포넌트를 가동한 것이라고 볼 수 있습니다.
  • coyote
    • TCP 기반으로 톰캣의 프로토콜을 지원합니다.
    • 클라이언트의 요청을 받고, 응답을 반환하는 역할을 수행합니다.
  • el
    • Expression Language 기능을 지원합니다.
  • jasper
    • 톰캣의 JSP 엔진입니다.
    • JSP 파일을 파싱해 서블릿 코드로 컴파일하고, JSP 파일의 변경을 감지해 리컴파일 작업을 수행합니다.
  • juli
    • 톰캣의 로그 기능을 제공합니다.
  • naming
    • JNDI 리소스와 관련된 기능을 제공합니다.
    • 외부 리소스를 JNDI 네임스페이스에 바인딩하고 웹 애플리케이션에서 이를 조회해 사용할 수 있습니다.
  • tomcat
    • Tomcat의 웹 서버 역할을 수행하는 컴포넌트입니다.

추가적으로, Tomcat의 패키지에는 포함되어 있지 않지만 웹 소켓과 관련된 패키지도 존재합니다.

Embed Tomcat

Embed Tomcat의 경우 Tomcat의 기능을 최적화해 스프링 부트 애플리케이션에 적합하게 만들기 위해 Tomcat의 컴포넌트를 분리해 별도의 라이브러리로 만들었습니다.

다음과 같은 라이브러리가 존재합니다.

  • Tomcat Embed Core
  • Tomcat Embed Jasper
  • Tomcat Embed Logging JULI
  • Tomcat Embed WebSocket
  • Tomcat Embed EL
  • Tomcat Embed Logging Log4j

Spring Boot Starter Web을 의존하는 경우, Spring Boot Starter Tomcat에 대한 의존성이 추가되어 결과적으로는 Tomcat Embed 라이브러리 중 Core, EL, Websocket에 대한 의존성이 추가됩니다.

Tomcat Embed Core는 이름 그대로 Tomcat의 핵심 기능을 제공하는 라이브러리입니다.

즉, Tomcat이 WAS의 역할에 충실할 수 있도록 하는 라이브러리입니다.

그렇기 때문에 jakarta 패키지에서는 security와 servlet에 대한 의존성만이 추가되었고, apache 패키지에서는 서블릿 컨테이너인 catalina, TCP 기반 프로토콜을 지원하는 coyote, 로그 기능을 제공하는 juli, JNDI를 통해 외부 리소스를 바인딩할 수 있는 naming, 웹 서버인 tomcat이 포함되어 있습니다.

Tomcat Embed EL은 이름 그대로 EL(Expression Language)에 대한 내용만이 존재합니다.

스프링 부트 버전 2.5부터 스프링에서 사용하는 EL의 구현체를 톰캣의 EL로 통일하기로 결정했기 때문에, 어떠한 WAS를 사용하더라도 무조건 Tomcat Embed EL을 의존하고 있음을 확인할 수 있습니다.
(해당 내용은 Switch to Apache EL implementation by default이라는 스프링 부트 깃허브 레포지토리 이슈에서 확인할 수 있습니다.)

실제 maven repository에서 Spring Boot Starter Netty를 확인해보면 의존성으로 Tomcat EL을 가지고 있는 것을 확인할 수 있습니다.

Tomcat Embed WebSocket 또한 이름 그대로 웹 소켓에 대한 내용만이 존재합니다.

구조

Tomcat 구조에 대해 살펴본 뒤, 실제 사용되는 구현체나 스프링 부트에서 초기화되는 과정에 대해 살펴보겠습니다.

Tomcat

Tomcat의 구조는 위와 같습니다.
하나씩 살펴보도록 하겠습니다.

Server

  • 톰캣의 인스턴스입니다.
  • 톰캣은 자바로 구성된 프로그램이므로 JVM에서 동작합니다.
  • 하나의 Server는 여러 개의 Service를 가질 수 있습니다.
  • 서버와 통신하기 위한 하나의 포트를 가집니다.

Service

  • Connector로부터 들어온 클라이언트 요청을 Engine - Host - Conext로 라우팅하는 역할을 수행합니다.
  • 하나의 Service는 하나의 Engine과 여러 개의 Connector를 가질 수 있습니다.
  • 로그의 편의성을 위해 각 Service에는 이름이 부여됩니다.

Connector

  • 클라이언트의 요청을 받아 Service에게 전달하는 역할을 수행합니다.
  • Connector는 어느 종점(EndPoint)에서 클라이언트의 요청이 받아지는지를 나타냅니다.
  • 서버 상의 포트를 할당합니다.

  • Connector는 Catalina 컴포넌트의 요소이며, 이 때 TCP 프로토콜을 지원받기 위해 Coyote 컴포넌트에 속한 ProtocolHandler를 통해 통신합니다.
    • ProtocolHandler는 환경에 맞는 종점(EndPoint)을 Connector에 지정합니다.
    • 즉, Service가 가지게 되는 여러 Connector는 동일한 타입을 가집니다.
      • Connector가 가지고 있는 ProtocolHandler가 무엇인지에 따라 Connector의 동작, 용도, 종류가 달라지게 됩니다.

Engine

  • 하나의 Engine은 여러 개의 Host와 하나의 Realm을 가질 수 있습니다.
  • 하나의 컨테이너를 의미하며, Catalina 서블릿 엔진을 의미합니다.
  • HTTP 헤더를 통해 클라이언트의 요청이 어느 Host, 어느 Context에 라우팅되어야 하는지 판단합니다.

Realm

  • 사용자 인증과 인가를 담당합니다.
  • 텍스트 파일, 데이터베이스 테이블, LDAP 서버 등을 통해 인증이 가능합니다.
  • Engine 하위 리소스는 하나의 Realm을 공유하므로, 하나의 컨테이너 안의 모든 컨텍스트들은 인증/인가를 위한 리소스를 공유합니다.

Pipeline

  • Pipeline은 여러 개의 Valve를 관리하기 위한 컴포넌트입니다.
  • Engine, Host, Context가 각각 하나의 Pipeline을 가지고 있습니다.
  • Pipeline에 지정된 Valve는 순차적으로 실행됩니다.
    • 특정 Pipeline에 지정된 Valve는 내부적으로 다음 Valve에 대한 참조를 가지고 있으므로 요청과 응답을 전달할 수 있습니다.

Valve

  • Valve는 Engine, Host, Context과 같은 Container의 Pipeline에 위치합니다.
  • 다음과 같은 역할을 수행합니다.
    • 요청 전처리
      • 요청이 컨테이너에 도달하기 전 Valve에서 요청에 대한 전처리 작업을 수행합니다.
    • 요청 라우팅
      • 요청 URL를 분석해 적절한 Container로 요청을 전달합니다.
    • 응답 후처리
  • 전처리/후처리 과정에서 요청/응답 데이터를 조작할 수 있지만, 매우 위험하므로 필요한 경우에만 사용해야 합니다.
    • 주로 다음과 같은 행위는 애플리케이션의 안전성을 위해 지양하는 것을 권장합니다.
      • 파이프라인 처리 흐름을 결정하는데 사용된 속성값 변경
      • 자체적으로 생성한 response를 현재 파이프라인 상의 후속 컴포넌트에 전달
      • request body 접근 시 wrapper를 생성하지 않은 채로 request input stream 핸들링
      • 현재 파이프라인 상에서 생성된 response에 대해 HTTP 헤더 변경
      • 현재 파이프라인 상에서 생성된 response에 대해 response output stream 핸들
  • 요약
    • Valve는 클라이언트의 요청을 Engine의 Connector로부터 받아서 Engine이 Context까지 요청을 라우팅하고, 응답을 반환하는 과정인 요청 처리 파이프라인에서 전/후처리 수행

Logger

  • 컴포넌트의 상태를 외부에 표현하기 위한 Logger 입니다.
  • 각 Container는 하나의 Logger를 가지며, 컴포넌트 계층 구조에 따라 설정을 상속하거나 오버라이딩할 수 있습니다.

Host

  • 하나의 Host는 여러 개의 Context를 가질 수 있습니다.
  • Apache Virtual Host와 유사한 방식으로 동작합니다.
    • 하나의 Engine에서 여러 Host가 존재할 수 있습니다.
    • Host의 이름은 Virtual Host의 이름으로 적용됩니다.
      • Virtual Host의 이름이 URL에 매핑됩니다.
      • 즉, Engine은 URL에 명시된 Virtual Host의 이름을 토대로 요청을 라우팅합니다.

Context

  • 하나의 Web Application을 의미합니다.
    • 내장 톰캣을 사용하지 않는 경우 WAR(Web Application Archive)로 패키징된 애플리케이션을 의미합니다.
    • 내장 톰캣을 사용하는 경우 웹 기반의 ApplicationContext를 의미합니다.

Wrapper

  • 가장 저 수준(Low Level)의 Container 입니다.
  • Servlet을 관리합니다.
    • URI와 Servlet을 매핑합니다.
    • 요청의 전/후처리를 위한 Filter를 관리합니다.
    • Servlet의 Meta data를 조회할 수 있습니다.

구현체

이제 실제 구현체가 무엇인지, 구현체가 어떤 과정을 통해 초기화되고 세팅되는지 확인해보겠습니다.

Tomcat

Embed Tomcat Core에 위치한 org.apache.catalina.startup.Tomcat 입니다.

이는 Embed Tomcat을 실행하기 위한 최소한의 기능을 가진 Tomcat Starter입니다.
가장 큰 차이점은 WEB-INF 디렉토리에 존재하는 web.xml을 사용하지 않고, 스프링 부트 Auto Configuration 과정 중에 추가된 ServletContextIntializer에 의해 설정이 진행된다는 점입니다.

물론 스프링 부트에 resources/WEB-INF/web.xml이 존재하고, 내용에 문제가 있지 않다면 동시에 적용 가능합니다.

동시에 적용할 때 다음과 같은 규칙에 따라 우선 순위가 결정됩니다.

  • Servlet 등록 시
    • ServletRegistrationBean의 우선 순위가 더 높습니다.
    • web.xml에 Servlet 등록 내용이 있다면 무시됩니다.
  • Filter 등록 시
    • FilterRegistartionBean과 web.xml의 Filter 등록 설정이 모두 적용됩니다.
    • 동일한 Filter를 등록한 경우 web.xml의 우선 순위가 더 높습니다.
  • 그 외
    • ServletContextInitializer 구현체들의 우선 순위가 더 높습니다.
    • web.xml에만 특정 설정이 존재한다면 web.xml의 설정이 적용됩니다.

사실상 스프링 부트를 사용하면서 resources/WEB-INF/web.xml을 사용할 일은 없다고 생각하기 때문에, 무시해도 좋을 것 같습니다.

다음으로 Tomcat 인스턴스가 어떤 타이밍에, 어디서 초기화되는지 확인해보겠습니다.

스프링 부트의 ApplicationContext Refresh 과정 중 ServletWebServerApplicationContext.onRefresh()에서 private의 createWebServer()를 호출하게 되면, TomcatServletWebServerFactory.getWebServer()를 호출하게 됩니다.

TomcatServletWebServerFactory.getWebServer()에서는 new Tomcat()을 통해 생성되고 있음을 확인할 수 있습니다.

Server

기본 구현체는 StandardServer 입니다.

TomcatServletWebServerFactory.getWebServer()을 보면 서블릿 생명 주기와 관련된 리스너를 등록하는 과정에서 Tomcat.getServer()를 호출하고 있는 것을 확인할 수 있습니다.

메서드를 타고 들어가다 보면 StandardServer 인스턴스를 생성하고 있는 것을 확인할 수 있습니다.

Service

기본 구현체는 StandardService 입니다.

getServer() 호출 시 StandardServer를 생성한 뒤, StandardService를 생성해 StandardServer에 등록합니다.

Connector

  • 기본 구현체는 Connector 입니다.

TomcatServletWebServerFactory.getWebServer() 호출 시 내부적으로 Connector를 세팅하고 있음을 확인할 수 있습니다.

Connector는 내부적으로 가지고 있는 ProtocolHandler에 의해 용도가 결정되기 때문에 스프링 부트에서 지정한 기본 값을 활용합니다.

TomcatServletWebServerFactory에서는 기본적으로 http11NioProtocol을 ProtocolHandler로 사용합니다.

Engine

기본 구현체는 StandardEngine 입니다.

TomcatServletWebServerFactory.getWebServer() 호출 시 내부적으로 Tomcat.getEngine()을 호출하고 관련된 설정을 세팅합니다.

내부적으로 StandardEngine을 생성하고, Service.setContainer()를 통해 StandardService에 StandardEngine을 세팅합니다.

StandardService.setContainer()의 경우 위와 같이 기존의 Engine을 삭제한 뒤, 지정한 Engine을 새롭게 등록합니다.

Realm

스프링 부트에서 사용하는 Realm은 SimpleRealm 입니다.

Tomcat은 여러 Realm을 가지고 있지만, 사실상 웹 애플리케이션을 구현할 때 인증/인가 과정은 매우 중요한 로직이기 때문에 별도의 아키텍처를 만들거나, 스프링 시큐리티를 커스터마이징해서 사용하는 것이 대부분입니다.

그렇다보니 Tomcat의 Realm은 사용하지 않으므로, Tomcat의 inner class로 선언된 SimpleRealm 만을 사용합니다.

TomcatServletWebServerFactory.getWebServer() 호출 시 내부적으로 Tomcat.getEngine()을 호출하고 관련된 설정을 세팅하게 되는데 getEngine() 내부적으로 createDefaultRealm()을 호출해 SimpleRealm을 Engine에 세팅합니다.

매우 간단하게 구현되어 있습니다.
사실상 의미가 없으니 무시해도 무방하다고 생각합니다.

Pipeline

기본 구현체는 StandardPipeline 입니다.

Tomcat에서 Container라고 취급되는 것들은 모두 ConatinerBase를 확장하게 됩니다.
이에 속하는 것들로는 Engine, Host, Context가 있습니다.

예시로 StandardEngine의 생성자를 보면, Pipeline에 StandardEngineValve를 생성해 기본 Valve로 세팅하고 있음을 확인할 수 있습니다.

ContainerBase를 확인해보면, 기본적으로 StandardPipeline으로 초기화하고 있음을 확인할 수 있습니다.

StandardHost, StandardContext 모두 동일하게 StandardPipeline에 Valve를 세팅하고 있습니다.

Valve

Valve는 Embed Tomcat 기준으로 34개의 구현체를 가지고 있습니다.
전부를 살펴보기에는 너무 많기 때문에 스프링 부트에서 사용하는 것들만 살펴보도록 하겠습니다.

다음과 같은 Valve가 등록됩니다.

  • StandardEngineValve
    • Engine에서 사용할 Pipeline에서 최초로 실행되는 Valve입니다.
    • 클라이언트의 요청을 적절한 Host로 라우팅하는 역할을 수행합니다.
  • StandardHostValve
    • Host에서 사용할 Pipeline에서 최초로 실행되는 Valve입니다.
    • 클라이언트의 요청을 적절한 Context로 라우팅하는 역할을 수행합니다.
  • StandardContextValve
    • Context에서 사용할 Pipeline에서 최초로 실행되는 Valve입니다.
    • 클라이언트의 요청을 적절한 Servlet Container에게 라우팅하는 역할을 수행합니다.
  • RemoteIpValue
    • 요청 헤더(X-Forwarded-For와 같은 헤더)를 통해 프록시나 로드 밸런서로부터 제공된 IP 주소 목록을 사용하여 해당 요청을 보낸 클라이언트 IP 주소와 호스트 이름을 대체합니다.
    • 요청 헤더(X-Forwarded-Proto와 같은 헤더)를 통해 프록시나 로드 밸런서로부터 제공된 스키마(http/https)와 서버 포트를 사용하여 요청의 원래 스키마와 서버 포트를 대체합니다.
  • ErrorReportValue
    • HTTP 상태 코드에 따른 HTML 예외 페이지를 생성합니다.
  • AccessLogValve
    • 웹 서버 Access Log를 생성합니다.
    • 기본 값은 info이며, application.properties/application.yml에서 설정을 적용할 수 있습니다.
  • StanrdardWrapperValve
    • 클라이언트의 요청을 실제 Servlet에 전달하고, 응답을 클라이언트에 전달하는 기능을 제공합니다.

TomcatServletWebServerFactory

자주 언급된 TomcatServletWebServerFactory.getWebServer()에서 세팅되는 Valve는 Engine, Host, Context 내부적으로 기본적인 Valve를 세팅합니다.

각각 StandardEngineValve, StandardHostValve, StandardContextValve를 Pipeline의 기본 Valve로 세팅합니다.


TomcatServletWebServerFactory.getWebServer()에서 수행하는 configureEngine()은 Valve를 세팅합니다.

디버깅으로 확인해보면 아무런 Valve도 없다는 것을 확인할 수 있습니다.

해당 메서드는 개발자가 직접 커스터마이징한 Vavle나 resources/WEB-INF/web.xml에 등록된 Valve를 등록하는 용도이기 때문입니다.

사실상 web.xml이나 Valve를 커스터마이징할 일은 없기 때문에 크게 신경쓰지 않아도 된다고 생각합니다.

TomcatWebServerFactoryCustomizer

TomcatServletWebServerFactory이 Tomcat을 생성한 뒤 TomcatWebServer를 반환한 이후, TomcatWebServerFactoryCustomizer에서 추가적인 설정을 수행합니다.

customizeRemoteIpValue()에서는 RemoteIpValue를 초기화합니다.

customizeErrorReportValve()에서는 ErrorReportValue를 초기화합니다.

application.properties 혹은 application.yml에서 명시한 로그 관련 설정을 기반으로 AccessLogValve를 초기화합니다.

Logger

기본 구현체는 DirectJDKLog 입니다.

Engine, Host, Context 모두 ContainerBase를 상속하고 있으며, ContainerBase는 필드로 org.apache.juli.logging.Log를 필드로 가지고 있습니다.

ContainerBase 타입에서 생성되는 Log의 경우, DirectJDKLog.getInstnace()로 초기화됩니다.

하지만 스프링 부트가 사용하는 기본적인 Log는 Slf4j의 구현체인 Logback을 기본적으로 사용하기 때문에 jul-to-slf4j에 의해 Slf4j Bridge로 라우팅됩니다.

실제로 LogbackLoggingSystem에서 Tomcat Juli 관련 설정 대신 SLF4JBridgeHandler를 세팅하고 있음을 확인할 수 있습니다.

즉, Embed Tomcat의 Log는 스프링 부트에서 기본적으로 사용하지 않습니다.

Host

기본 구현체는 StandardHost 입니다.

TomcatServletWebServerFactory.getWebServer()에서 auto deploy 설정을 false로 세팅하기 위해 Tomcat.getHost()를 호출합니다.
(auto deploy는 자동 배포 설정으로, Context를 추가/삭제하거나 Context에 등록된 웹 애플리케이션의 코드를 수정할 경우 재배포하는 기능입니다.)

Tomcat.getHost()에서는 Engine이 가지고 있는 Host가 없는 경우 StandardHost를 초기화합니다.

Context

기본 구현체는 StandardContext 입니다.

스프링 부트에서 사용하는 Context는 StandardContext를 확장한 TomcatEmbeddedContext 입니다.

TomcatServletWebServerFactory.getWebServer()에서 prepareContext()를 호출합니다.

prepareContext() 내부적으로 TomcatEmbeddedContext를 생성하고 있음을 확인할 수 있습니다.

Wrapper

기본 구현체는 StandardWrapper 입니다.

addServlet()에서는 TomcatEmbeddedContext.createWrapper()를 호출합니다.

DispatcherServlet 등록 과정

TomcatEmbeddedContext 내에 스프링 부트 애플리케이션의 FrontController인 DispatcherServlet이 어떤 과정을 통해 등록되는 지 확인해보겠습니다.

TomcatServletWebServerFactory.prepareContext() 하단부에서 configureContext()를 호출하는 것을 확인할 수 있습니다.

configureContext()는 내부적으로 TomcatStarter를 생성하고, 이를 TomcatEmbeddedContext.setStarter()를 통해 세팅합니다.

이렇게 세팅한 TomcatStarter는 StandardContext.startInternal()에 의해 호출됩니다.

TomcatStarter.onStartup()은 ServletContextInitializer를 모두 순회하며 필요한 설정을 세팅합니다.

DispatcherServlet은 ServletWebServerApplicationContext에서 등록됩니다.

ServletWebServerApplicationContext에서 DispatcherServletRegistrationBean으로 인해 DispatcherServlet이 세팅됩니다.

DispatcherServletRegistrationBean은 초기화 시 DispatcherServletAutoConfiguration.DispatcherServletRegistrationConfiguration에 의해 DispatcherServlet을 주입 받습니다.

DispatcherServletRegistrationBean.addRegistration()을 통해 org.apache.catalina.core.ApplicationContextFacade에 Servlet을 추가합니다.

org.apache.catalina.core.ApplicationContextFacade는 org.apache.catalina.core.ApplicationContext에 Servlet을 추가합니다.

org.apache.catalina.core.ApplicationContext.addServlet()은 오버로딩이 적용되어 있으며 결과적으로는 private 접근 제어자의 addServlet()을 호출하게 됩니다.

addServlet()에서는 TomcatEmbeddedContext.createWrapper()를 호출합니다.

StandardWrapper를 생성하고, 생성자에서는 실제 클라이언트의 요청을 Servlet으로 전달하는 StandardWrapperValve를 생성합니다.

요청 처리 과정

전처리

Embed Tomcat에서 클라이언트의 요청을 받기 위해 이전에 살펴본 구성 요소들의 초기화 과정이 필요합니다.

초기화 과정을 위해 Tomcat은 Tomcat.start()를 호출합니다.

이를 위해 Tomcat의 생명 주기(LifecycleBase)를 시작하고, Connector, Container를 초기화하며, HostConfig 등의 설정을 세팅합니다.

Tomcat.start()에서 많은 작업을 수행하므로 요청 처리 과정에서 필수적으로 사용되는 ContextVersion에 대해서만 살펴보도록 하겠습니다.

위와 같이 TomcatServletWebServerFactory부터 시작해 MapperListner를 거쳐 Mapper에 도달하게 됩니다.

MapperListener에서 Host와 Context 등의 과정을 거치고, 요청을 라우팅하기 위해 필요한 ContextVersion를 생성합니다.

ContextVersion은 Mapper의 static inner class로, MapElement를 상속한 클래스입니다.

필드에서 확인할 수 있듯이, path나 WebResourceRoot, Context 등 특정 요청과 Context를 매핑하는 용도입니다.

MapperListener.registerContext()의 경우 지정한 Context, 경로, Host, WebResourceRoot 등 필요한 내용을 파라미터로 전달합니다.

Tomcat.start() 호출 시 초기화가 되지 않았으므로 MappedContext는 존재하지 않습니다.
그러므로 ContextVersion, MappedContext를 생성하고 등록하게 됩니다.

이 과정은 Tomcat.start() 호출 시 한 번만 동작하게 됩니다.

요청 메세지 파싱

이 과정부터는 요청이 올 때 마다 매번 동작하는 과정입니다.

Tomcat 스레드 풀에서 Worker 스레드가 동작하게 된 이후, 위와 같은 과정을 통해 CoyoteAdapter에 요청이 전달됩니다.

CoyoteAdapter는 Connector와 Container 사이의 통신을 담당합니다.
Connector에서 전달된 클라이언트 요청을 Container가 이해할 수 있는 Request로 변환하고, 변환한 Request를 적절한 Container에게 전달합니다.

매우 중요한 역할을 수행하기 때문에, CoyoteAdapter부터 살펴보도록 하겠습니다.

CoyoteAdapter.service()를 확인해보면 Request를 생성하고 초기화하는 것을 확인할 수 있습니다.

이후 파싱 작업을 수행하는 postParesRequest()를 호출합니다.

postParseRequest() 메서드는 요청 URI 파싱, 매핑 데이터 설정, 인코딩 설정, 세션 관리, 요청 속성 설정, 프로토콜 설정, 비동기 지원 설정 등 다양한 작업을 수행합니다.

여기서는 클라이언트의 요청을 라우팅하기 위한 과정만 확인하겠습니다.

중간에 Connector에서 Mapper를 가져와 map을 수행하는 것을 확인할 수 있습니다.
이 때 요청에 세팅된 MappingData 객체를 전달하게 됩니다.

MappingData는 Context와 요청을 매핑하기 위해 필요한 데이터들을 관리하는 용도입니다.

Mapper.map()은 internalMap()을 호출합니다.
해당 메서드는 MappingData가 가지고 있는 Host, Context 등을 초기화하게 됩니다.
Context의 경우 이전 초기화 과정에서 등록한 ContextVersion을 활용해 세팅하게 됩니다.

Request 라우팅

CoyoteAdapter.service()에서 postParseRequest()를 통해 클라이언트 요청을 파싱해 Request를 초기화한 이후, Pipeline의 Valve를 통해 Request를 라우팅하게 됩니다.

postParseRequest()가 성공했다면 Connector -> Service -> Engine의 Pipeline의 첫 번째 Valve를 가져와 invoke()를 호출합니다.

즉, StandardEngineValve부터 시작해 다음과 같은 Valve를 거치게 됩니다.

  • StandardEngineValve
  • ErrorReportValve
  • StandardHostValve
  • AuthenticatorBase
  • StandardContextValve
  • StandardWrapperValve

간단하게 하나씩 살펴보도록 하겠습니다.

StandardEngineValve

StandardEngineValve는 비동기 처리 지원 여부를 체크합니다.

Engine에서 Host의 Pipeline을 조회하기 위해 Request 객체에서 Host를 꺼내고, 그 Host의 Pipeline에서 첫 번째 Valve를 꺼내는 것을 확인할 수 있습니다.

ErrorReportValve

ErrorReportValve는 예외/에러가 발생한 경우 예외 처리 및 예외 페이지를 렌더링하는 기능을 제공합니다.

이 때의 예외 페이지는 스프링 부트에서 예외 처리를 하지 않은 예외 발생 시, 브라우저에서 확인할 수 있는 그 예외 페이지입니다.

예외 처리 및 예외 페이지 렌더링을 하다 보니 우선 다음 Vavle.invoke()를 호출합니다.

invoke()의 report()를 타고 들어가다 보면 findErrorPage() 메서드가 호출되고, 이를 통해 예외 페이지를 찾아서 렌더링하게 됩니다.

StandardHostValve

HTTP 요청/응답을 처리하는 Host Valve 입니다.
Context를 매핑하고 Context의 Pipeline을 호출합니다.

Host에서 Context를 조회하기 위해 Request에서 이를 조회하고, Context를 매핑합니다.

이 과정까지 에러/예외가 발생하지 않았다면 Context의 Pipeline을 가져와 첫 번째 Valve.invoke()를 호출합니다.

AuthenticatorBase

AuthenticatorBase는 Tomcat의 인증 과정을 처리하는 Valve입니다.
인증/인가 처리를 하지 않으므로, 인증 없이 모든 요청을 허용하는 Authenticator인 NonLoginAuthenticator가 무조건 동작하게 됩니다.

사실상 Tomcat에서 인증/인가 처리를 하지 않으므로 무조건 위 코드를 통해 다음 Valve를 호출하게 됩니다.

StandardHostValve에서 NonLoginAuthenticator를 호출하고 있음을 확인할 수 있습니다.

StandardContextValve

StandardContextValve는 Request URI를 분석해 알맞은 Wrapper에게 전달하는 역할을 수행합니다.

Resource Directory 검증, Request Wrapper 검증, 응답에 ACK 설정을 수행한 뒤 Wrapper에 세팅된 Pipeline을 조회해 첫 번째 Valve.invoke()를 호출합니다.

StandardWrapperValve

StandardWrapperValve는 FilterChain을 통해 요청의 전/후처리를 수행하도록 요청을 전달합니다.

ApplicationFilterChain을 생성한 뒤, doFilter()를 호출합니다.

ApplicationFilterChain

ApplicationFilterChain은 요청의 전/후처리를 수행하는, 등록된 모든 Filter를 순회한 뒤 Servlet을 실제로 호출합니다.

FilterChain에 등록된 Filter를 순회하면서 doFilter()를 호출합니다.

이후 Servlet.service()를 호출합니다.

Embed Tomcat 설정 과정

스프링 부트에서 application.properties/application.yml을 통해 Embed Tomcat과 관련된 설정을 진행할 수 있습니다.

# Tomcat 
server:
  tomcat:
    accept-count: 5
    max-connections: 150
    threads:
      max: 50
      min-spare: 20

이러한 설정은 ServerProperties와 매핑됩니다.

ServerProperties$Tomcat 클래스에 Tomcat 관련 설정 값들에 대한 필드가 존재하는 것을 확인할 수 있습니다.

이러한 ServerProperties는 TomcatWebServerFactoryCustomizer에 의해 호출되어 설정한 값들이 적용됩니다.

profile
안녕하세요

0개의 댓글