백앤드 개발일지/웹, 백앤드

디스패처 서블릿의 흐름

giron 2022. 5. 18. 13:18
728x90

서블릿을 만들려면 HttpServlet 클래스를 상속받으면 되나?

- 서블릿 프로그래밍의 핵심은 Servlet 인터페이스를 이해해야 한다.

서블릿

웹 브라우저 <-> 웹 서버 <-> 웹 애플리케이션에서 웹서버와 웹 애플리케이션 사이에서 CGI규칙에 따라 데이터를 주고받는다.

자바로 만든 CGI 프로그램을 서블릿이라고 부른다.

서블릿 컨테이너(톰캣)

이런 서블릿의 생성, 실행, 소멸 등 생성 주기를 관리하는 프로그램을 서블릿 컨테이너라고 한다.

서블릿은 한 번 생성되면 웹 애플리케이션을 종료할 때까지 유지한다.

서블릿 컨테이너가 서블릿을 대신하여 CGI 규칙에 따라 웹 서버와 데이터를 주고받는다. 따라서 개발자는 더 이상 CGI규칙에 대해서 알 필요가 없다. 대신 서블릿 컨테이너와 서블릿 사이의 규칙을 알아야 한다.

서블릿과 서블릿 컨테이너 요청 처리

톰캣: 서블릿 컨테이너 or WAS

  1. 클라이언트로부터 요청이 들어오면 웹 컨테이너(톰캣)이 요청을 받는다.
  2. 로그인을 처리하는 서블릿을 개발자가 작성했으면, 컴파일 후 만들어진 클래스파일을 웹 컨테이너(WAS)가 서블릿 객체로 만들어논다.
    • 로그인을 처리하는 서블릿은 하나만 만들어 진다.
    • 로그아웃을 처리하는 서블릿은 하나만 만들어 진다.
    • 하지만 서블릿 객체는 싱글톤이 아니다. 내부가 싱글톤 구조가 아니다. 구조상 한 번만 만들 뿐
  3. 요청을 가지고 Request, Response 객체를 만든다.
  4. URL에 맞춰 적절한 서블릿을 찾아서 service()메서드를 실행한다. 파라미터로 Request, Response를 보내준다.
  5. 서블릿 객체는 하나지만 여러 스레드가 작동하므로 가능하다.
  6. response객체로 컨테이너에게 돌려주면 컨테이너는 클라이언트에게 준다.
  7. 서블릿 객체는 유지되지만, 쓰레드는 반납하고 Request, Response객체는 JVM에 반환한다.

디스페처 서블릿

서블릿 컨테이너의 한 종류로 HTTP 프로토콜로 들어오는 모든 요청을 가장 먼저 받아 적합한 컨트롤러에 위임해주는 프론트 컨트롤러(Front Controller)이다.

이를 통해 과거에는 모든 서블릿을 URL 매핑을 위해 web.xml에 모두 등록해주어야 했지만, 디스페처 서블릿이 해당 어플리케이션으로 들어오는 모든 요청을 핸들링해주고 공통 작업을 처리하면서 상당히 편리하게 이용할 수 있게 되었습니다. 우리는 컨트롤러를 구현해두기만 하면 디스패처 서블릿가 알아서 적합한 컨트롤러로 위임을 해주는 구조가 되었습니다.

 

[정적 자원 처리]

이렇게 되면 들어오는 모든 요청을 디스페처 서블릿이 처리하므로 정적인 자원에 대해서도 서블릿이 가로채는 현상이 있다. 따라서 애플리케이션에 대한 요청을 탐색하고 없으면 정적 자원에 대한 요청으로 처리하도록 구성했다.

디스페처 서블릿이 요청을 처리할 1)컨트롤러를 먼저 찾고, 요청에 대한 컨트롤러를 찾을 수 없는 경우에, 2)2차적으로 설정된 자원(Resource) 경로를 탐색하여 자원을 탐색하는 것입니다. 이렇게 영역을 분리하면 효율적인 리소스 관리를 지원할 뿐 아니라 추후에 확장을 용이하게 해준다는 장점이 있습니다.

디스페처 서블릿 플로우

예시
예시

  1. 디스페처 서블릿으로 요청이 들어온다.
  2. 디스페처 서블릿은 요청 정보를 보고 WebApplicationContext안에서 Handler Mapping빈 객체를 통해 위임할 컨트롤러를 찾는다.
    1. 애노테이션 기반인 @RequestMapping으로 적절한 핸들러가 있는지 찾기 
    2. BeanNameUrlHandlerMapping // 스프링 빈의 이름으로 핸들러를 찾기
  3. Handler Mapping이 핸들러(컨트롤러)를 찾으면 Handler Adapter에게 전달된다. 스프링에서 컨트롤러를 등록하는 방법이 3가지 정도가 있으므로 Adapter를 통해 적절한 컨트롤러매핑 방식을 찾는다. 이때, HandlerAdapter의 handle()메서드로 handler(컨트롤러)의 메서드를 수행하기 전에 인터셉터의 preHandler가 작동한다.
    1. 애노테이션 기반인 @Controller를 통해서 구현한다.
    2. HttpRequestHandler를 사용한다.
    3. Controller인터페이스를 사용한다.(2.5 버전까지만 사용하고 더이상 사용하지 않는다.)
  4. HandlerAdapter가 컨트롤러의 알맞은 메서드를 호출하고 반환값을 ModelAndView라는 객체로 디스페처서블릿에게 반환한다. * 그리고 HandlerAdapter가 컨트롤러에게 메서드를 호출하기 전에 아규먼트 리졸버가 작동한 후 넘어간다.
    1. 컨트롤러는 Root WebApplicationContext  Service, Repository… 등을 호출해가며 비즈니스 로직을 처리한다.
  5. handle() 메소드를 수행하고 나면, Interceptor에서 postHandle() 메소드를 호출한다. 이는 비즈니스 로직을 전부 수행하고 실행하는 메소드로, 반환 타입은 void이다.
  6. 이때, @Controller면 ModelAndView를 반환하고 @RestController면 null을 반환한다. 따라서, @RestController의 경우 이후 View에 Model이 렌더링되는 과정은 생략하고 바로 그 다음 단계를 진행한다.
  7. 마지막으로 디스페처 서블릿의 processDispatchResult() 메소드에서 render() 메소드를 호출한다. 해당 메소드의 반환 타입은 void이다.이 메소드 내부에서 resolveViewName() 메소드를 호출하여 View의 논리 이름을 물리 이름으로 변환한다. 마지막으로, View 객체의 render() 메소드를 호출하여 View에 Model 데이터를 렌더링한다.

