[WEB] Servlet에 대한 이해

웹 어플리케이션의 기본이라고 할 수 있는 서블릿에 대해서 다시 정리해보았다. 오래된 개념이면서 아직 활발하게 사용되고 있는 기술이기 때문에 개발자들 마다 다른 방식으로 구현하고 있으나 크게 그 의미를 신경쓰지 않는다. 그래서 처음이라고 생각하고 다시 책을 뒤적이며 정리해보았다.


서블릿Servlet에 대한 이해

서블릿의 기본은 브라우저에서 사용자 request을 받아 처리한 후, 동적 데이터가 채워진 결과를 response하기 위한 내기 위한 Java 프로그램이다. 이를 위해서 서블릿은 자신이 처리하고자 하는 URL을 정의하고, 요청 타입(GET, POST, DELETE 등)에 따른 동작을 정의하게 된다.

웹 어플리케이션에서 Web Server는 정적엔 데이터를 전달하기 위해서 사용되고, 추가적으로 동적인 HTML 페이지를 만들기 위해서는 WAS(Web Application Server)를 통해 처리해야 한다. WAS의 역할은 서블릿을 사용하여 요청을 처리하고 결과를 받아서 동적으로 HTML을 받아오는 것으로, WAS가 전달받은 요청에 대한 처리를 하기 위해서는 어떤 서블릿을 사용해야 하는지에 대한 정보 및 설정값을 알고 있어야 한다. 이를 설정하는 방식은 크게 두 가지 이다.

  • web.xml 정의
  • Java code를 사용 (servlet 3.0 이상)

Web.xml 을 사용한 서블릿 정의

가장 일반적이고 전통적인 방법으로 web.xml을 통해서 WAS가 서블릿에 대한 정보를 전달 받을 수 있다. web.xml은 웹 어플리케이션의 context root에 위치한 WEB-INF 폴더 아래에 존재하여, WAS가 시작될 때에 메모리에 로딩된다.

<WEB-APP> 
    <servlet> 
        <servlet-name>hello-servlet</servlet-name>
        <servlet-class>com.mypackage.HelloServlet</servlet-class>
    </servlet>
    
    <servlet-mapping>
        <servlet-name>hello-servlet</servlet-name>
        <url-pattern>/hello</url-pattern>
    </servlet-mapping>
</WEB-APP>

위에서는 com.mypackage.HelloServlet 이라는 서블릿 클래스를 선언하고, 해당 서블릿이 /hello 하위의 모든 요청을 전달받아 처리할 수 있도록 정의하였다. /hello/getUserList 같은 url로 요청이 들어온다면, 우선적으로 HelloServlet에게 전달될 것이다.

DispatcherServlet 정의

Spring MVC기반의 웹 어플리케이션을 만드는 경우 Spring framework에서 제공하는 DispatcherServlet을 사용할 수 있다. 이를 통해서 Spring IoC등의 기능을 활용할 수 있으며, 들어오는 URL에 대해서 자동으로 적절한 Controller에 요청을 전달해준다. 만약 Spring의 DispatcherServlet을 사용하지 않는다면 모든 요청 url에 대한 서블릿을 각각 만들어줘야 할 것이다.

WebServlet 사용 예

서블릿 구현

@WebServlet
public class UserServlet extends HttpServlet {
    private static final long serialVersionUID = 1L; 
    
    protected void doGet(HttpServletRequest request, HttpservletResponse response) throws ServletException, IOException{
        //.. 생략 .. 
    }    
    
    protected void doPost(HttpServletRequest request, HttpservletResponse response) throws ServletException, IOException{
        //.. 생략 .. 
    }
}

@WebServlet
public class ItemServlet extends HttpServlet {
    private static final long serialVersionUID = 1L; 
        protected void doGet(HttpServletRequest request, HttpservletResponse response) throws ServletException, IOException{
        //.. 생략 .. 
    }    
    
    protected void doPost(HttpServletRequest request, HttpservletResponse response) throws ServletException, IOException{
        //.. 생략 .. 
    }
}

web.xml 설정

