웹 서버(WS)의 진화 : Apache vs NGINX
위 post에 이어지는 내용입니다.

WAS 란?

WAS의 정의

먼저 WAS(Web Application Server)의 정의는 다음과 같다.

웹 애플리케이션과 서버 환경을 만들어 동작시키는 기능을 제공하는 소프트웨어 프레임워크

대표적으로 Apache Tomcat이 있다.

  • 참고로 Apache Tomcat는 WS 단계에서 계속 이야기했던 Apache와는 아예 별개의 소프트웨어이다.
Apache와 Tomcat Apache HTTP Server (보통 Apache라고 부름)
  • 웹 서버(WS) 역할을 담당
  • 주로 정적 콘텐츠를 처리
  • HTTP 요청을 처리하며, 동적 콘텐츠를 처리할 경우에는 WAS(Tomcat)와 연동하여 작업을 나눕니다.
Apache Tomcat (보통 Tomcat이라고 부름)
  • WAS 역할을 담당
  • Servlet Container로서 동작하며, Servlet/JSP 기반의 동적 콘텐츠를 처리
    • 정적 콘텐츠도 처리할 수는 있다.
    • 정적 콘텐츠 처리 성능은 Apache HTTP Server보다 낮다.
  • Java EE 표준의 일부인 Servlet/JSP 스펙을 구현하며, Spring MVC와 같은 프레임워크의 기반이 되는 동작을 지원한다.


그러나 이런 물음표가 떠오른다.

❓ WAS? TOMCAT? Servlet? Servlet Container? Spring MVC?

다음 기본 개념부터 인지하고 넘어가면 이해하기 쉽다.

  1. WAS는 클라이언트의 요청을 처리하여 동적인 웹 콘텐츠를 제공하는 서버를 말한다.
  2. Tomcat은 WAS의 대표적인 예시 중 하나이다.
  3. Tomcat은 Servlet Container의 한 종류이며, Servlet/JSP 요청을 처리하는 역할을 한다.
  4. Tomcat은 Servlet 표준에 맞춘 애플리케이션을 실행하는 데 최적화되어 있다.
    1. Servlet 표준Java EE(현재는 Jakarta EE) 명세의 일부로, Java 기반 동적 웹 콘텐츠 처리를 위한 규격이다.
    2. 즉, Tomcat은 Java 기반의 웹 애플리케이션을 실행하기 위한 서버이다.

WS와 WAS의 차이의 핵심는 ‘동적 처리 여부’에 있다.

  • 현대 웹사이트는 스크롤에 따라 새로운 피드를 보여주거나,
    사용자 맞춤 광고를 제공하는 등의 동적 기능을 포함한다.
  • 이러한 “사용자 맞춤 가공 처리(동적 콘텐츠)”는 결국 서버가 특정 코드를 실행했다는 의미다.
  • 결국 궁금한 것은 ‘서버가 어떻게 이 코드를 실행하고, 그 결과를 클라이언트에게 제공하는가’이다.
Memory Usage

누구나에게 제공되는 “정적인” 컨텐츠이다.

Requests Per Second

하지만 로그인하고 나면 “동적인” 컨텐츠가 로드된다.

아래는 동적 페이지를 로드하는 과정을 그림으로 표현한 것이다.

Image

우리는 앞서 실제 운영/대규모 서비스는 WS-WAS 분리 구조를 선호한다고 말했다.

나는 WAS의 한 종류인 Tomcat까지의 웹통신 흐름이 어떻게 이루어지는 지가 궁금해졌다.
그리고 ‘jvm, survlet, spring 등 어떤 개념이고 어떤 연관관계가 있고 어떻게 유동적으로 작동할까…?’ 가 궁금했다.


JVM(Java Virtual Machine)

Image

JVM 자체가 현재 내용에서 매우 중요한 내용은 아니라 짧게 이야기하면,

  • ⇒ 그냥 JVM은 간단히 Java 코드를 실행시켜주는 놈이라고 생각하자.
  • WAS(Tomcat)는 Java로 작성된 웹 애플리케이션을 실행하는 컨테이너이므로,
    WAS도 JVM 위에서 동작한다.
JVM 이란? Apache HTTP Server (보통 Apache라고 부름)
  • JVM은 JVM은 Java 바이트코드(.class 파일)를 해석하고 실행하는 가상 머신
  • JRE는 Java 프로그램을 실행하는 데 필요한 모든 것을 포함한다. 라이브러리 등
    • JRE은 JVM을 시작, 필요한 클래스 라이브러리 로드, main() 메서드를 실행한다.
  • JDK는 Java 애플리케이션을 개발하는 데 필요한 모든 도구를 포함한다.


Servlet Container [Tomcat]

