Notice
Recent Posts
Recent Comments
Link
«   2025/03   »
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 31
Archives
Today
Total
관리 메뉴

장미정원

✨ Servlet, Dispatcher Servlet - 서블릿과 디스패처 서블릿 본문

Back-end

✨ Servlet, Dispatcher Servlet - 서블릿과 디스패처 서블릿

신희성 2024. 2. 21. 18:54

서블릿이란?

클라이언트의 요청을 처리하고 그 결과를 반환하는 프로그램이다.

 

서블릿의 등장 배경

처음 웹 페이지는 클라이언트의 요청에 대해 정적인 페이지로만 응답할 수 있었다. 

그래서 동적인 웹 페이지를 개발하기 위해 웹 서버에 프로그램을 붙혀 동적인 페이지를 개발하였다.

서블릿도 동적인 페이지를 만들기 위해 웹 서버에 붙히는 프로그램 중 하나이다.

 

서블릿을 사용하면 얻는 이점

아래는 HTTP 요청과 응답 메시지이다.

요청 HTTP 메시지 예시
GET /api/roses/100 HTTP/1.1
Host: rosegarden.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.99 Safari/537.36
Accept: application/json
Accept-Encoding: gzip
응답 HTTP 메시지 예시
HTTP/1.1 200 Ok
Date: Sat, 20 Feb 2024 12:00:00 GMT
Server: Apache/2.4.38 (Unix)
Location: /api/roses/100
Content-Type: application/json
Content-Length: 56
Cache-Control: no-cache, no-store, must-revalidate

{
    "id" : 100,
    "name" : "rose"
    "price" : 10000
}

 

만약 개발자가 이러한 텍스트 형식인 HTTP 메시지를 직접 처리해서 응답 메시지같은 텍스트 형식의 응답을 만들어야한다면 개발이 엄청나게 힘들 것이다.

 

HttpServletRequest가 제공하는 메서드의 일부

 

위 사진은 서블릿으로 요청을 처리할 때 사용할 수 있는 HttpServletRequest가 제공하는 메서드의 일부이다.

httpServletRequest.getHeader("Accept-Encoding");

 

HTTP 메시지 텍스트를 직접 파싱해서 처리하는 것HttpServletRequest가 제공하는 메서드를 사용하는 방법 중 간편한 방법을 고른다면

 

당연히 HttpServletRequest가 제공하는 메서드를 사용하는 것이 더 간편할 것이다.

 

서블릿을 사용하면서 서블릿의 구현 규칙을 잘 준수해준다면 HTTP 요청 메시지 정보를 쉽게 사용할 수 있고 처리 결과를 쉽게 응답 메시지로 변환할 수 있다. 이렇게 된다면 개발자는 가장 중요한 비즈니스 로직 개발에 집중할 수 있게 된다.

 

Sevlet

 

위 사진은 HttpServlet을 상속받아 만든 서블릿이다. 서블릿은 이렇게 생겼다.

 

init() 메서드는 서블릿이 생성될 때 실행되는 메서드이다.

destory() 메서드는 서블릿이 소멸될 때 실행되는 메서드이다.

service() 메서드는 요청을 처리할 때 실행되는 메서드이다.

 

HttpServlet 객체의 service 메서드 로직 중 일부

 

HttpServlet의 service 부분을 확인해보면 HTTP 요청이 들어오면 해당 요청의 메서드 기준으로 분기하여

doPost(), doDelete() 같은 메서드를 호출하는 것을 볼 수 있다.

 

 

만약 요청을 핸들링 하고 싶다면 해당 doXXX() 메서드를 오버라이딩 하여 로직을 작성하면 된다.

 

이렇게되면 해당 서블릿을 처리할 URL만 매핑 해주면 요청이 왔을 때 URL과 매핑된 서블릿 객체의 오버라이딩한 메서드가 호출되어 로직을 처리하게 된다.

 

서블릿 컨테이너

서블릿을 담고 관리해주는 컨테이너이고 서블릿의 생명주기를 관리해주는 객체이다. 

