티스토리 뷰

 

간단한 웹 프로그램을 한번이라도 개발해본 적이 있다면 웹 서버 또는 WAS에 대해 들어본 적이 있을 것이다. 이름도 비슷해서 혼동하기 쉬운데 이참에 둘의 개념을 확실히 이해해보자. 또한 스프링으로 개발된 프로그램이 서버에서 서비스될 때 어떤 프로세스로 요청이 처리되는지도 추가로 짚고 넘어가자.

 

 

웹 프로그램은 클라이언트의 HTTP 프로토콜에 기반한 요청에 대해 서버가 적절한 응답을 반환하는 식으로 동작한다. 즉 서버는 요청에 따른 리소스를 클라이언트에게 제공하는데, 이때 제공되는 리소스는 추가 처리 여부에 따라 정적 리소스, 동적 리소스로 구분될 수 있다. 먼저 정적 리소스는 별도의 서버에서의 처리 없이 단순히 조회되는 리소스로, 모든 사용자에게 동일하게 보여지며 예로는 HTML, CSS, JS, 이미지, 영상 파일 등이 있다. 반면 동적 리소스는 DB에서 데이터 조회 등 별도의 서버에서의 처리 결과가 포함된 리소스로, 요청마다 새롭게 생성되어 사용자별로 다르게 보여질 수 있다.

 

 

웹 서버와 WAS는 단순히 제공하는 리소스에 따라 구분할 수 있다. 즉 웹 서버는 주로 정적 리소스를 제공하며 WAS는 클라이언트의 요청에 따른 동적 리소스를 주로 제공한다. 이러한 구분에 대해 "아니 그냥 정적/동적 리소스를 모두 제공하는 한 종류의 서버만 두면 안되나?" 라는 의문이 들 수 있다. 이러한 의문을 해결하기전 웹 서버와 WAS에 대해 조금 더 자세히 알아보자.

 

 

 

Web Server(웹 서버)

HTTP 프로토콜을 기반으로 클라이언트가 요청한 정적 리소스를 제공하는 서버

 

웹 서버의 기능은 크게 2가지로 요약할 수 있는데, 먼저 정적 리소스를 클라이언트에게 제공하는 것과 애플리케이션 로직 같은 동적인 처리가 필요한 경우에는 웹 애플리케이션을 호출하는 것이다. 웹 서버의 예로는 Nginx, Apache HTTP Server 등이 있다.

 

 

WAS(웹 애플리케이션 서버, Web Application Server)

웹 서버로부터 오는 동적인 요청을 처리하는 서버

 

WAS는 프로그램 코드(ex. 서블릿, JSP)를 실행해 애플리케이션 로직을 수행하여 클라이언트별로 다른 응답을 제공한다. 웹 서버로 동적 처리를 하기 위해서는 모든 요청에 대한 결과값을 미리 만들어 놓아야 하기 때문에 자원 소모가 크지만 WAS는 요청에 따라 DB에서 데이터를 조회하거나 비즈니스 로직을 수행하여 그때 그때 결과를 만들기 때문에 자원을 보다 효율적으로 사용할 수 있다는 장점이 있다. WAS의 예로는 Tomcat, JBoss, Jeus, Web Sphere 등이 있다.

 

 

Web Server vs WAS

Web Server는 정적 리소스(파일) 제공, WAS는 애플리케이션 로직 처리

 

정리해보자면 웹 서버는 정적 리소스를, WAS는 애플리케이션 로직을 처리하여 동적 리소스를 제공한다고 결론내릴 수 있다. 그러나 사실 웹 서버도 프로그램 코드를 실행하는 기능을 포함하며 WAS도 웹 서버의 기능을 제공할 수 있기 때문에 어떤 타입의 컨텐츠(동적, 정적)를 주로 제공하느냐에 따라 구분하는 것이 더 적절하다. 

 

 