<WEB-APP> 
    <servlet> 
        <servlet-name>userServlet</servlet-name>
        <servlet-class>com.mypackage.UserServlet</servlet-class>
    </servlet>
    <servlet> 
        <servlet-name>itemServlet</servlet-name>
        <servlet-class>com.mypackage.ItemServlet</servlet-class>
    </servlet>
    
    <servlet-mapping>
        <servlet-name>userServlet</servlet-name>
        <url-pattern>/user</url-pattern>
    </servlet-mapping>
    <servlet-mapping>
        <servlet-name>itemServlet</servlet-name>
        <url-pattern>/item</url-pattern>
    </servlet-mapping>
    <!-- 서블릿을 추가할 때 마다 web.xml 에 추가 설정해야 한다-->
    <!-- Java code 기반으로(annotation 활용)하여 처리하는 방법도 존재 -->
</WEB-APP>

DispatcherServlet 사용 예

서블릿(RequestMapping) 구현


@Contrller
@RequestMapping("/user")
public class UserController {
    @RequestMapping(value="/", method=RequestMethod.GET)
    public void doPost(){}
    
    @RequestMapping(value="/", method=RequestMethod.POST)
    public void doPost(){}
}

@Contrller
@RequestMapping("/item")
public class ItemController {
    @RequestMapping(value="/", method=RequestMethod.GET)
    public void doGet(){}
    
    @RequestMapping(value="/", method=RequestMethod.POST)
    public void doPost(){}
}

web.xml 설정

<WEB-APP> 
    <servlet> 
        <servlet-name>dispatcherServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>dispatcherServlet</servlet-name>
        <url-pattern>/</url-pattern>
        <!-- context root를 별도로 설정할 수 있지만, 이 예제에서는 root(/)로 설정하는 default servlet을 사용한다 -->
    </servlet-mapping>
</WEB-APP>

두 방법을 비교하였을 때 두드러지게 다른 점은 Controller를 사용하는 경우 java code 상에서 좀 더 유연하게 URL 요청에 대한 맵핑을 할 수 있다. 또한 기능이 추가 될 때 기존 코드 변경을 최소화하여 변경할 수 있다는 장점이 있다. DispatcherServlet의 동작 메커니즘은 다음과 같다.

DispatcherServlet <그림-1. Dispatcher Servlet 동작 매커니즘>

DispatcherServlet을 사용하는 경우 요청을 Controller를 통해 처리하기 때문에 정적 리소스를 요청하는 url 같은 경우에도 불필요하게 Controller에게 전달된다. 그렇기 때문에 아래와 같은 방법을 선택하여 수행해야 한다.

  1. 정적 자원에 대한 요청과 웹 어플리케이션 요청을 분리(ex. url pattern을 구분)하여 처리. 코드가 지저분해질 수 있음.
  2. 순차적으로 웹 어플리케이션 요청을 먼저 탐색 후 없는 경우 정적 리소스를 검색하는 방식으로 구현. 요청에 맞는 controller를 찾지 못하는 경우 정적 리소스를 찾음. 논리적으로 간단하고 확장성도 좋음

WebApplicationInitializer를 사용한 서블릿정의

Servlet 3.0 부터 web.xml 파일을 사용하는 대신 java code를 사용하여 서블릿을 설정할 수 있게 되었다. 이에따라 Spring에서도 DispatcherServlet을 java code 기반으로 정의하였다.

public class SpringServletConfig implements WebApplicationInitializer{
    
    @Override
    public void onStartup(ServletContext servletContext servletContext) throw ServletException{
        XmlWebApplicatinoContext servletAppContext = new XmlWebApplicationContext();
        servletAppContext.setConfigLocation("/WEB-INF/dispatcher.xml");
        
        DispatcherServlet dispatcherServlet = new DispatcherServlet(servletAppContext);
        ServletRegistration.Dynamic registration = servletContext.addSErvlet("dispatcher", dispatcherServlet);
        registration.setLoadOnStart(1);
        registration.addMapping("*.do");
    }
    // .. 생략 .. 
}

XML 기반의 설정

XML 기반으로 Spring context 을 설정하는 경우에는 아래와 같이 AbstractDispatcherServletInitializer 을 상속받아 구현한다