위에서 “Tomcat은 Servlet Container의 한 종류이며, Servlet/JSP 요청을 처리하는 역할을 한다.” 라고 했다.

1) Servlet Container는 Servlet의 생명 주기를 관리하는 곳이다.

  • Servlet과 Servlet Container의 관계를 Bean과 Bean Container의 관계와 흡사하다고 생각하면 된다. (“컨테이너와 관리 대상”의 관계로 이해)
    • Servlet Container는 HTTP 요청과 응답을 처리하는 Servlet을 관리
    • Bean Container는 Spring의 애플리케이션 내 객체(Bean)의 생명 주기와 의존성을 관리

2) 또한, HTTP 요청 처리 및 응답 생성 과정은 Servlet Container의 핵심 기능 중 하나이다.

  • 웹 브라우저에서 요청이 들어오면, Tomcat은 그 요청을 받아서 적절한 Java 코드(서블릿)로 전달하고, 그 결과를 다시 브라우저로 보내는 역할을 한다.


Servlet

Servlet은 HTTP 요청을 처리하고 응답을 생성하는 스펙을 정의한 인터페이스이다.

  • Servlet은 jakarta.servlet.Servlet을 최상위 인터페이스로 가진다. (Java API)

Servlet은 Servlet Container에 의해 관리되며, 요청이 들어오면 컨테이너가 Servlet의 생명 주기를 관리하고 요청을 처리하기 위한 스레드를 생성한다.

Servlet은 HTTP 요청을 처리하고, 그에 따른 적절한 응답을 생성하는 기본 단위이다. 이를 통해 REST API의 엔드포인트가 되거나 동적 웹 페이지를 생성하는 역할을 수행할 수 있다.

  1. Servlet은 HTTP 요청을 받아 비즈니스 로직을 처리하고 JSON, XML 등의 형식으로 데이터를 응답할 수 있다.
  2. 동적인 HTML 콘텐츠를 생성할 때도 사용된다.
    • 예를 들어, 비즈니스 로직 처리가 필요한 HTTP 요청이 Web Server로 들어오면, Web Server는 이를 Servlet Container가 실행하는 Servlet에 전달한다.
    • Servlet은 요청을 처리하고 동적인 웹 콘텐츠를 생성한 뒤, 결과를 Web Server를 통해 클라이언트로 반환한다.

전체적인 통신 흐름은 다음과 같다.

  • WS 는 HTTP 요청을 받아 정적 리소스를 처리하거나, 요청을 WAS에 전달한다.
  • WAS는 Servlet Container를 통해 요청을 처리하고, 처리 결과를 Web Server를 거쳐 클라이언트에 반환한다.

Image


Servlet의 생명주기

위 그림을 보면 알 수 있듯, Servlet은 다음과 같은 생명 주기를 가진다.

1) 초기화 단계:

  • 최초 요청이 오면 Servlet 클래스(HelloServlet.class)가 로딩된다.
  • web.xml 설정을 참고하여 매핑할 Servlet을 확인한다.
  • 해당 Servlet 인스턴스가 있다면 사용하고, 없다면 init()으로 초기화한다.

2) 서비스 단계:

  • Servlet Container는 각 요청마다 새로운 스레드를 생성한다.
  • 새로운 Request/Response 객체를 생성하여 service() 메서드를 실행한다.
  • service()는 요청 방식(GET, POST 등)에 따라 적절한 do메서드를 호출한다.
  • 요청 처리가 완료되면 해당 Request/Response 객체는 소멸된다.

3) 소멸 단계:

  • 서버 종료나 애플리케이션 재배포 시에만 destroy()가 호출된다.
  • Servlet이 사용하던 리소스를 정리한다.


정리하자면,

  • Tomcat은 Servlet 객체를 한 번 생성하면 메모리에 유지하고, 요청이 올 때마다 해당 객체의 service() 메서드를 호출한다.
  • 따라서, init() 메서드는 최초 요청 시(또는 설정에 따라 애플리케이션 시작 시) 단 한 번 실행된다.
  • 또 종료되기 전이나 reload 전에 destroy()를 호출하여 매번 객체가 생성되는 것을 방지한다.

(🤔 궁금증) servlet은 왜 생성만 하고 제거는 안하지? 만약 사용이 끝날 때마다 제거한다면 다음과 같은 문제가 있다.

  • 모든 요청에 대해 servlet을 생성하고 소멸하는 것은 OS 와 JVM 에게 필요없는 부하를 일으킨다.
    • JVM의 Garbage Collection메모리 할당/해제를 빈번하게 유발
  • 동시에 다수의 요청이 들어올 경우 CPU 또는 메모리 리소스 소모에 대한 제한이 어렵다. 결국 순간적으로 서버가 다운되거나 동시 처리에 문제가 생길 수 있다.
    • (참고로 Tomcat 3.2 이전 버전은 실제 매번 생성과 제거를 했다.)