그럼 이제 위의 "아니 그냥 정적/동적 리소스를 모두 제공하는 한 종류의 서버만 두면 안되나?" 라는 의문에 대해 답해보자. WAS도 정적 리소스를 제공할 수 있기 때문에 웹 서비스를 WAS + DB 시스템만으로 구성할 수 있다. 그러나 이 경우 몇가지 문제점이 발생한다. 먼저 WAS가 너무 많은 역할을 담당하여 서버 과부하가 발생할 수 있다는 것이다. 즉 WAS가 동적 리소스뿐만 아니라 정적 리소스도 처리해야 하기 때문에 애플리케이션 로직 수행이 어려워질 수 있다. 또한 WAS 장애 시 정적 리소스를 제공하는 별도의 서버가 없어 오류 화면을 노출하는 것도 불가능해진다. 이러한 문제점으로 정적/동적 리소스를 모두 제공하는 한 종류의 서버만을 두는 것은 적절치 않다.

 

 

일반적으로 웹 서비스는 Web Server + WAS + DB 시스템으로 구성되는데 정적 리소스는 웹 서버가, 애플리케이션 로직은 WAS가 처리하며 웹 서버는 애플리케이션 로직 같은 동적인 처리가 필요한 경우에는 WAS에 요청을 위임하는 식으로 동작한다. 이러한 시스템은 WAS + DB 시스템에 비해 더욱 안정적이다. Web Sever + WAS + DB 시스템이 WAS + DB 시스템에 비해 가지는 장점은 다음과 같다. 

 

 

서버 부하 방지

웹 서버는 웹 페이지를 단순히 호스팅하고 렌더링하는 작업이 대부분이기 때문에 WAS에 비해 많은 자원을 소모하지 않는다. 따라서 단순한 정적 컨텐츠는 웹 서버에서 빠르게 클라이언트에게 제공하고 WAS는 DB 조회, 비즈니스 로직 등을 처리하는데 집중하여 서버 부하를 적절하게 분배할 수 있다.

 

효율적인 리소스 관리

기능별로 서버를 분리하였기 때문에 특정 종류의 요청이 증가하면 해당 요청을 처리하는 서버만을 증설하는 식으로 리소스를 효율적으로 관리할 수 있다. 즉 정적 리소스가 많이 사용되면 웹 서버를, 애플리케이션 리소스가 많이 사용되면 WAS를 증설하면 된다.

 

보안 강화

서버가 물리적으로 분리되어 있어 보안이 강화될 수 있다. 예를 들어 웹 서버가 SSL에 대한 암호화 및 복호화 처리를 하거나 접근 허용 IP를 관리함으로써 부적절한 요청으로부터 서버를 보호할 수 있다.

*SSL(Secure Sockets Layer): 인터넷에서 데이터를 안전하게 전송하기 위한 인터넷 통신 규약 프로토콜

 

장애 극복 및 대응

애플리케이션 로직이 동작하는 WAS는 서버가 다운될 가능성이 높지만 정적 리소스만 제공하는 웹 서버는 잘 죽지 않아 WAS, DB 장애 시에도 웹 서버가 오류 화면을 제공해줄 수 있다. 또한 여러 대의 WAS를 사용하는 대용량 웹 애플리케이션의 경우 앞 단의 웹 서버는 정상적으로 동작하는 WAS에게만 요청을 위임함으로써 특정 WAS에 오류가 발생해도 사용자는 계속해서 서비스를 이용할 수 있는 등 무중단 운영을 위한 장애 극복에 보다 쉽게 대응할 수 있다.

 

 

 

Web Server + Web Container 구조의 WAS

 

그러나 사실 지금까지의 웹 서버와 WAS에 대한 정의는 개념적인 구분이고, 실제 서버들에서 웹 서버와 WAS 간 경계는 모호하다. 예를 들어 WAS인 Tomcat은 웹 서버의 기능도 포함하고 있다. 더 정확히 말하자면 톰캣은 Web Server + Web Container로 구성되어 있다. 따라서 클라이언트로부터 동적 처리가 필요한 요청이 들어오면 톰캣 내의 웹 서버는 컨테이너에게 처리를 위임하며 처리 결과는 컨테이너로부터 전달받는 식으로 동작하게 된다. 이러한 웹 서버와 컨테이너 간 통신을 위해 CGI라는 일종의 규약이 필요하게 되는데 이에 대해서는 아래에서 더 자세히 알아보자.