Interceptor의 postHandle()뷰 렌더링 직전에 호출하므로 view를 수정할 수 있다. 

Interceptor의 afterComplention()메서드는 view가 렌더링 된 이후에 실행된다. 따라서 뷰 랜더링 후, 응답 객체 수정 혹은 뷰렌더링 이후의 예외를 처리할 때 사용할수 있다.

 

Handler Mapping

handlerMapping은 어떻게 동작하는지가 궁금했다. HandlerMapping을 파고 들다보면 MappingRegistry를 볼 수 있다.

class MappingRegistry {
  private final Map<T, MappingRegistration<T>> registry = new HashMap<>();

  private final Map<T, HandlerMethod> mappingLookup = new LinkedHashMap<>();

  private final MultiValueMap<String, T> urlLookup = new LinkedMultiValueMap<>();

  private final Map<String, List<HandlerMethod>> nameLookup = new ConcurrentHashMap<>();

  private final Map<HandlerMethod, CorsConfiguration> corsLookup = new ConcurrentHashMap<>();

  private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
  ...

여기서 LinkedMultiValueMap<>()은 하나의 Key에 여러개의 Value를 가질 수 있는 spring에서 만든 자료구조이다.

key : "/api/user/ 
value : [GET /api/user,POST /api/user]
위와 같은 형태로 정보를 저장해둔다.
 

3번의 preHandler작동 코드

// DispatcherServlet.java
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
    ...

    try {
        try {  
            if (!mappedHandler.applyPreHandle(processedRequest, response)) {
                return;
            }
        }
    }

    ...
}

boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
    for (int i = 0; i < this.interceptorList.size(); i++) {
        HandlerInterceptor interceptor = this.interceptorList.get(i);
        if (!interceptor.preHandle(request, response, this.handler)) {
            triggerAfterCompletion(request, response, null);
            return false;
        }
        this.interceptorIndex = i;
    }
    return true;
}

6번의 postHandler작동 코드

// DispatcherServlet.java
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
    ...

    try {
        try {  
            mappedHandler.applyPostHandle(processedRequest, response, mv);
        }
    }

    ...
}

void applyPostHandle(HttpServletRequest request, HttpServletResponse response, @Nullable ModelAndView mv)
        throws Exception {

    for (int i = this.interceptorList.size() - 1; i >= 0; i--) {
        HandlerInterceptor interceptor = this.interceptorList.get(i);
        interceptor.postHandle(request, response, this.handler, mv);
    }
}

Servlet 인터페이스에서 생명주기 메서드

  1. init()
    • 서블릿을 생성한 후 초기화 작업을 수행하기 위해 호출하는 메서드
    • ex) connection연결, 스토리지 서버 연결 등
  2. service()
    • 클라이언트가 요청할 때마다 호출되는 메서드 - 서블릿이 해야 할 일을 작성하는 곳
  3. destroy()
    • 서블릿 컨테이너가 종료되거나 웹 애플리케이션이 멈출 때, 또는 비활성화시킬 때.
    • ex) connection 해제, 데이터 저장 등

서블릿의 보조 메서드 : 서블릿 정보를 추출할 때 사용

1. getServletConfig(): servelteConfig 객체를 반환하며, 서블릿 이름, 초기 매개변수 값, 서블릿 환경정보를 얻을 수 있다.

init()이 호출될 때, 이 객체에 저장해둘 수 있다.

2. getServletInfo(): 서블릿을 작성한 사람에 대한 정보, 서블릿 버전, 권리 등을 담은 문자열 반환한다.

 

클라이언트로부터 요청이 들어오면 서블릿 컨테이너는 호출 규칙에 따라 서블릿의 메서드를 호출한다. 서블릿 호출 규칙은 javax.servlet.Servlet 인터페이스에 정의되어 있다. 따라서 서블릿을 만들 때는 반드시 Servlet인터페이스를 구현해야 한다.

 

JavaEE 사양 중에서 Servlet과 JSP 기술만 구현한 서버를 `서블릿 컨테이너`라고 한다. ex) Tomcat

GenericeServlet 추상 클래스는 Sevlet 인터페이스에 선언된 메서드 중에서 service()를 제외한 나머지 메서드들을 모두 구현해있어서 서블릿을 만들 때 사용하면 보다 편리하게 개발할 수 있다.

HttpServlet 클래스

HttpServlet 클래스를 사용하면 service() 대신 doGet() 또는 doPost()를 정의한다.

servlet 그림

 

 

Reference

https://mangkyu.tistory.com/18 

https://tinkerbellbass.tistory.com/1

728x90