[Spring] 서블릿(Servlet)과 스프링(Spring), 서블릿 필터(Filter) 총정리

유알·2022년 12월 30일
3

[Spring]

목록 보기
7/17

0. 들어가면서

오라클은 2017년 자바EE 8 릴리즈를 마지막으로 오픈소스 SW를 지원하는 비영리 단체인 이클립스 재단에 자바EE 프로젝트를 이관했습니다. 썬 마이크로시스템즈를 인수한 오라클이 사실상 자바EE의 수익화에 실패하면서 기술 주도권을 포기한 것으로 판단됩니다.

오라클이 자바EE 프로젝트는 이관했지만 자바 상표권은 여전히 보유하고 있기 때문에 자바 네임스페이스 사용에 제약이 있었습니다. 이러한 이유로 자카르타EE에서는 자바 네임스페이스가 Jakarta로, API 패키지명은 javax. 에서 Jakarta. 로 변경되었습니다.

1. 서블릿(Servlet)

자바 서블릿(Java Servlet)은 자바를 사용하여 웹페이지를 동적으로 생성하는 서버측 프로그램을 말하는데요.

쉽게 말하면 클라이언트가 서버에 요청을 보낼 때와 응답을 받을때 필요한 HTTP 작업을 도와주는 녀석이 서블릿 입니다.

아래의 예시를 보겠습니다.

<form action="/save" method = "post">
    <input type = "text" name="username" />
    <input type = "text" name="password" />
    <button type = "submit">전송</button>
</form>

회원의 이름과 비밀번호를 받아서 전송할 수 있는 로직을 수행하는데요.

이 form 을 이용하여 클라이언트가 HTTP Request를 보내면 어떻게 될까요?

  1. 웹브라우저가 요청 HTTP를 만듭니다.
POST /save HTTP/1.1
Host: localhost:8080
Content-Type: application/x-www-form-urlencoded

username = "sunghyeon"&password="1234"
  1. 그리고 서버는 웹브라우저가 만든 요청 메시지를 다음과 같은 과정으로 처리해야되는데요.
  • 서버 TCP/IP 연결 대기, 소켓 연결
  • HTTP 요청 메시지를 파싱해서 읽기
  • POST 방식, /save URL 인지
  • Content-Type 확인
  • HTTP 메시지 바디 내용 파싱
  • 저장 프로세스 실행
    - 비즈니스 로직 실행 → 데이터베이스에 저장 요청
  • HTTP 응답 메시지 생성
  • TCP/IP에 응답 전달, 소켓 종료

그런데 우리가 개발을 할때는 저 비지니스 로직 실행을 제외한 나머지를 일일히 처리한 적이 없다는 것을 알 수 있습니다.

바로 서블릿이 대신 처리해 주기 때문이죠

서블릿의 생성

서블릿은 추상 클래스 HttpServlet을 extend 해서 만들어집니다.
HttpServlet doc 이 문서를 보는것을 추천한다. 메서드 종류를 보면 직관적이라 이해가 갈것이다.
이 HttpServlet에는 생명주기 관리를 위한 init(), service(), destory()메서드가 있고,
각 메서드의 요청을 처리하기 위한 doGet(), doPost(), doPut(), doDelete() 등의 메서드가 있다.
서블릿을 관리하는 서블릿 컨테이너가 service 메서드를 호출하면 내부 로직에 따라 알맞은 메서드로 연결지어 처리한다.

서블릿의 동작과정

  1. 사용자가 URL을 클릭하면 HTTP Request를 서블릿 컨테이너로 전송한다.
  2. HTTP Request를 전달 받은 서블릿 컨테이너HttpServletRequest, HttpServletResponse 두 객체를 생성한다.
  3. web.xml에는 어떤 URL에 어떤 서블릿 컨테이너에 mapping할지 쓰여있고 이에 따라 서블릿으로 이를 전달해준다. @WebServlet어노테이션을 이용해서도 매핑을 할 수 있다.
  4. 해당 서블릿에서 service 메서드를 호출하고 클라이언트의 요청 종류(Get,Post..)에 따라 doGet, doPost ... 를 호출한다.
  5. doGet, doPost ... 메서드는 동적으로 페이지를 생성한 후 HttpServletResponse 에 응답을 보낸다.
  6. 응답이 끝나면 HttpServletRequest, HttpServletResponse 두 객체를 소멸시킨다.
    서블릿 컨테이너는 서블릿 객체들을 관리하는 컨테이너인데 아래 나올 것이다.

