Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Springboot 요청/응답 처리 흐름 #20

Open
ngwoon opened this issue Aug 13, 2022 · 1 comment
Open

Springboot 요청/응답 처리 흐름 #20

ngwoon opened this issue Aug 13, 2022 · 1 comment

Comments

@ngwoon
Copy link

ngwoon commented Aug 13, 2022

사전지식

제가 현재 맡고 있는 프로젝트에서는 세 단계의 로그를 출력하고 있습니다.

  • access log
    • 가장 앞단에서 raw한 http 요청/응답 정보를 기록합니다.
    • referer, x-forwarded-for, redirect_uri 등의 정보를 보는 용도입니다.
  • history log
    • 애플리케이션의 비즈니스 로직과 관련된 http 요청/응답 정보를 기록합니다.
    • action(ex. GET /test) , controller의 파라미터, result (http 응답 데이터) 등의 정보를 보는 용도입니다.
  • error log
    • http 요청 처리 도중 오류 발생 시 기록되는 로그입니다.
    • 주로 stack_trace를 보는 용도입니다.

정상적인 요청이 들어오면 access, history 로그가 출력되고, 오류를 유발하는 요청은 access, history, error 로그 모두 출력됩니다. 이 때 모니터링의 편의성을 위해 uuid라는 랜덤 문자열을 만들고, 동일한 http 요청에 의해 출력되는 로그들에 모두 같은 uuid 값을 넣어주고 있습니다.

위 세 가지 로그는 log 폴더 아래 각각 access.log, history.log, error.log 파일로 기록되며, 일정 주기로 rollover됩니다. 이 로그 데이터들은 filebeat에 의해 실시간으로 Logstash로 보내지며, ElasticSearch를 거쳐 Kibana로 시각화됩니다.

로깅을 위한 Slf4j의 구현체로 Logback을 사용하고 있습니다.

사건의 발단

Kibana로 로그 모니터링 중, 특정 로그들이 중복해서 찍히는 현상을 발견했습니다. 이 현상을 설명하기 위해 access, history, error 로그가 언제, 어느 곳에서 출력되는지 알아봤습니다.

access log는 HttpHandler를 구현한 AccessLogHandler.class에서 출력하고 있었고,

history log는 HandlerInterceptorAdapter를 상속한 HistoryLogHandlerInterceptor.class에서 출력하고 있었습니다.

error log는 @ExceptionHandler가 붙어있는 메서드 내부에서 출력하고 있었습니다.

이렇게 살펴보고 나니, http 요청이 들어와서 나갈 때 까지의 흐름을 제대로 몰라서 로그가 왜 저렇게 출력되는지 이해할 수가 없었습니다. 이에 Springboot 기반 서버로 http 요청이 들어왔을 때 어떤 과정을 거쳐 http 응답이 만들어지는지 전체 흐름을 이해해야 했습니다.

단계별로 파악하기

제가 알고 있던 http 서버의 요청/응답 흐름은 다음과 같았습니다.

  1. 클라이언트가 웹 서버로 http 요청을 보낸다.
  2. 웹 서버는 이를 받아, 본인이 처리할 수 없는 요청이면 WAS에게 위임한다.
  3. WAS에서 비즈니스 로직을 거쳐 동적 응답이 만들어지고, 클라이언트에게 반환된다.

저는 시작부터 멈칫했습니다.

Springboot 서버를 띄우면 Tomcat, Jetty와 같은 컴포넌트가 함께 띄워지는 것으로 알고 있었는데, 이 컴포넌트가 WAS인가? 라는 의문이 들었습니다. Tomcat이 WAS이면 내가 작성한 비즈니스 로직은 뭐지? 애초에 WAS를 뭐라고 정의해야 하는거지? 질문이 꼬리에 꼬리를 물었고, 근본적인 해결을 위해 WS와 WAS 개념부터 다시 잡았습니다.

Web Server

웹 프로토콜을 기반으로 정적 리소스를 클라이언트에게 제공할 수 있는 서버를 말합니다.

웹 서버 자체는 정적 리소스만을 제공할 수 있다는 단점이 있습니다. 클라이언트에게 동적 리소스를 제공하기 위해, 웹 서버로 요청이 들어오면 외부 프로그램에 동적 리소스 생성을 요청하고, 그 결과를 받아 클라이언트에게 제공하는 방법이 고안되었습니다. 그리고 이 과정에서 필요한 웹 서버와 외부 프로그램 사이의 통신 규약이 CGI(Common Gateway Interface) 입니다.