*컨테이너: 동적인 데이터들을 처리하여 정적인 페이지로 생성해주는 소프트웨어 모듈, 자바의 경우 서블릿 컨테이너

 

 


CGI(Common GateWay Interface)

클라이언트로부터 동적 처리가 필요한 요청이 들어왔을 때 웹 서버는 WAS 즉 웹 애플리케이션에게 처리를 위임한다. 그러나 웹 서버와 웹 애플리케이션은 서로 다른 언어, 체계로 만들어졌기 때문에 데이터를 주고 받는 방식에 대한 규칙이 필요한데 이것이 바로 CGI 즉 Common GateWay Interface이다. 이러한 규칙 CGI를 따라 작성된 프로그램을 CGI 프로그램이라 하는데 기본적으로 동적인 처리를 하는 웹 애플리케이션을 실행하고 실행 결과를 다시 웹 서버로 전달해주는 역할을 한다.

 

자바로 만든 CGI 프로그램은 별도로 서블릿(Servlet)이라 지칭한다. 서블릿은 Server + Applet의 합성어로 클라이언트에게 서비스를 제공하는 작은 단위의 서버 프로그램이라 할 수 있다. 다른 언어로 작성된 CGI 프로그램과 달리 서블릿은 웹 서버와 직접 데이터를 주고 받지 않고 별도의 프로그램에 의해 관리되는 것이 특징으로, 이러한 서블릿의 생성, 실행, 소멸 등의 생명주기를 관리하며 서블릿을 대신해 CGI 규칙에 따라 웹 서버와 데이터를 주고 받는 별도의 프로그램이 바로 서블릿 컨테이너이다. 그럼 서블릿 컨테이너가 어떤 식으로 서블릿을 관리하는지, 스프링의 요청 처리 단계 어디쯤에서 서블릿 컨테이너와 서블릿이 사용되는지에 대해 자세히 알아보자. 

 

 

 

서블릿 컨테이너(Servlet Container)와 서블릿(Servlet)

간단히 말해 서블릿은 요청에 대한 응답을 동적으로 생성하여 요청을 처리하는 클래스로 서블릿 컨테이너는 이러한 서블릿들을 관리한다. 웹 서버가 요청을 받으면 이를 서블릿 컨테이너에게 전달하고 서블릿 컨테이너는 다시 적절한 서블릿에게 처리를 위임한다. 서블릿 컨테이너는 단순히 서블릿을 관리하고 처리를 위임하는 것 외에도 다양한 기능을 하는데 주요 기능은 다음과 같다. 

 

 

웹 서버와의 통신 지원

일반적으로 웹 서버와 통신하기 위해서는 소켓을 생성 후 listen, accept 과정을 거쳐야하지만, 서블릿 컨테이너는 이러한 과정을 API로 제공하여 웹 서버와 서블릿 간 보다 편리한 통신이 가능하도록 한다. 

 

멀티 쓰레딩 관리

서블릿 컨테이너는 요청마다 새로운 쓰레드를 생성해 작업을 수행한다. 즉 동시 요청에 대해 서블릿 컨테이너가 알아서 멀티 쓰레딩 처리를 지원하기 때문에 개발자는 복잡한 멀티 쓰레드 관련 코드를 신경쓰지 않고 편리하게 개발할 수 있다.

 

쓰레드 생성 비용은 매우 비싸고 요청 시마다 쓰레드를 생성하면 응답 속도가 늦어지기 때문에 실제 서블릿 컨테이너는 쓰레드 풀 방식으로 쓰레드를 관리한다. 즉 풀 안에 쓰레드를 미리 생성해놓고 필요 시 쓰레드를 꺼내 사용하고 사용을 완료하면 다시 쓰레드를 풀에 반납하는 식으로 동작한다. 이 경우 쓰레드가 미리 생성되어 있어 쓰레드를 생성하고 종료하는 비용이 절약되며 요청에 대한 응답 시간도 빨라진다. 또한 생성 가능한 쓰레드의 최대치가 정해져 있어 매우 많은 요청이 들어와도 서버가 죽지 않아 기존 요청은 안전하게 처리될 수 있다. (쓰레드가 무제한으로 생성되면 서버 자원이 고갈되어 서버가 죽을 수 있다.)

 

