Backend/Spring

Web Server, WAS, Servlet

olsohee 2024. 1. 2. 18:11

Web Server

  • 웹 서버는 클라이언트가 웹 브라우저에서 어떠한 페이지 요청을 하면 웹 서버에서 그 요청을 받아 정적 컨텐츠를 제공하는 서버이다. 
  • 여기서 정적 컨텐츠란 html 문서, css, 자바스크립트, 이미지, 영상 등 즉시 응답 가능한 컨텐츠이다.
  • 동적인 컨텐츠 요청시에는 WAS에게 클라이언트의 요청을 전달하고, WAS가 처리한 결과를 클라이언트에게 전달한다.

WAS(Web Application Server)

  • WAS는 웹 서버의 기능을 포함하여 정적 컨텐츠를 제공할 뿐만 아니라, DB 조회나 비즈니스 로직 처리 등을 요구하는 동적 컨텐츠도 제공하는 애플리케이션 서버이다.
  • 즉 WAS는 "Web Server + Web Container(Servlet Container)"라고 정의하기도 한다. 그래서 정적 컨텐츠를 제공하기도 하면서(Web Server), 웹 컨테이너를 이용하여 내부 로직을 거쳐 동적 컨텐츠(Web Container)도 제공할 수 있는 것이다. 
  • 결국 WAS는 웹 서버에서 요청을 받고, 이를 웹 컨테이너로 보내 요청을 수행한 후, 그 결과를 다시 웹 서버로 보내 최종적으로 클라이언트에게 보내주는 것이다.

웹 서버와 WAS를 구분하는 이유

WAS는 웹 서버의 기능을 포함한다. 그렇다면 WAS만 있으면 되는 것이 아닐까? 왜 웹 서버를 사용할까?

  • 서버 과부화를 방지: 정적 컨텐츠는 웹 서버만으로도 제공할 수 있다. 따라서 웹 서버에서 정적 컨텐츠를 제공하여 WAS의 부담을 줄일 수 있다. 반면 동적 컨텐츠에 대한 요청은 웹 서버가 WAS의 Web Container에게 요청을 위임하여, Web Container가 동적인 컨텐츠에 대한 로직을 수행하도록 한다.
  • 효율적인 리소스 관리: 정적 리소스가 많이 사용되면 웹 서버를 증설하고, 애플리케이션 리소스가 많이 사용되면 WAS를 증설한다.
  • WAS 오류시 오류 화면 제공 가능: 웹 서버는 잘 죽지 않으나, 애플리케이션 로직이 동작하는 WAS는 잘 죽는다. 만약 WAS만 사용하면 오류가 나면 오류 화면조차 제공할 수 없다. 그러나 웹 서버를 함께 사용하면, WAS에서 오류가 발생해도 웹 서버에서 오류 화면을 제공할 수 있다.

Servlet

클라이언트가 웹 브라우저에서 서버로 요청을 보내면, HTTP 요청 메시지가 생성되어 서버로 전달된다. 그러면 개발자는 웹 브라우저가 생성한 HTTP 요청 메시지를 직접 해석하고 처리해야 한다. 그리고 응답에 대해 HTTP 응답 메시지를 생성하여 반환해야 한다.

 

다음은 HTTP 요청 메시지의 예시이다. 개발자가 이를 직접 해석하고 처리하는 것은 매우 힘들고 비효율적이다. 

다음과 같이 의미있는 비즈니스 로직은 얼마 되지 않는다. 나머지는 HTTP 요청 메시지를 해석하며 처리하고, 응답 메시지를 생성해서 반환하는 과정이다. 

서블릿을 사용하면 반복되는 HTTP 요청 메시지를 처리하고 HTTP 응답 메시지를 생성하는 과정을 생략할 수 있다서블릿은 HTTP 요청을 처리하고 그에 대한 응답을 반환하는 자바 웹 프로그래밍 기술이다. 즉, HTTP 요청을 처리하고 그 결과를 응답하는 과정을 서블릿이라는 자바 프로그램이 하는 것이다. 따라서 개발자는 비즈니스 로직에만 집중할 수 있다.

Servlet Container

그리고 이때 서블릿을 관리해주는 것이 서블릿 컨테이너이다.

