톰캣 직접 사용

톰캣을 직접 생성하여 서블릿을 등록해보자.

package jch.inflearn;

import org.apache.catalina.Context;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.startup.Tomcat;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

public class Application {

    public static void main(String[] args) throws LifecycleException {
        Tomcat tomcat = new Tomcat();
        tomcat.setPort(8080);

        Context context = tomcat.addContext("/", "/");

        HttpServlet servlet = new HttpServlet() {
            @Override
            protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
                PrintWriter writer = resp.getWriter();
                writer.println("<html></head><title>");
                writer.println("Hey Tomcat");
                writer.println("</title></head>");
                writer.println("<body><h1>Hello Tomcat</h1></body>");
                writer.println("</html>");
            }
        };
        String servletName = "helloServlet";
        tomcat.addServlet("/", servletName, servlet);
        context.addServletMappingDecoded("/hello", servletName);

        tomcat.start();
        tomcat.getServer().await();
    }
}
  • 17 ~ 20: 톰캣을 생성하여 포트를 8080로 설정하면서 contextPath와 docBase를 지정한다.
  • 22 ~ 32: 요청을 받을 서블릿을 생성한 뒤, HTTP 요청을 수행할 메소드를 구현한다. 예제에서는 get요청을 구현하여 HTML을 응답하는 코드를 작성했다.
  • 33 ~ 35: 해당 서블릿을 톰캣에 등록하면서 URL을 매핑한다.
  • 37 ~ 38: 애플리케이션을 실행한 뒤, localhost:8080/hello을 요청하면 HTML이 반환될 것이다.

서블릿 웹 서버 생성

스프링부트의 내장 톰캣은 어떻게 동작하는 것일까? 지난 강의에서 등장한 자동설정과 관련이 있다. 자동설정을 통해 톰캣이랑 서블릿이 자동설정으로 설정된다. 먼저 서블릿 웹 서버(서블릿 컨테이너)를 생성하는 org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration 클래스를 살펴보자.

@Configuration
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@ConditionalOnClass(ServletRequest.class)
@ConditionalOnWebApplication(type = Type.SERVLET)
@EnableConfigurationProperties(ServerProperties.class)
@Import({ ServletWebServerFactoryAutoConfiguration.BeanPostProcessorsRegistrar.class,
		ServletWebServerFactoryConfiguration.EmbeddedTomcat.class,
		ServletWebServerFactoryConfiguration.EmbeddedJetty.class,
		ServletWebServerFactoryConfiguration.EmbeddedUndertow.class })
public class ServletWebServerFactoryAutoConfiguration {
    // ...
}
  • 3: ServletRequest라는 클래스가 클래스패스에 있으면 자동설정으로 사용한다.
  • 6~9: 다른 설정파일을 참조하여 사용한다

@Import 어노테이션을 이용해 다른 설정파일들을 참조하는데, tomcat, jetty와 undetrow에 대한 설정을 참조하는 것을 알 수 있다. 다음으로 EmbeddedTomcat 클래스를 살펴보자.

@Configuration
@ConditionalOnClass({ Servlet.class, Tomcat.class, UpgradeProtocol.class })
@ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT)
public static class EmbeddedTomcat {

    @Bean
    public TomcatServletWebServerFactory tomcatServletWebServerFactory() {
        return new TomcatServletWebServerFactory();
    }

}

ConditionalOnClass에있는 클래스를 빈으로 등록하지 않았기 때문에 조건을 만족하면서 EmbeddedTomcat을 통해 TomcatServletWebServerFactory를 빈으로 등록하게 된다. 마지막으로 서버를 커스터마이징하는 TomcatServletWebServerFactory를 들여다보자.

@Override
public WebServer getWebServer(ServletContextInitializer... initializers) {
    Tomcat tomcat = new Tomcat();
    File baseDir = (this.baseDirectory != null) ? this.baseDirectory
        : createTempDir("tomcat");
    tomcat.setBaseDir(baseDir.getAbsolutePath());
    Connector connector = new Connector(this.protocol);
    tomcat.getService().addConnector(connector);
    customizeConnector(connector);
    tomcat.setConnector(connector);
    tomcat.getHost().setAutoDeploy(false);
    configureEngine(tomcat.getEngine());
    for (Connector additionalConnector : this.additionalTomcatConnectors) {
        tomcat.getService().addConnector(additionalConnector);
    }
    prepareContext(tomcat.getHost(), initializers);
    return getTomcatWebServer(tomcat);
}

해당 팩토리 클래스에는 getWebServer라는 메소드가 있다. getWebServer메소드에서 톰캣을 생성하고 초기작업을 수행한 뒤 반환한다. 우리가 스프링부트 애플리케이션을 실행할 때, 웹 애플리케이션을 실행하는 경우 이러한 자동설정 과정을 거쳐서 등록된 내장 웹 서버를 사용하게 된다.

내장 서버에서 알 수 있듯이, 스프링부트의 자동 설정은 복잡한 설정 과정을 보다 상세하고 유연하게 설정할 수 있도록 도와준다.

서블릿 기반의 MVC

우리의 애플리케이션이 서블릿 기반의 MVC라면 DispatcherServlet이 필요하다. DispatcherServlet 또한 웹서버와 마찬가지로 자동설정으로 등록된다. org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration를 살펴보자.

@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@Configuration
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass(DispatcherServlet.class)
@AutoConfigureAfter(ServletWebServerFactoryAutoConfiguration.class)
@EnableConfigurationProperties(ServerProperties.class)
public class DispatcherServletAutoConfiguration {
    // ...
    @Bean(name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
    public DispatcherServlet dispatcherServlet() {
        DispatcherServlet dispatcherServlet = new DispatcherServlet();
        dispatcherServlet.setDispatchOptionsRequest(
            this.webMvcProperties.isDispatchOptionsRequest());
        dispatcherServlet.setDispatchTraceRequest(
            this.webMvcProperties.isDispatchTraceRequest());
        dispatcherServlet.setThrowExceptionIfNoHandlerFound(
            this.webMvcProperties.isThrowExceptionIfNoHandlerFound());
        return dispatcherServlet;
    }
}

dispatcherServlet() 메소드에서 DispatcherServlet을 빈으로 등록한다. DispatcherServlet은 HttpServlet을 상속해서 만든 스프링 MVC의 핵심 클래스이다.

참고

서블릿 컨테이너를 만드는일과 DispatcherServlet을 만들어서 등록하는 일이 분리되어있다. 서블릿 컨테이너는 우리의 설정에 따라 얼마든지 바뀔 수 있지만, 서블릿은 변하지 않는다. 서블릿 컨테이너의 종류에 상관없이 서블릿을 등록하기 때문에 분리되어있다는 정도로만 이해해도 좋다.

해당 포스팅은 스프링 부트 개념과 활용 강의 내용을 토대로 작성하였습니다.