(✨해결) 해답은 Servlet의 특징과 관리 방식에 있다.

  • 수많은 클라이언트의 요청을 동시에 처리해야 하는 WAS의 특성상, Servlet은 싱글톤 패턴으로 관리하게 되었다.
    • 한 번 생성된 Servlet 인스턴스는 메모리에 계속 유지된다.
    • 모든 요청은 동일한 Servlet 인스턴스를 공유한다.
    • init()은 최초 요청 시 한 번만 실행된다.
    • destroy()는 서버 종료나 재배포 시에만 실행된다.
  • (Tomcat 3.2 부터는 디폴트로 싱글톤 패턴 도입Thread Pool 을 사용)


Servlet 처리 구조의 발전

기존 Servlet 처리 방식

Tomcat은 다음과 같은 역할을 한다고 하였다.

  • Servlet Container로서 HTTP 요청과 응답을 처리하는 환경 제공
  • Servlet 객체의 생성, 초기화, 요청 처리, 소멸 등 생명주기를 관리하는 역할

(🤔 문제점) Servlet은 요청마다 개별적으로 처리 로직을 구현해야 하므로,
비슷한 요청을 처리하는 여러 Servlet에서 중복된 코드가 발생할 수 있다.

Image

// 기존 서블릿 방식 - 각 URL마다 서블릿을 따로 만들어야 했습니다
public class UserServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) {
        // 로깅 처리
        // 인증 처리
        // 공통 에러 처리
        // 실제 비즈니스 로직
    }
}

public class ProductServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) {
        // 위와 동일한 로깅, 인증, 에러 처리 코드가 반복됨
        // 실제 비즈니스 로직
    }
}

이러한 방식은 다음과 같은 문제가 있다.

  1. 하나의 서블릿 클래스에 너무 많은 책임이 집중된다.
  2. URL 매핑, 파라미터 처리, 뷰 선택 등을 모두 수동으로 처리해야 함
  3. 모든 서블릿에서 공통 로직(로깅, 인증, 에러 처리 등)이 반복됨
  4. 각각의 서블릿을 web.xml에 일일이 등록해야 함
    (web.xml : 웹 애플리케이션의 구성 정보를 제공)
  • 과거 web.xml은 다음과 같은 중요한 정보들을 담고 있었다.
    • 어떤 서블릿이 존재하는지
    • 각 서블릿이 어떤 URL 패턴을 처리하는지
    • 어떤 필터들이 요청을 처리하는지
    • 웹 애플리케이션의 시작과 종료 시 필요한 리스너는 무엇인지

⇒ 각 서블릿(요청을 처리하는 클래스)에 대한 정보를 등록해야 한다는 것이다… 😢


DispatcherServlet - Spring MVC : 공통 로직의 중앙 집중화

(✨ 해결책) 공통 로직의 중앙 집중화

Spring Framework는 기존 서블릿 스펙의 장점을 활용하면서도 그 한계를 개선하는 방향으로 발전했다.

DispatcherServlet이라는 Front Controller를 사용해 공통 로직을 처리하고 요청에 맞는 컨트롤러를 호출하는 특수한 Servlet을 만들어 새로운 구조를 도입했다.

  • 참고로 DispatcherServletServlet이다.
    • (이쯤에서 Spring에서의 Controller의 역할이 아구 맞게 떠오른다.)

Image

Spring MVC는 이러한 문제를 DispatcherServlet이라는 하나의 Front Controller로 해결했다.

  • 이제 우리가 아는 친숙한 코드가 나온다….
// Spring MVC 방식
@Controller  // 더 이상 HttpServlet을 상속할 필요가 없음
public class UserController {
    @GetMapping("/users")
    public String handleUsers() {
        // 비즈니스 로직에만 집중
        return "userList";
    }
}

@Controller
public class ProductController {
    @GetMapping("/products")
    public String handleProducts() {
        // 비즈니스 로직에만 집중
        return "productList";
    }
}

DispatcherServlet은 다음과 같은 일을 한다.

  1. DispatcherServlet은 웹 애플리케이션에 들어오는 모든 HTTP 요청을 먼저 받는다.
  2. 요청을 어떤 컨트롤러가 처리할지 결정하고, 해당 컨트롤러로 요청을 디스패치(위임) 한다.
  3. 요청에 대해 공통 처리(예: 인증, 로깅 등)를 먼저 한 후, 적절한 컨트롤러로 요청을 전달한다.
  4. DispatcherServlet 덕분에, 이제 다른 컨트롤러는 굳이 서블릿을 상속할 필요가 없다.

4번에 대해 추가 설명을 하자면, Spring MVC에서는 DispatcherServlet만 서블릿으로 등록하고, 실제 비즈니스 로직을 처리하는 컨트롤러서블릿이 아니어도 된다는 뜻이다.

  • Spring MVC에서는 DispatcherServlet만 서블릿으로 등록하고, 실제 비즈니스 로직을 처리하는 컨트롤러서블릿이 아니어도 된다는 뜻이다.
  • Spring MVC의 컨트롤러는 단순한 자바 클래스로, @Controller 어노테이션을 붙인 클래스를 만들어 요청을 처리할 수 있다.
  • 이때, 컨트롤러 클래스는 Servlet을 상속하지 않아도 된다.
  • DispatcherServlet이 모든 요청을 처리하고, 요청을 컨트롤러로 전달하기 때문에, 실제로는 서블릿이 아닌 컨트롤러Servlet 객체를 상속하지 않아도 된다는 뜻이다.

이렇듯 Spring 프레임워크는 Front Controller를 제공해주고 이를 Dispather Servlet 이라 부른다.

Image


Servlet과 DispatcherServlet의 역할 분담의 의의

Servlet 표준에서 중요한 점은 Servlet이 전체 데이터 처리 과정에서 특정한 역할만 수행한다는 것이다.

전통적인 서블릿 방식에서는 HTTP 요청/응답 처리부터 비즈니스 로직까지 모든 것을 책임져야했다.

변화한 Spring MVC 구조에는 다음과 같은 장점이 있다.

1) 관심사의 분리(Separation of Concerns)

  1. DispatcherServlet이 모든 HTTP 요청을 먼저 받아서 적절한 Controller로 라우팅하는 역할을 담당하게 되었다.
  2. 이로 인해 Controller는 비즈니스 로직에만 집중할 수 있게 되었다.

2) POJO(Plain Old Java Object) 기반 개발

  1. Controller가 더 이상 HttpServlet을 상속받지 않아도 된다는 것은 매우 중요한 의미를 가진다.
  2. 이는 일반적인 자바 클래스로 웹 개발이 가능해졌다는 뜻이다.

3) 각 엔드포인트가 명확하게 분리된다.


⇒ Spring Boot는 이러한 통합을 더욱 단순화했다.

개발자는 복잡한 설정 없이도 Tomcat과 Spring이 자연스럽게 협력하는 환경에서 개발할 수 있게 되었다.

web.xml 없이도 자동 구성을 통해 모든 것이 가능해졌다.

@SpringBootApplication 어노테이션은 다음 3가지 핵심 어노테이션의 조합이다.

  1. @SpringBootConfiguration
  2. @EnableAutoConfiguration [자동 구성]
  3. @ComponentScan
@SpringBootApplication
public class MyApplication {
    public static void main(String[] args) {
            // Spring Boot가 내장 Tomcat을 자동으로 구성하고 시작
        SpringApplication.run(MyApplication.class, args);
    }
}

즉, 현대에 와서 DispatcherServlet이 서블릿 역할을 하면서 요청을 Controller에 전달하고, Controller서비스를 호출하여 실제 비즈니스 로직을 처리하게 하는 방식으로 발전하였다.

  • 단지 DispatcherServlet은 요청을 받아서 적절한 컨트롤러에게 넘겨주는 역할을 한다.

지금까지 살펴본 WAS의 발전 과정을 보면 다음과 같다.

  1. WAS는 동적 컨텐츠 처리를 위한 웹 애플리케이션 서버로 분리되며 시작됐다.
  2. Tomcat은 Servlet Container로서 Java 기반 웹 애플리케이션을 실행시킨다.
  3. 초기 Servlet 방식의 한계(중복 코드, 복잡한 설정)를 Spring MVC가 해결했다.
  4. DispatcherServlet의 도입으로 공통 로직 처리와 요청 위임이 체계화되었다.

결국 DispatcherServlet이 모든 요청을 받아 적절한 Controller에 위임하므로 Controller는 순수 비즈니스 로직에만 집중할 수 있게 되었다.

  • 개발자는 반복적인 설정 없이 핵심 기능 개발에 집중 가능!

이처럼 WAS와 Servlet, 그리고 Spring MVC로 이어지는 발전 과정은 더 나은 웹 개발 환경을 만들어왔다.

더 나아가, 웹 애플리케이션 아키텍처는 계속해서 진화하고 있다.

  • Spring WebFlux를 통한 리액티브 프로그래밍 도입
  • 클라우드 네이티브 환경 지원
  • 마이크로서비스 아키텍처로의 전환
  • 서버리스 아키텍처의 등장