선언적인 보안관리

서블릿 컨테이너를 사용하면 개발자는 보안과 관련된 내용을 서블릿 또는 자바 클래스에 구현하지 않아도 된다. 일반적으로 보안 관련 내용은 XML 배포 서술자에 기록하기 때문에 보안 관련 수정 사항이 생겨도 자바 코드를 수정해 다시 컴파일할 필요가 없다.

 

생명주기 관리

서블릿 컨테이너의 가장 중요한 기능은 서블릿의 생성, 초기화, 실행, 소멸의 생명주기를 관리하는 것이다. 서블릿 컨테이너는 다음의 과정으로 서블릿의 생명주기를 관리한다.

1. 서블릿 클래스를 로딩하여 인스턴스화(서블릿 클래스 생성자 호출)한 후, 초기화 메소드 호출 

2. 해당 서블릿의 실행 메소드 호출 

3. 소멸 메소드 호출하여 가비지 컬렉터로 메모리에서 제거

 

 

 

서블릿의 생명주기 메소드 

서블릿의 생명주기를 관리하기 위한 메소드에는 init, service, destroy가 있다. 실제 Servlet 인터페이스에도 해당 메소드들이 정의되어 있는 것을 확인할 수 있다. 그럼 각 메소드들은 어떤 기능을 하는지 자세히 알아보자. 

package jakarta.servlet;

import java.io.IOException;

public interface Servlet {

    void init(ServletConfig config) throws ServletException;
    void service(ServletRequest req, ServletResponse res) throws ServletException, IOException;
    void destroy();
    //...
}

 

 

init

서블릿 컨테이너는 웹 서버로부터 요청을 받았을 때 해당 요청을 처리할 수 있는 서블릿이 현재 컨테이너 안에 없다면 ClassLoader로 해당 서블릿 클래스를 로딩하고 객체화한 후 init 메소드를 호출하여 해당 서블릿 객체를 초기화한다. 즉 init 메소드는 서블릿 생성 후 딱 한 번만 호출되며 해당 서블릿이 요청을 처리하기 전(service 메소드를 호출하기 전) 항상 init 메소드가 성공적으로 호출되어야 한다. 만약 init 메소드 호출 시 ServletException 예외가 발생하거나 일정 시간 내에 init 메소드가 반환되지 않으면 서블릿 컨테이너는 해당 서블릿의 service 메소드를 호출할 수 없다. 

 

 

service

서블릿 컨테이너는 클라이언트의 요청을 처리할 수 있는 적절한 서블릿의 service 메소드를 호출함으로써 처리를 위임한다. service 메소드는 서블릿 컨테이너의 새로운 쓰레드에서 실행되므로 동시 요청은 멀티 쓰레드로 처리할 수 있다.

 

서블릿은 HTTP 프로토콜에 제한된 기술이 아니기 때문에 웹 애플리케이션 개발 시에는 기존 Servlet에 HTTP 관련 기능이 추가된 HttpServlet 인터페이스를 주로 사용한다. HttpServletservice 메소드의 파라미터로는 요청 관련 정보가 담긴 HttpServletRequest 객체와 응답 관련 정보를 담을 HttpServletResponse 객체가 전달되는데, 해당 객체들은 요청 시마다 서블릿 컨테이너에 의해 생성된다. 아래 코드는 실제 HttpServletservice 메소드의 일부로, service 메소드에서는 HttpServletRequest 객체를 통해 요청의 메소드 타입을 파악하고 이에 따라 doGet, doPost 메소드 등을 호출하는 것을 확인할 수 있다.

protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    String method = req.getMethod();
    if (method.equals(METHOD_GET)) {
        //...
        doGet(req, resp);
        //...
    } else if (method.equals(METHOD_HEAD)) {
        //...
        doHead(req, resp);
        //...
    } else if (method.equals(METHOD_POST)) {
        doPost(req, resp);
    } else if (method.equals(METHOD_PUT)) {
        doPut(req, resp);
    } else if (method.equals(METHOD_DELETE)) {
        doDelete(req, resp);
    } else if (method.equals(METHOD_OPTIONS)) {
        doOptions(req, resp);
    } else if (method.equals(METHOD_TRACE)) {
        doTrace(req, resp);
    } else {
        //...
        resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg);
    }
}

 

 

참고로 요청별로 적절한 서블릿을 매핑하기 위해서는 xml 파일로 직접 서블릿 클래스와 요청의 URL 패턴을 지정하는 방법과 별도의 파일 없이 간단하게 서블릿 클래스 내에서 어노테이션을 활용하여 지정하는 방법이 있다. 아래 예에서는 /test 요청에 대해 com.example.TestServlet 클래스가 매핑되도록 설정하고 있다. 

<!-- web.xml 파일 설정 -->
<web-app>

    <servlet>
       <servlet-name>testServlet</servlet-name>
       <servlet-class>com.example.TestServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>testServlet</servlet-name>
        <url-pattern>/test</url-pattern>
    </servlet-mapping>

</web-app>
//@WebServlet 어노테이션 설정
package com.example.testServlet;

@WebServlet(name = "testServlet", urlPatterns = "/test")
public class TestServlet extends HttpServlet {

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        ...
    }
}

 

 

destroy

서블릿 컨테이너 종료 시 서블릿도 함께 종료되며 이때 destroy 메소드를 호출하여 서블릿 관련 모든 리소스를 메모리에서 해제한다.

 

 

요청마다 계속해서 객체를 생성하는 것은 비효율적이기 때문에 서블릿 컨테이너는 서블릿 객체를 싱글톤으로 관리한다. 즉 최초 요청 시에만 서블릿 객체를 생성하고 이후 요청에서는 이미 생성되어 있는 서블릿을 재활용한다. 따라서 동일한 요청이 동시에 발생한 경우 여러 쓰레드에서 동일한 서블릿 객체에 접근할 수 있기 때문에 서블릿 내의 공유 변수 사용에 주의해야 한다. 즉 서블릿의 인스턴스 변수에 특정 사용자를 위한 데이터나 요청 관련 데이터를 보관해서는 안된다.

 

 

 

서블릿 컨테이너와 서블릿의 요청 처리 흐름 정리

Web Server와 Web Container 기반 웹 서비스의 요청 처리 흐름

 

마지막으로 웹 서버로부터 요청이 전달되었을 때 서블릿 컨테이너의 요청 처리 흐름을 정리해보자. 

 

1. 서블릿 컨테이너는 클라이언트의 요청을 처리할 수 있는 서블릿을 컨테이너 내에서 찾음 

2. 현재 요청을 처리할 수 있는 서블릿이 컨테이너 내에 없다면, 가능한 서블릿 클래스를 ClassLoader로 로딩하고 인스턴스화한 후 init 메소드를 호출하여 초기화 

3. 새로운 쓰레드에서 해당 서블릿의 service 메소드를 호출하여 요청 처리 (HTTP 요청을 파싱하여 HttpServletRequest 객체를 생성하고 빈 HttpServletResponse 객체도 생성하여 파라미터로 전달)

4. service 메소드 결과 HttpServletResponse 객체를 HTTP 프로토콜에 맞추어 웹 서버에 전달

5. 서블릿 컨테이너 또는 웹 애플리케이션 종료 시, 컨테이너 내 모든 서블릿의 destroy 메소드를 호출하여 서블릿을 소멸시킴

 

 

 

Spring의 서블릿 컨테이너 

 