쉽게 말해서 서블릿을 생성하고 필요한 순간에 호출하고 소멸시키는 일을 담당한다.

 

서블릿이 호출되는 과정

1. Servlet Request, Servlet Response 객체를 생성한다.

더보기

ServletRequest, ServletResponse 객체는 HTTP 요청과 응답에 대한 메타데이터를 갖고있다.

 

2. 서블릿 컨테이너는 설정파일을 참고하여 해당 요청과 매핑된 서블릿을 찾는다.

더보기

-> 설정 파일에 URL과 서블릿 객체를 매핑시켜둔다. (web.xml)

 

3. 해당 서블릿 인스턴스가 있는지 확인하고 없다면 생성(init())하고 있다면 생성되어있는 서블릿 인스턴스를 호출한다.

싱글톤 패턴이 적용되어있다.

더보기

싱글톤 패턴은 클래스의 인스턴스가 딱 1개만 생성되도록 보장하는 디자인 패턴이다.

단 하나의 인스턴스로 여러 클라이언트가 사용하기 때문에 불필요한 인스턴스 생성을 막아 리소스 낭비를 막는다.

 

4. 서블릿 컨테이너에 스레드를 생성하고 만들어뒀던 Servlet Request, Servlet Response로 service 메서드를 실행시킨다.

 

5. 응답을 처리하였다면 distory() 메서드를 실행하여 Servlet Request, Servlet Response 객체를 소멸시킨다.

 

문제점

  • 모든 서블릿을 URL 매핑을 위해 web.xml에 매핑할 URL, 서블릿 객체를 모두 등록해주어야 한다.
  • 핸들러들의 공통 로직이 매번 중복된다.

 

이러한 문제점들은 프론트 컨트롤러 패턴을 사용하면 해결할 수 있다.

 

위 사진처럼 기존의 서블릿을 사용하는 방식은 각각의 서블릿과 URL의 매핑 정보를 모두 web.xml에 등록시켜주어야 하고 클라이언트가 매핑된 URL로 요청을 보내면 각각의 서블릿 인스턴스를 호출하여 요청을 처리하였다.

이렇게 되면 핸들러에 대한 공통로직이 중복된다는 문제가 생긴다.

 

프론트 컨트롤러 패턴을 도입한다면 클라이언트의 모든 요청을 프론트 컨트롤러가 받아서 처리하는 방식이 된다.

이렇게 된다면 하나의 서블릿(프론트 컨트롤러)에 모든 URL요청을 매핑하고 공통로직을 한번에 처리할 수 있게 되어 효율적으로 요청을 처리하게 된다.

 

 

디스패처 서블릿

모든 HTTP 요청을 제일 먼저 받아서 요청 정보에 맞는 적절한 컨트롤러에 처리 작업을 위임해주는 프론트 컨트롤러이다. 클라이언트로부터 요청이 온다면 서블릿 컨테이너가 요청을 받게 된다. 그리고 이 요청은 프론트 컨트롤러인 디스패처 서블릿이 가장 먼저 받게 된다.

디스패처 서블릿은 공통로직을 먼저 처리한 후 해당 요청을 처리할 적합한 컨트롤러를 찾아 처리 작업을 위임한다.

 

디스패처 서블릿의 장점

Spring Web MVC는 기존에 서블릿 사용 방식에서는 모든 서블릿의 매핑 정보를 web.xml에 등록 시켜주어야 했고 각각의 핸들러에 대한 공통로직이 중복되는 비효율적인 방식이였지만

디스패처 서블릿을 적용하면서 모든 요청을 제일 먼저 받아 핸들링 해주고 공통로직을 한번에 처리하면서 web.xml의 역할을 줄여주며 효율적이다.

디스패처 서블릿 덕분에 요청을 처리할 컨트롤러만 구현한다면 디스패처 서블릿이 요청이 오면 핸들링하여 요청 정보에 적합한 컨트롤러를 찾아 위임해주는 방식이되었다.

 