public class SpringServletConfig extends AbstractDispatcherServletInitializer{
    @Override    protected WebApplicationContext createServletApplicationContext(){/* return servletAppContext; */}
    @Override    protected String getServletName(){/* return servletName; */}
    @Override    protected String[] getServletMappings(){/* return servletMappings; */}
    @Override    protected boolean isAsyncSupported(){/* return bool value */}
    @Override    
    protected WebApplicationContext createRootApplicationContext(){
        XmlWebApplicatinoContext rootAppContext = new XmlWebApplicationContext();
        rootAppContext.setConfigLocation("/WEB-INF/dispatcher.xml");
        return rootAppContext; 
    }
    
    @Override
    protected Filter[] getServletFilters(){
        CharacterEncodingFilter encodingFilter = new CharacterEncoding Filter(); 
        encodingFilter.setEncoding("utf-8");
        Filter[] filters = new Filter[] {encodingFilter};
        return filters; 
    }
}

Annotation 기반의 설정

Annotation 기반(@Configuration) 기반으로 자바 설정을 사용하고 싶은 경우 AbstractAnnotationConfigDispatcherServletInitializer 를 상속받아 구현하는 것이 더 간결하다

public class SpringServletConfig extends AbstractAnnotationConfigDispatcherServletInitializer{
    @Override	protected Class<?>[] getRootConfigClasses(){ return new Class<?>[] RootConfig.class}
    @Override 	protected Class<?>[] getSErvletConfigClasses(){ return new Class<?>[] WebConfig.class}
    @Override   protected String getServletName(){/* return servletName; */}
    @Override   protected boolean isAsyncSupported(){/* return bool value */}
    @Override
    protected Filter[] getServletFilters(){
        CharacterEncodingFilter encodingFilter = new CharacterEncoding Filter(); 
        encodingFilter.setEncoding("utf-8");
        Filter[] filters = new Filter[] {encodingFilter};
        return filters; 
    }
}

Web.xml / Java code 기반 서블릿 정의 어느것이 더 좋은가?

모든 것이 그렇지만 장단점이 있는 것 같다. java code 기반으로 서블릿을 설정하는 경우 IDE에서 제공하는 자동완성 등의 기능을 더 잘 활용할 수 있을 것이다. web.xml을 사용하는 경우는 프로그램 수정 없이 서버의 재기동 만으로 설정을 변경할 수 있다는 장점이 있다. 따라서 운영중에 설정을 변경해야 하는 경우가 있을때는 web.xml 을 사용하는게 더 바람직 할 것으로 보인다. 만약, 운영 배포 이전에 반드시 테스트가 진행되거나 도커 컨테이너 이미지 등으로 생성해야 하는 경우는 java code 기반으로 작성하는 것이 더 배포/관리가 편할 것으로 생각된다.


추가 - SpringBoot 에서 서블릿은 어떻게 설정될까?

SpringBoot는 Spring Initializr을 사용하여 프로젝트를 생성하는 경우 tomncat, jetty, undertow 를 기본적인 embedded servlet container로 제공한다. 따라서 내장된 tomcat을 사용하는 경우에는 별도로 Servlet을 작성할 필요가 없다. (default servlet 으로 DispatcherServlet이 수행된다.)

@SpringBootApplication  // for auto-configuration of the application
public class WebBoardApplication {
    public static void main(String[] args) {
        SpringApplication.run(WebBoardApplication.class, args);
    }
}

만약 embedded servlet container를 사용하는 것이 아닌 외부의 servlet container(ex. tomcat) 을 사용해야 하는 경우 war 형태로 웹 어플리케이션을 배포해야 한다. 이때는 서블릿 정보를 확인할 수 있도록 아래와 같이 추가 작성해야 한다

import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;

public class ServletInitializer extends SpringBootServletInitializer {
    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
        return application.sources(WebBoardApplication.class);
    }
}

Embedded servlet container 를 사용하는 경우 DispatcherServlet이 사용됨으로 servlet을 별도로 구현할 것인지에 대한 고민은 상황에 따라 다를 것 같다. 이는 Spring boot를 좀 더 사용한 뒤에 깊이 알아볼 수 있지 않을까 생각된다.