2. 서블릿 컨테이너(Servlet Container)

서블릿 객체들을 관리하는 컨테이너인 서블릿 컨테이너가 등장한다.
톰캣처럼 서블릿을 지원하는 WAS를 서블릿 컨테이너라고 한다.

서블릿 컨테이너의 역할

웹 서버와의 통신지원

서블릿과 웹서버가 쉽게 통신할 수 있게 해준다.

서블릿 생명주기 관리

서블릿 컨테이너는 서블릿의 탄생과 죽음을 관리한다.
요청이 들어왔을 때 서블릿이 로드되어있지 않으면, init() 메서드를 호출하여 서블릿을 인스턴스화 한다.
그리고 서블릿의 service()를 호출하여 처리한다.
이후에 서블릿이 더이상 요청을 받지 않을 때 destroy() 메서드를 호출하여 서블릿을 제거한다.

멀티쓰레드 지원 및 관리

서블릿 컨테이너에서는 쓰레드를 만들어 서블릿 객체를 호출하도록 한다.
서블릿 컨테이너는 여러 클라이언트가 서버에 요청을 보내게 되면 여러개의 쓰레드를 생성하고 각 쓰레드는 서블릿 객체를 호출하여 작업을 처리한다.

하지만 모든 요청마다 쓰레드를 생성하게 되면 동시 요청은 처리할 수는 있지만 쓰레드 생성 비용과 쓰레드의 컨텍스트 스위칭 비용이 발생할 수 있다.
그리고 너무 많은 요청이 들어오면 쓰레드를 무한정 생성해야 하기 때문에 CPU의 한계를 넘어서서 서버가 다운될 수 있는 상황까지 초래하게 된다.

그래서 WAS는 아래처럼 쓰레드 풀이라는 개념을 사용한다.

쓰레드 풀은 처음에 일정한 수의 쓰레드를 미리 생성해놓고 클라이언트의 요청이 오면 쓰레드 풀 안에 있는 쓰레드를 할당하고 요청을 처리하고 나면 해당 쓰레드를 다시 반납 받는다.

톰캣은 최대 200개의 쓰레드를 쓰레드 풀에 생성한다.
이로 인해 쓰레드 생성과 종료 비용이 절약되고, 응답 시간이 빨라질 수 있다.

쓰레드가 모두 사용 중인 상황에서는 기다리는 요청에 대해 특정 숫자만큼만 대기하도록 설정할 수 있도록 만들수도 있죠.

결국 핵심은 WAS가 멀티 쓰레드 지원을 통한 동시 요청 처리를 해준다는 것이다.

3. 프론트 컨트롤러 패턴 (Front Controller Pattern)

각 요청 URL마다 별개의 서블릿을 만들면 어떤 문제가 있을까?
새로운 요청이 생길때 마다 새롭게 HttpServlet을 상속한 클래스를 정의해야하고, Web.xml에 매핑 내용을 추가해줘야 한다.
이 과정에서 많은 코드의 중복이 발생한다.

이를 해결하기 위해 웹 어플리케이션에서 사용하는 소프트웨어 디자인 패턴이 프론트 컨트롤러 패턴이다.
프론트 컨트롤러(Front Controller)는 이름 그대로 앞 단에서 서블릿 하나로 클라이언트의 모든 요청을 받는다. 그리고 프론트 컨트롤러가 요청에 맞는 컨트롤러를 찾아서 호출하는 패턴이다.
스프링 MVC의 DispatcherServlet이 FrontController 패턴으로 구현 되어있다.