정적 자원의 처리

디스패처 서블릿이 요청을 핸들링하여 컨트롤러에게 위임하는 방식은 효율적으로 보인다.

하지만 디스패처 서블릿이 모든 요청을 가로채 처리하다보니 이미지, HTML, CSS, Javascript 와 같은 정적 리소스에 대한 요청까지 모두 가로채버려서 정적 리소스를 불러오지 못하는 상황이 생긴다.

이 문제를 해결하기 위한 방법은 대표적으로 2가지가 있다.

 

 

1. 정적 리소스 요청과 컨트롤러가 위임할 요청을 분리한다.

클라이언트의 요청을 2가지로 분리하여 둘의 요청을 구분하는 방법이다.

  • /apps 의 URL로 요청한다면 디스패처 서블릿이 요청을 핸들링하여 컨트롤러에게 위임한다.
  • /resources 의 URL로 요청한다면 디스패처 서블릿이 핸들링 하지 않고 정적 리소스를 요청한다.

이렇게 URL을 분리하여 구분하는 방법은 두개의 요청을 분기하여 구분하는 과정에서 코드가 많이 더러워지고 모든 요청 URL 앞에 구분하는 문자를 적어주어야 하므로 직관적인 설계를 할 수 없다.

 

 

2. 요청을 위임할 컨트롤러를 찾고 없다면 정적 리소스 요청으로 처리한다.

두번째 방법은 디스패처 서블릿이 요청을 위임할 적절한 컨트롤러를 탐색한 후, 없다면 정적 리소스 경로를 탐색하여 리소스를 요청하는 방식이다. 

이렇게 영역을 분리한다면 계층적으로 효율적인 리소스 관리가 가능하고 애플리케이션 확장에도 용이하다.

 

 

디스패처 서블릿의 동작과정

  1. 클라이언트의 요청을 디스패처 서블릿이 받는다.
  2. 요청 정보를 바탕으로 요청을 위임할 적절한 컨트롤러를 찾는다.
  3. 요청을 컨트롤러로 위임할 핸들러 어댑터를 찾아서 전달한다.
  4. 핸들러 어댑터가 컨트롤러로 요청을 위임한다.
  5. 서비스 단에서 비즈니스 로직을 처리한다.
  6. 컨트롤러가 처리 결과를 반환한다.
  7. 핸들러 어뎁터가 반환값을 처리한다.
  8. 서버가 요청에 대한 응답을 클라이언트로 반환한다.

 

더 자세한 동작과정

 

 

1. 클라이언트의 요청을 디스패처 서블릿이 받는다.

HTTP 요청이 온다면 서블릿 컨텍스트(웹 컨텍스트)에서 필터들을 거쳐 스프링 컨텍스트에서 디스패처 서블릿이 가장 먼저 요청을 받게 된다.

 

 

2. 요청 정보를 바탕으로 요청을 위임할 적절한 컨트롤러를 찾는다.

디스패처 서블릿은 어떤 컨트롤러가 요청을 처리할 수 있는지 식별해야하는데, 해당 역할을 하는 것이 HandlerMapping이다.

 

흔히 컨트롤러를 구현하는 방식인 @Controller 어노테이션을 사용하여 컨트롤러를 구현하는 방식은 RequestMappingHandlerMapping 구현체가 처리한다. 이 방법은 @Controller로 작성된 모든 컨트롤러를 파싱하여 HashMap 형식으로 <요청 정보, 처리할 대상> 으로 관리한다.

처리할 대상은 HandlerMethod 객체로 요청에 매핑되는 컨트롤러 정보와 메서드 객체를 가지고 있다.

 

요청이 온다면 요청 정보를 비교해 HashMap에서 요청을 처리할 대상(HandlerMethod)을 찾은 후 HandlerExecutionChain에 감싸서 반환한다. HandlerExecutionChain으로 감싸는 이유는 컨트롤러로 위임하기 전에 처리해야하는 인터셉터 등을 포함하기 위함이다.

 

 

3. 요청을 컨트롤러로 위임할 핸들러 어댑터를 찾아서 전달한다.

디스패처 서블릿은 컨트롤러로 요청을 직접 위임해주는 것이 아니라 HandlerAdapter를 통해서 요청을 위임한다.

이 이유는 컨트롤러의 구현 방식이 다양하기 때문에 어댑터 패턴을 사용한 것이다.

 

과거에는 컨트롤러를 Controller 인터페이스를 구현하여 작성하였는데, 최근에는 어노테이션 기반으로 컨트롤러를 작성한다.

스프링은 HandlerAdapter라는 어댑터 인터페이스를 사용하여 컨트롤러의 구현 방식에 얽매이지 않고 컨트롤러에 요청을 위임할 수 있다.

 

 

4. 핸들러 어댑터가 컨트롤러로 요청을 위임한다.

핸들러 어댑터가 컨트롤러로 요청을 위임하기 전에 공통적인 전/후처리 과정이 필요하다.

대표적으로 인터셉터를 포함하여 요청시 @RequestParam, @RequestBody 등을 처리하기 위한 ArgumentResolver들과,

응답시에 ResponseEntity의 Body를 Json으로 직렬화 하는 등의 처리를 하는

ReturnValueHandler 등이 핸들러 어댑터에서 처리된다.

 

ArgumentResolver 등을 통해서 요청에 대한 파라미터가 준비가 된다면 리플렉션을 이용하여 컨트롤러로 요청을 위임한다.

요청을 처리할 대상인 HandlerMethod 객체에는 컨트롤러와 메서드 객체가 있으므로 리플렉션의 메서드 객체를 invoke 한다. 

 

더보기

-> 구체적이 클래스 타입은 알지 못해도 런타임에 메서드를 호출하거나 변수의 값을 변경할 수 있는 기술이다.

 

실제로는 HandlerMethod 객체에 컨트롤러의 빈 이름과 메서드, 빈 팩토리가 있어 빈 팩토리에서 컨트롤러 빈을 찾고 해당 컨트롤러 빈 객체에서 리플렉션을 사용한다.

 

 

5. 서비스 단에서 비즈니스 로직을 처리한다.

이후 컨트롤러는 서비스를 호출하고 개발자가 작성한 비즈니스 로직이 실행된다.

 

 

6. 컨트롤러가 처리 결과를 반환한다.

비즈니스 로직이 처리된 후에 컨트롤러가 결과 값을 반환한다.

  • 응답 데이터: ResponseEntity -> 반환
  • 응답 페이지: 스프링의 View 이름 -> 반환

요즘에는 RestAPI 방식을 많이 사용하기 때문에 주로 ResponseEntity를 반환한다.

 

 

7. 핸들러 어뎁터가 반환값을 처리한다.

HandlerAdapter는 컨트롤러에게 받은 반환값을 응답 처리기인 ReturnValueHandler가 후처리 후에 디스패처 서블릿에게 돌려준다.

만약 컨트롤러가 ResponseEntity를 반환한다면 HttpEntityMethodProcessor가 MessageConverter를 사용해 응답 객체를 직렬화하고 응답 HttpStatus를 설정한다.

만약 컨트롤러가 View 이름을 반환한다면 ViewResolver를 통해 View를 반환한다.

 

 

8. 서버가 요청에 대한 응답을 클라이언트로 반환한다.

디스패처 서블릿을 통해 반환되는 응답은 다시 필터들을 거쳐 클라이언트에게 반환된다.

 

 

마치며

서블릿과 디스패처 서블릿에 대해 정리하였다. 스프링으로 개발을 하다보면 자주 나오는 개념이기 때문에 개념과 동작과정에 대해 이해하고 사용하면 더 좋은 개발자가 될 수 있다고 생각한다.

 

 

참고자료

https://mangkyu.tistory.com/18

https://youtu.be/calGCwG_B4Y?si=I30jcDuPKN7E37jz

https://zzang9ha.tistory.com/449