스프링에서 직접 HttpServlet 인터페이스를 구현하여 서블릿을 개발해본 경험을 아마 드물 것이다. 이는 바로 스프링의 혁신적인? 서비스 구조인 MVC 패턴과 프론트 컨트롤러 패턴 때문인데, 간단히 말해 스프링에서는 특정 요청을 처리하는 클래스로 Servlet이 아닌 Controller를 사용하기 때문이다.

 

 

 

Servlet이 아니라 Controller를 선택했을까 이유를 생각해보자. 위에서 살펴봤듯이 Servlet의 요청 처리 메소드인 service는 메소드 시그니처가 확정되어 있어 보다 유연한 개발이 매우 어렵다. 또한 비즈니스 로직을 처리하는 부분이 HttpServletRequest, HttpServletResponse 같은 HTTP 프로토콜과 서블릿 기술 관련 클래스에 의존하고 있다. 이 경우 기존 웹 애플리케이션에서 다른 통신 프로토콜이나 서블릿이 아닌 다른 기술을 사용하게 되면 비즈니스 로직에까지도 영향을 미치게 된다. 즉 기술적인 변경이 어플리케이션 로직에도 영향을 미쳐 객체지향설계 원칙인 OCP에 적합하지 않다.

 

또한 파라미터로 전달된 HttpServletRequest 객체로부터 요청 내용을 파악하기 위한 지루하고 쓸데없는 작업이 매번 반복될 수 밖에 없다. 이러한 단점들을 보완하기 위해 Controller가 등장했다고 볼 수 있다.

*OCP(Open-Closed Principle, 개방-폐쇄 원칙): 소프트웨어 개체는 확장에는 열려 있어야 하고, 변경에는 닫혀 있어야 한다는 원칙

 

 

 

그럼 스프링에서는 Servlet이 아예 사용되지 않느냐? 그건 아니다. 개발자가 직접 Servlet을 구현할 필요가 없다 뿐이지 스프링에서는 모든 요청에 대해 매핑되는 아주 중요한 서블릿이 존재하는데 바로 DispatcherServlet이다. 즉 DispatcherServlet이 마치 문지기처럼 모든 요청을 받아들이며 DispatcherServletservice 메소드에서는 아주 복잡한 일련의 과정을 거쳐 현재 요청에 매핑되는 Controller에게 다시 처리를 위임한다.

 

진짜 마지막으로 스프링에서의 요청 처리 흐름을 전체적으로 정리해보자.

 

1. 클라이언트로부터 동적 처리가 필요한 요청이 웹 서버로 전달

2. 웹 서버는 서블릿 컨테이너에 처리를 위임

3. 서블릿 컨테이너는 모든 요청에 매핑되는 DispatcherServlet을 생성하고 초기화한 후(최초 요청 시에만) service 메소드 호출

4. 여러 객체들의 도움을 통해 현재 요청과 매핑되는 Controller에 처리를 위임

5. ControllerService, Repository 등 다른 객체들의 도움을 통해 요청 처리 완료

6. Controller가 반환한 결과를 여러 객체들의 도움을 통해 HttpServletResponse 객체에 저장

7. 처리 결과인 HttpServletResponse 객체를 HTTP 프로토콜에 맞추어 웹 서버로 전달 

 

(스프링의 DispatcherServlet 관련 더 자세한 내용은 다음을 참고)

 

 

 

끝.

 

 

 

참고

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-1/dashboard

https://inpa.tistory.com/entry/WEB-%F0%9F%8C%90-%EC%9B%B9-%EC%84%9C%EB%B9%84%EC%8A%A4-%EA%B5%AC%EC%A1%B0-%EC%A0%95%EB%A6%AC#%EC%9B%B9_%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88_container

https://jinbroing.tistory.com/205

https://lasbe.tistory.com/91

https://taes-k.github.io/2020/02/16/servlet-container-spring-container/

https://www.programcreek.com/2013/04/what-is-servlet-container/

https://www.baeldung.com/intro-to-servlets

 

php-style Home

php-style stories

php-style.selfhow.com

 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/04   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30
글 보관함