서블릿 컨테이너의 특징은 다음과 같다.

  • 서블릿 컨테이너는 클라이언트의 요청을 받아 응답할 수 있도록 웹 서버와 소켓으로 통신한다. 톰캣이 서블릿 컨테이너의 대표적인 예이다.
  • 톰캣처럼 서블릿을 지원하는 WAS를 서블릿 컨테이너라고 한다. WAS는 Web Server + Web Container(Servlet Container)이므로, WAS가 서블릿 컨테이너를 포함하는 개념이라고 이해하면 된다
  • 서블릿 컨테이너는 서블릿 객체의 생성, 초기화, 호출, 종료 등 서블릿 객체의 생명주기를 관리한다.
    • 서블릿 컨테이너는 HTTP 요청마다 request, response 객체를 생성하여 서블릿에게 넘겨준다. 
    • 서블릿 객체는 싱글톤으로 관리된다. 따라서 고객의 요청이 올 때마다 서블릿 객체가 새로 생성되지 않고, 서블릿 컨테이너에서 서블릿 객체를 조회하여 재사용한다. 따라서 서블릿 객체에서 공유 변수 사용에 주의해야 한다.
    • 서블릿 객체는 서블릿 컨테이너 종료시 함께 소멸된다.
  • 서블릿은 동시 요청을 위한 멀티스레드 처리를 지원한다. 서블릿 컨테이너는 요청마다 자바 스레드를 새롭게 생성한다. 
    • 만약 스레드가 1개이면 하나의 요청에 대한 처리가 지연될 때, 다른 요청에 대한 처리도 지연된다. 따라서 요청마다 스레드를 생성하는 멀티스레드 방법이 있다. 요청마다 스레드를 생성한다면, 요청 1에 대한 처리가 지연될 때, 요청 2에 대한 처리는 별도의 스레드에서 처리되기 때문에 지연되지 않는다.
    • 요청마다 스레드를 생성했을 때의 장점은 다음과같다.
      • 동시 요청을 처리할 수 있다.
      • 하나의 스레드가 지연되어도 나머지 스레드는 정상 동작한다.
    • 그러나 다음과 같은 단점이 있다.
      • 스레드는 생성 비용이 매우 비싸다. 고객의 요청이 올 때마다 스레드를 생성하면 응답 속도가 느려진다.
      • 멀티 스레드의 경우 컨텍스트 스위칭 비용이 발생한다. CPU의 코어는, 코어 하나 당 하나의 스레드만 수행할 수 있다. 따라서 여러 개의 스레드를 동시에 수행하는 것이 아니라 각 스레드를 각각 번갈아서 수행한다. 따라서 멀티 스레드를 사용할 때는 CPU에서 실행할 프로세스를 교체하는 컨텍스트 스위칭 비용이 발생한다.
      • 스레드 생성에 제한이 없다. 따라서 고객의 요청이 너무 많이 오면, CPU와 메모리의 임계점을 넘어서 서버가 죽을 수 있다.
    • 따라서 이를 해결하기 위해 스레드 풀이라는 개념이 등장한다.
      • 스레드 풀에 생성 가능한 스레드의 최대 치를 미리 생성해서 관리한다. (톰캣은 기본 설정이 200개이다.)
      • 스레드가 필요하면 스레드 풀에서 이미 생성된 스레드를 꺼내 쓰고, 스레드 사용이 종료되면 스레드 풀에 스레드를 반납힌다.
      • 최대 스레드가 모두 사용 중이어서 스레드 풀에 스레드가 없으면, 요청을 거절하거나 특정 수만큼 대기하도록 설정할 수 있다.
      • 스레드가 미리 생성되어 있으므로, 스레드를 생성하는 비용이 절약되어 응답 속도가 빠르다.
      • 생성 가능한 스레드의 최대치가 정해져 있으므로, 많은 요청이 들어와도 기존 요청은 안전하게 처리할 수 있다.
      • WAS의 주요 튜닝 포인트는 스레드 풀의 최대 스레드 수이다. 
        • 이 값을 너무 낮게 설정하면, 동시 요청이 많은 경우 서버 리소스는 여유롭지만 클라이언트 입장에서는 스레드를 기다려야 하므로 응답이 지연된다.
        • 이 값을 너무 높게 설정하면, 동시 요청이 많은 경우 많은 요청을 모두 받아들여 스레드를 사용하므로 CPU와 메모리 리소스 임계점 초과로 서버가 다운될 수 있다.
        • 따라서 실제 서비스와 유사하게 성능 테스트를 하며 적절한 수를 찾아야 한다.
    •  핵심은 서블릿 컨테이너가 멀티 스레드를 지원한다는 것이다. 따라서 멀티 스레드에 대한 부분은서블릿 컨테이너가 대신 처리해주기 때문에 개발자는 스레드 풀의 최댓값만 설정하고, 싱글 스레드 프로그래밍을 하듯 편리하게 소스 코드를 개발할 수 있다. 단, 멀티 스레드 환경이므로 서블릿이나 스프링 빈 같은 싱글톤 객체는 주의해서 사용해야 한다.

Servlet 사용 예제

 

@ServletComponentScan 
@SpringBootApplication
public class ServletApplication {

	public static void main(String[] args) {
		SpringApplication.run(ServletApplication.class, args);
	}
}
  • @ServletComponentScan: 해당 클래스의 패키지를 포함한 하위 패키지를 탐색하여 서블릿을 찾아서 서블릿 컨테이너에 등록된다.
@WebServlet(name = "helloServlet", urlPatterns = "/hello")
public class HelloServlet extends HttpServlet {

    // urlPatterns로 지정한 url을 통해 서블릿이 호출되면 service 메소드가 호출된다.
    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        System.out.println("HelloServlet.service() 호출");

        // request, response 객체 조회
        System.out.println("request = " + request);
        System.out.println("response = " + response);

        // 쿼리 파라미터 값 조회
        String username = request.getParameter("username");
        System.out.println("username = " + username);

        // 응답 메시지 생성
        response.setContentType("text/plain");
        response.setCharacterEncoding("utf-8");
        response.getWriter().write("hello " + username);
    }
}
  • @WebServlet: 서블릿으로 등록하기 위해 필요한 어노테이션이다.
    • name: 서블릿 이름
    • urlPatterns: url 매칭
  • 매핑된 url(/hello)이 호출되면서 특정 서블릿(HelloServlet)이 호출되면, 서블릿 컨테이너는 해당 서블릿의 service 메소드를 호출한다.

http://localhost:8080/hello?username=kim 호출 결과는 다음과 같다.

 

  • request 객체와 response 객체를 확인해보면 RequestFacade, ResponseFacade라고 되어 있는 것을 알 수 있다. HttpServletRequest와 HttpServletResponse는 인터페이스이다. WAS의 종류로 톰캣, 제티 등 여러가지가 있는데, 이 WAS들이 HttpServletRequest와 HttpServletResponse를 구현한다. RequestFacade, ResponseFacade는 톰캣의 HttpServletRequest와 HttpServletResponse 구현체이다.
  • request.getParameter(): 쿼리 파라미터의 값을 편리하게 조회할 수 있다. 만약 서블릿이 없었다면, 개발자가 직접 HTTP 요청을 파싱해서 쿼리 파라미터의 값을 읽어왔어야 할 것이다.
  • response.setContentType(), response.setCharacterEncoding(): HTTP 응답 메시지의 헤더에 데이터가 들어가게 된다.
  • response.getWriter().write(): HTTP 응답 메시지의 바디에 데이터가 들어가게 된다.

Servlet 동작 방식

@WebServlet(name = "helloServlet", urlPatterns = "/hello")
public class HelloServlet extends HttpServlet {

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {}
}
  1. 스프링 부트를 통해 애플리케이션을 실행시키면 스프링 부트는 내장된 톰캣 서버를 띄운다. 톰캣 서버는 내부에 서블릿 컨테이너를 가지고 있다.
  2. 클라이언트가 "/hello" url로 HTTP 요청을 보내면 Web Server는 요청을 WAS의 서블릿 컨테이너에게 넘겨준다. 
  3. 서블릿 컨테이너는 HTTP 요청을 받으면 request, response 객체를 생성하고, 요청에 맞는 서블릿(HelloServlet)의 service 메소드를 호출하며 생성한 request, response 객체를 넘겨준다.
    • 이때 request, response 객체는 요청마다 생성되고, 서블릿 객체는 싱글톤으로 관리된다.
    • 이때 서블릿 객체의 생성 시점은 설정에 따라 다르다. 서블릿 컨테이너의 로딩 시점에 생성될 수도 있고, HTTP 요청이 들어올 때 생성될 수도 있다.
  4. 개발자는 request 객체에서 HTTP 요청 정보를 편리하게 꺼내서 사용할 수 있다. (직접 HTTP 요청 메시지를 해석할 필요가 없다.)
  5. 개발자는 response 객체에 HTTP 응답 정보를 편리하게 입력할 수 있다. (직접 HTTP 응답 메시지를 형식에 맞게 생성하지 않아도 된다.)
  6. WAS는 response 객체에 담긴 내용을 토대로 HTTP 응답 메시지를 생성하고 웹 브라우저에 전달한다.

Reference