CGI는 규약이므로, 다양한 언어로 해당 규약을 구현할 수 있다는 장점이 있습니다. 아래는 웹 서버와 CGI 기반의 동적 리소스 제공이 이루어지는 과정을 표현한 그림입니다.

https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbXR9WD%2FbtqGRnGOtZv%2FJXXcSaE2iPZiMTaJZwUqa0%2Fimg.png

개발자가 CGI 스크립트를 만들어두면, 웹 서버가 동적 리소스 요청에 맞는 CGI 스크립트를 실행하는구조입니다. 뒤에 등장할 Servlet과 동작 원리가 꽤 유사합니다.

Web Application Server

웹 서버 + CGI 조합은 각 요청마다 새로운 CGI 프로세스를 생성하여 처리하므로 요청 처리 속도가 느리다는 큰 단점이 있었습니다. 이에 새롭게 등장한 동적 리소스 처리 방식이 WAS입니다.

WAS와 CGI의 가장 큰 차이는, CGI 방식은 웹 서버가 CGI 스크립트를 직접 실행하는 구조였다면, WAS방식은 웹 서버가 요청 처리를 WAS에게 위임한다는 데에 있습니다. 아울러 멀티 프로세스가 아닌 멀티 쓰레드를 사용하여 서버 부하가 적습니다.

WAS를 조금 더 자세하게 살펴보면, WAS는 웹 서버 + 웹 컨테이너로 구성되어 있습니다. Java EE 표준 기반의 WAS에서는 웹 컨테이너를 서블릿 컨테이너라고도 부릅니다.

(정확히 말하자면 웹 컨테이너 == 서블릿 컨테이너는 아닙니다. 통상적으로 그렇게 부른다는 뜻입니다.)

여기서 잠깐, 서블릿이란?

서블릿은 “Java EE 플랫폼 상에서, 클라이언트의 요청을 동적으로 처리하고 그 결과를 반환하는 자바 웹 프로그래밍 기술” 입니다. Java Servlet Specification이 존재하며, 이 표준을 지켜 구현된 어떤 것이든 서블릿이 될 수 있습니다. 현재 서블릿은 javax.servlet.http.HttpServlet 클래스를 상속해 구현할 수 있습니다.

서블릿 컨테이너는 서블릿의 생명주기를 관리하는 컴포넌트로, 아래와 같은 역할들을 수행합니다.

  • 웹 서버와의 통신
    • 서블릿은 요청에 따른 처리 방법을 정의할 뿐이고, 실제 요청이 왔을 때 적절한 서블릿을 찾아 요청 처리를 시작하는 건 서블릿 컨테이너의 몫이다.
    • 즉, 서블릿 컨테이너는 외부의 웹 서버와 소켓 통신을 지속적으로 수행해야 한다.
  • 서블릿 생명주기 관리
    • 서블릿의 생명주기는 init() → service() + doGet/doPost/doOptions … → destroy() 이다.
    • 요청에 대응되는 서블릿을 web.xml을 통해 찾고, 이를 메모리에 적재 및 초기화한 뒤, 새로운 쓰레드에 할당하여 요청이 처리될 수 있게 만든다.
    • 서블릿이 더 이상 필요없다고 판단되면, destory() 를 통해 파괴한다.
  • 멀티쓰레드 지원 및 관리
    • 서블릿 컨테이너는 각 요청을 별도의 Thread로 할당한다.
    • 따라서 개발자는 코드를 직접 멀티쓰레드로 개발하지 않아도 된다.
  • 선언적 보안 관리
    • 서블릿 컨테이너는 보안 관련 내용을 xml로 관리한다.
    • 따라서 개발자는 보안과 관련된 내용을 서블릿에 구현해 놓지 않아도 되며, 수정이 필요할 시 자바 클래스들을 재컴파일할 필요 없이 xml 파일만 수정하면 된다.

간단히 정리하면, 서블릿은 요청 처리 정의서이고, 서블릿 컨테이너는 http요청에 맞는 서블릿을 찾아 수행시키는 컴포넌트입니다.

지금까지 살펴본 웹 서버 + 서블릿 컨테이너 조합으로 HTTP 요청을 처리하는 과정을 요약하면 아래 그림과 같습니다.

https://ssup2.github.io/images/theory_analysis/Servlet_Servlet_Container/Servlet_Servlet_Container.PNG

