Tomcat & Spring Bootstrapping Sequence — 3편 HTTP

Spring Framework를 이용해 다음과 같은 Controller를 생성했습니다.

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class TestController {
@ResponseBody
@RequestMapping("/api/v1/test")
public String test() {
return "HelloWorld";
}
}

기본설정으로 톰캣을 구동해보겠습니다.
이제 브라우저에서 http://127.0.0.1:8080/api/v1/test로 해당 Controller를 호출 할 수 있습니다.
Screen Shot 2015-08-23 at 7.13.14 PM
어떤 과정을 통해 브라우저에 Hello World가 표시될까요?
1편, 2편에 이야기했던 초기화 프로세스를 다시 살펴보면  Server -> Service -> Engine -> Host -> Context 순이었습니다.
그 중 Service (StandardService) 과정에서 Http 요청을 받을 Connector를 생성하는데, 이 과정에 대해서 좀더 자세히 살펴보겠습니다.
public void addConnector(Connector connector) {
synchronized (connectorsLock) {
connector.setService(this);
Connector results[] = new Connector[connectors.length + 1];
System.arraycopy(connectors, 0, results, 0, connectors.length);
results[connectors.length] = connector;
connectors = results;
if (getState().isAvailable()) {
try {
connector.start();
} catch (LifecycleException e) {
log.error(sm.getString(
"standardService.connector.startFailed",
connector), e);
}
}
// Report this property change to interested listeners
support.firePropertyChange("connector", null, connector);
}
}
server.xml에 선언한 Connector가 이과정에서 생성 된 후 StandardService에 추가 됩니다.
server.xml의 기본값을 살펴보겠습니다 ( Tomcat 8.0 기준 )
<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443" />
<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />
2개의 Connector가 있습니다. 둘 중 우리가 살펴볼 Connector는 HTTP/1.1 Connector 입니다.
HTTP/1.1 Connector가 어떻게 생성되는지 살펴보겠습니다.
public Connector(String protocol) {
setProtocol(protocol);
// Instantiate protocol handler
ProtocolHandler p = null;
try {
Class clazz = Class.forName(protocolHandlerClassName);
p = (ProtocolHandler) clazz.newInstance();
} catch (Exception e) {
log.error(sm.getString(
"coyoteConnector.protocolHandlerInstantiationFailed"), e);
} finally {
this.protocolHandler = p;
}
if (!Globals.STRICT_SERVLET_COMPLIANCE) {
URIEncoding = "UTF-8";
URIEncodingLower = URIEncoding.toLowerCase(Locale.ENGLISH);
}
}

protocol을 넘겨받는데, 이때 넘어오는 protocol이 HTTP/1.1 입니다.
public void setProtocol(String protocol) {
if (AprLifecycleListener.isAprAvailable()) {
if ("HTTP/1.1".equals(protocol)) {
setProtocolHandlerClassName
("org.apache.coyote.http11.Http11AprProtocol");
} else if ("AJP/1.3".equals(protocol)) {
setProtocolHandlerClassName
("org.apache.coyote.ajp.AjpAprProtocol");
} else if (protocol != null) {
setProtocolHandlerClassName(protocol);
} else {
setProtocolHandlerClassName
("org.apache.coyote.http11.Http11AprProtocol");
}
} else {
if ("HTTP/1.1".equals(protocol)) {
setProtocolHandlerClassName
("org.apache.coyote.http11.Http11NioProtocol");
} else if ("AJP/1.3".equals(protocol)) {
setProtocolHandlerClassName
("org.apache.coyote.ajp.AjpNioProtocol");
} else if (protocol != null) {
setProtocolHandlerClassName(protocol);
}
}
}
HTTP/1.1일 경우 기본적으로 Http11NioProtocol이 생성되도록 되어있습니다. Tomcat 8부터HTTP Connector로 NIO(Nonblocking IO) Protocol을 사용하도록 변경되었다네요. Tomcat 7까지는 BIO (Blocking IO)로 기본값이 설정되어 있었습니다.
참고 : http://techblog.bozho.net/tomcats-default-connectors
Tomcat7의 setProtocol 메서드
public void setProtocol(String protocol) {
if (AprLifecycleListener.isAprAvailable()) {
if ("HTTP/1.1".equals(protocol)) {
setProtocolHandlerClassName
("org.apache.coyote.http11.Http11AprProtocol");
} else if ("AJP/1.3".equals(protocol)) {
setProtocolHandlerClassName
("org.apache.coyote.ajp.AjpAprProtocol");
} else if (protocol != null) {
setProtocolHandlerClassName(protocol);
} else {
setProtocolHandlerClassName
("org.apache.coyote.http11.Http11AprProtocol");
}
} else {
if ("HTTP/1.1".equals(protocol)) {
setProtocolHandlerClassName
("org.apache.coyote.http11.Http11Protocol");
} else if ("AJP/1.3".equals(protocol)) {
setProtocolHandlerClassName
("org.apache.coyote.ajp.AjpProtocol");
} else if (protocol != null) {
setProtocolHandlerClassName(protocol);
}
}
}
Http11NioProtocol을 살펴보면, EndPoint로 NioEndPoint를 생성하고, Processor로는 Http11NioProcessor를 생성합니다.
public Http11NioProtocol() {
endpoint=new NioEndpoint();
cHandler = new Http11ConnectionHandler(this);
((NioEndpoint) endpoint).setHandler(cHandler);
setSoLinger(Constants.DEFAULT_CONNECTION_LINGER);
setSoTimeout(Constants.DEFAULT_CONNECTION_TIMEOUT);
setTcpNoDelay(Constants.DEFAULT_TCP_NO_DELAY);
}
public Http11NioProcessor createProcessor() {
Http11NioProcessor processor = new Http11NioProcessor(
proto.getMaxHttpHeaderSize(), (NioEndpoint)proto.endpoint,
proto.getMaxTrailerSize(), proto.getAllowedTrailerHeadersAsSet(),
proto.getMaxExtensionSize(), proto.getMaxSwallowSize());
proto.configureProcessor(processor);
register(processor);
return processor;
}
이과정을 통해서 NIO 기반의 Server Socket을 생성하고, NIO Event를 통해 Http Call을 처리 합니다.
NioEndPoint -> URL Mapped Servlet 까지 어떻게 찾아가는지 대력적으로 살펴볼 포인트들을 정리했습니다.
  • NioEndpoint processSocket — 614 Line ( 관련 추상클래스 : AbstractProtocol process — 668 Line / AbstractHttp11Processor process — 972 Line )
  • NioEndpoint -> Socket Processor doRun — 1478 Line
  • NioEndPoint -> Socket Processor doRun — 1521 Line
  • CoyoteAdapter service — 472 Line
  • StandardEngineValve invoke — 71 Line
  • AccessLogValve & ErrorLogReportValve
  • StandardHostValve invoke — 107 Line
  • AuthenticatorBase invoke — 502 Line
  • StandardContextValve invoke 72 Line
  • StandardWrapperValve invoke 94 Line -> filterChain.doFilter
  • ApplicationFilterChain service 291 Line
위 과정들을 거쳐 NioEndPoint에서 ApplicationFilterChain 까지 찾아 오게 됩니다.
ApplicationFilterChain service()에 도착하면 요청한 /api/v1/test URL을 처리할 servlet의 실제 service() 메소드를 호출 하게 됩니다.  (SpringFramework DispatcherServlet)
Screen Shot 2015-08-23 at 11.57.21 PM
DispacherServlet은 호출한 URL을 보고, 실제 URL Mapping된 Controller를 찾아서 Request를 전달하는 역할을 합니다.  간단하게 생각하면 SpringFramework으로 들어오는 모든 Request / Response EndPoint로 볼수 있습니다. ( 많은 SpingFramework 서적과 웹 문서에서 DispatcherServlet에 대해서 설명하고 있습니다. 상당히 많은 역할이 있는 Servlet으로 자세한 내용은 웹 문서들을 참고 하세요! - http://egloos.zum.com/springmvc/v/504151 )
그렇다면,  Tomcat은 어떻게 ApplicationFilterChain service() 메소드에서, Spring Framework의 DispatcherServlet을 찾았을까요?
StandardWrapperValve를 살펴보면
if (!unavailable) {
servlet = wrapper.allocate();
}
이과정에서 ApplicationFilterChain이 호출할 Servlet을 결정하게 되는데, 해당 Servlet은 Spring Framework가 초기화 되는 과정에서 미리 DispatcherServlet 을 등록해 놓았기 때문입니다.
Spring Framework에서 ServletRegistrationBean을 통해 다음과 같은 순서로 DispatcherServlet이 톰캣에 등록됩니다.
  • (Spring) ServletRegistrationBean onStartup — 189 Line — UrlMapping & DispatcherServlet Setting
  • (Tomcat) ApplicationContextFacade addServlet — 522 Line
  • (Tomcat) ApplicationContext addServlet — 1023 Line
  • (Tomcat) ApplicationContext addServlet — 1087 Line
또, 한가지 궁금한점은,
톰캣은 여러 ContextPath를 지원하는데, 내가 요청한 URL이 어떤 Context Path에 등록된 Servlet인지 어떻게 알아낼까요?
소스코드를 뒤져보니, AuthenticatorBase.java의 488 Line에 해답이 있었습니다.
Wrapper wrapper = request.getMappingData().wrapper;
Wrapper에서 Servlet을 결정한다면, AuthenticatorBase에서는 URL을 처리 할 수 있는 해당 Wrapper를 결정하고 있었습니다.
1편, 2편, 3편을 통해
  • 톰캣이 어떻게 시작되는지
  • 톰캣이 Spring Framework를 어떻게 찾는지
  • 톰캣이 내가 만든 Spring Framework Controller를 어떻게 찾는지
간략하게 살펴보았습니다.
좀더 깊이 있는 내용들은 Tomcat, Spring Framework 의 소스코드와 레퍼런스들을 꼭 살펴보면 더 많은 도움이 됩니다.