이를 통해 프론트 컨트롤러는 각 컨트롤러에서 부담해야 하는 공통적인 로직을 혼자 처리해줄 수 있게 되었고, 나머지 컨트롤러들은 불필요한 서블릿을 사용하지 않아도 되는 장점이 생기게 되었습니다.

간단한 과정

  1. 클라이언트의 HTTP 요청은 FrontController에서 받습니다.

  2. FrontController는 requestServlet에서 파라미터를 파싱하고, 사용자 요청에 맞는 컨트롤러를 호출하면서 paramMap과 model 객체를 넘겨줍니다.

  3. 사용자 요청에 해당하는 컨트롤러는 paramMap에서 데이터를 받아서 비즈니스 로직을 수행하고 model 객체에 데이터를 다시 담아서 넘겨줍니다. 이 때, 클라이언트에게 돌려줄 ViewName을 같이 보내줍니다. 이 때, ViewName은 Prefix나 Suffix를 제외한 부분만을 리턴합니다.

  4. viewResolver는 컨트롤러가 반환한 ViewName을 정확한 원본 ViewName(MyView)로 만들어줍니다.

  5. FrontController는 MyView 객체를 통해 HttpServletRequest에 model에 대한 데이터를 담고 dispatcher를 통해 view를 Forwarding합니다.

4. Spring MVC의 DispatcherServlet

Spring MVC의 DispatcherServlet도 프론트 컨트롤러 패턴을 따르고 있다.
그래서 아래 사진과 같이 서블릿을 하나만 두고 모든 요청을 다 받은 후 그 요청을 처리하기 위해 다른 클래스의 메서드를 호출하는 구조로 되어있다.

Spring Context과 Servlet Context


여기서 굉장히 많이 헷갈렸는데, 이해를 위해 아주아주 쉽게 생각하면(완전히 정확한 설명은 아니다)

  • Servlet Container : Tomcat
  • DispatcherServlet : Servlet Container에 등록된 Servlet
  • Servlet WebApplicationContext : Servlet Context
  • Root WebApplication : Application Context (스프링의 IOC Container)

Servlet Container

위에서 설명했듯이 Servlet을 관리하는 Container. 대표적으로 Tomcat이 있다.

DispatcherServlet

위에서 설명했듯이 Front Controller Pattern에 따라 만들어진 Servlet
모든 요청을 받아 적절한 class에 위임한다.

Servlet WebApplication Context

  • Servlet Context로 보면 된다.
  • Servlet Containerservlet을 등록하면 해당 servlet이 같은 하나의 작은 컨테이너 역할을 하는 객체
  • 즉 여기서는 DispatcherServletServlet이므로 내부에 Servlet Container를 갖고 있다.
  • Spring을 사용할 경우, 스프링 컨테이너(Apllication Context)를 부모 Context로 사용한다. (상속받는다)
  • Bean을 찾는 순서가 Servlet에서 ServletContext를 확인한 후 부모인 AppicationContext(Spring Container)를 확인한다.
  • 그렇기 때문에 만약 ApplicationContext와 Servlet Context에 같은 id로 된 Bean이 있으면 ServletContext에 있는 Bean을 우선 사용한다.

Application Context(Root WebApplicationContext)

  • Servlet Context의 부모 (스프링 사용시)
  • BeanFactory를 사용받는 Context
  • 여러 Servlet에서 공통으로 사용할 Bean을 등록하는 Context.
    이게 무슨말이냐면, 이 객체의 상속을 받는 Servlet WebApplicationContext가 여러개가 있을 수 있다.
    이때 이 공통의 부모로 ApplicationContext를 갖으면 여러 Servlet에서 공통으로 사용할 Bean을 등록할 수 있다.
  • @Transactional으로 트랜잭션을 이용해야할 때 ApplicationContext에 있는 Service에서만 트랜잭션이 정상 작동합니다.

5. Servlet Filter

사실 따로 주제를 빼기는 애매하지만 내가 이 내용을 공부하게 된 계기가 Spring Security에 DelegatingFilter 가 이해가 되지 않아서 공부하기 시작했기 때문에 한번 확실히 적는다.

  • Servlet 실행 전, 후에 어떤 작업을 하고자 할 때 Servlet Filter를 사용한다.
  • Servlet Container에 등록한다.
  • jakarta.servlet.Filter 인터페이스의 구현체 (javax.servlet.Filter)

Filter는 스프링 빈 주입이 불가능하다??

몇몇 포스팅과 예전 책들을 보면 필터(Filter)는 서블릿 기술이라서 Spring의 빈으로 등록할 수 없다는 내용이 나온다.
상식적으로 스프링 컨테이너보다 큰 범위인 서블릿 컨테이너의 필터가 스프링 컨테이너에 의해 관리된다는 것은 이상하다.

하지만 테스트를 해보면 필터 역시 스프링 빈으로 등록이 가능하며, 빈을 주입 받을 수도 있다.
이러한 잘못된 설명이 나오는 이유는 과거에 실제로 필터(Filter)가 스프링 컨테이너에 의해 관리되지 않았기 때문이다. 그래서 빈으로 등록할 수도 빈을 주입받을 수도 없었다.

하지만 DelegatingFilteProxy 가 ㄷ3ㅡㅇ장하면서 이제 이러한 설명은 더 이상 유효하지 않는데, 해당 내용을 살펴보자

DelegatingFilterProxy와 SpringBoot의 등장

DelegatingFilterProxy의 등장 이전

필터는 서블릿이 제공하는 기술이브로 서블릿 컨테이너에 의해 생성 되며 서블릿 컨테이너에 등록이 된다.
그렇기 때문에 스프링의 빈으로 등록할 수도, 주입 받을 수도 없었다.
하지만 필터에서도 DI와 같은 스프링 기술을 필요로 하는 상황이 발생하면서, 스프링 개발자는 스프링 빈을 주입 받을 수 있도록 대안을 마련했는데, 그것이 바로 DelegatingFilterProxy이다.

DelegatingFilterProxy의 등장 이후

Spring 1.2 부터 추가되면서 필터가 스프링에서 관리가 가능해졌다.
DelegatingFilterProxy는 서블릿 컨테이너에서 관리되는 프록시용 필터로써 우리가 만든 필터를 가지고 있다.
우리가 만든 필터는 스프링 컨테이너의 빈으로 등록되는데, 요청이 오면 DelegationgFilterProxy가 요청을 받아서 우리가 만든 필터(Spring Bean)에게 요청을 위임한다.
1. Filter 구현체가 스프링 빈에 등록됨
2. ServletContext가 Filter 구현체를 갖는 DelgatingFilterProxy를 생성
3. ServletContext가 DelegationgFilterProxy를 서블릿 컨테이너에 필터로 등록
4. 요청이 오면 DelegatingFilterProxy 가 필터 구현체에게 요청을 위임하여 필터 처리

이 그림은 SpringSecurity 공식 레퍼런스 문서에서 가져온 것이다.

Spring에서

필터를 등록하려면 직접 서블릿 컨텍스트에 addFilter로 추가해주어야 했다.

public class MyWebApplicationInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

   public void onStartup(ServletContext servletContext) throws ServletException {
      super.onStartup(servletContext);
      servletContext.addFilter("myFilter", DelegatingFilterProxy.class);
   }
}

SpringBoot에서

SpringBoot에서는 DelegatingFilterProxy 조차 필요가 없다.
왜냐하면 SpringBoot가 내장 웹서버를 지원하면서 톰캣과 같은 서블릿 컨테이너까지 SpringBoot가 제어 가능하기 때문이다.😮
SpringBoot가 서블릿 필터의 구현체 빈을 찾으면 DelegatingFilterProxy없이 바로 필터 체인(FilterChain)에 필터를 등록해주기 때문이다. (충격)

요약

  • 필터에서 다른 스프링 빈의 주입이 불가능했음
  • DelegatingFilterProxy의 등장으로 빈 주입이 가능해짐
  • SpringBoot에서 웹서버를 직접 관리하면서 DelegatingFilterProxy 조차 필요 없게 됨(지림)
profile
더 좋은 구조를 고민하는 개발자 입니다

0개의 댓글