여기까지 알아봤다면, Tomcat이나 Jetty와 같은 서블릿 컨테이너와 WAS는 분명 차이가 있음을 알 수 있습니다. 이 차이를 명확하게 글로 정리하면 다음과 같습니다.

  • 서블릿 컨테이너는, Servlet API만을 지원하는 컴포넌트이다.
  • WAS는, Java EE의 모든 명세를 지원하는 컴포넌트이다. (EJB, JMS, CDI, JTA, Servlet API)

위에서 WAS == 웹 서버 + 웹 컨테이너 라고 설명했습니다. Java 진영의 WAS에서 웹 컨테이너를 서블릿 컨테이너로 부르기도 한다고 말했지만, 정확히는 웹 컨테이너가 서블릿 컨테이너보다 더 넓은 서비스를 지원해주는 개념입니다.

Springboot 기반의 WAS는 서블릿 컨테이너에 스프링 컨테이너가 합쳐져 완성됩니다. 서블릿 컨테이너가 Servlet API만을 지원하고, 스프링 컨테이너가 그 외 Java EE 플랫폼의 서비스들을 지원합니다.

스프링 컨테이너는 스프링 생태계의 핵심으로, 이미 구현되어있는 스프링 프레임워크에 개발자가 작성한 비즈니스 로직을 알맞게 조립하는 역할을 담당합니다. 가장 대표적인 역할로는 IoC 컨테이너로써의 역할 (올바르게 DI해주는 역할) 이 있습니다.

Filter vs Interceptor

Filter와 Interceptor는 DispatcherSerlvet을 기준으로 밖, 안에 위치해 있다는 것은 다들 아실 겁니다.

지금까지 웹 서버, 서블릿 컨테이너, 스프링 컨테이너의 개념을 배웠으니, 아래 그림을 보면 Filter와 Interceptor의 위치가 명확히 보일겁니다.

https://gowoonsori.com/spring/architecture/spring-architecture.PNG?classes=shadow,border

DispatcherServlet은 서블릿이므로 서블릿 컨테이너에 의해 관리됩니다.

Filter는 DispatcherServlet 앞에 위치해야 하므로, 서블릿 컨테이너 영역에 위치합니다.

Interceptor는 DispatcherServlet 뒤에 위치하므로, 스프링 컨테이너 영역에 위치합니다.

Filter

Filter는 서블릿 컨테이너 영역에 위치하며, 요청에 대한 사전처리를 담당하는 컴포넌트입니다.

Undertow 기준으로 설명드리면, Undertow는 요청이 들어오면 HttpHandler들이 연쇄적으로 해당 요청을 처리합니다.

아래 그림 마지막에서 두 번째에 보이는 Servlet Filter Handler에서 우리가 아는 Filter가 수행되고, 그 다음 Servlet Handler에서 DispatcherServlet으로 요청을 위임합니다.

package io.undertow.servlet.handlers;

import ...

/**
 * @author Stuart Douglas
 */
public class FilterHandler implements HttpHandler {

    private final Map<DispatcherType, List<ManagedFilter>> filters;
    private final Map<DispatcherType, Boolean> asyncSupported;
    private final boolean allowNonStandardWrappers;
		
		... 생략
}

Undertow의 경우 Undertow의 Handler를 등록할 수도 있고, Undertow의 FilterHandler에 Filter를 등록할 수도 있습니다.

Interceptor

DispatcherServlet에서 handler(controller) 를 실행하기 전, 후로 Interceptor의 preHandle(), postHandle()을 실행시킵니다.

https://examples.javacodegeeks.com/wp-content/uploads/2016/01/spring-mvc-architecture.jpg

정확히 말하자면 DispatcherServlet에서 controller를 실행시키기 위해 HandlerChainExecution객체를 찾습니다. HandlerChainExecution 객체는 하나의 handler와 해당 handler에 적용될 Interceptor 목록을 갖고 있습니다. HandlerChainExecution이 실행 순서는 다음과 같습니다.

  • Interceptor 목록의 모든 preHandle() 호출
  • handler (controller) 호출
  • Interceptor 목록의 모든 postHandle() 호출

이후 DispatcherServlet은 HandlerChainExecution의 triggerAfterCompletion을 통해 Interceptor 목록의 모든 afterCompletion()을 호출함으로써 Interceptor가 정상적으로 동작할 수 있게 됩니다.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants