Spring MVC, Security 동작 원리와 처리 흐름

Web Server (static 페이지)

웹 초기에는 서버에 정적인 문서(html)를 저장해서 유저가 요청하면 해당 파일을 유저의 브라우저에서 다운받아 보여주는 방식이었습니다. 예를 들면 특정 서버 aaron.com 에 있는 hello.html 문서를 보고싶다면 브라우저에 aaron.com/hello.html 을 호출하면 되는것입니다. 아주 예전에 대학교에서 교수가 자신의 연구실 서버를 이용해서 강의자료를 배포할때 아래와 같은 페이지에 들어가서 다운받았던 기억이 납니다.

서버에 있는 페이지를 유저들에게 보여줍니다.

서버에서 제공하고자 하는 파일들을 실제 서버 내부에 일일히 적재를 해야했으며, 서버에 존재하지 않는 파일에 접근한다면 404 Not Found Error 오류를 보게 됩니다. 이렇게 유저에게 정적인 페이지를 제공하는 서버를 Web Server(웹 서버)라고 부르며 많이 접해봤을 Apache, Nginx 가 이에 해당합니다.

Nginx 의 요청/처리 흐름

요청 처리시

웹 서버의 예로 nginx 에서는 유저 요청을 아래의 과정으로 처리합니다.

요청 처리시

  • 유저는 웹 서버에게 특정 페이지(index.html)를 요청합니다.
  • 웹 서버index.html 검색 후, 있다면 유저에게 반환합니다.

Web Application (dynamic 페이지)

Javascript 의 등장으로 초기 웹 서버처럼 유저에게 단순한 문서를 공유하는 일방적인 서비스를 제공하는것에서 그치지 않고, 유저와의 인터렉션을 통해 회원가입도 가능하고, 글도 쓸 수 있고, 작성한 글들을 서로 볼 수 있는 등의 양방향의 서비스에 대한 요구사항이 생겨나게 되었습니다. 이를 위해서는 일반적인 어플리케이션처럼 데이터베이스와의 연결도 필요하고, 회원의 상태에 따른 동적 페이지 렌더링 등이 필요해졌습니다. 서버는 서버에 있는 자원만 반환하는것이 아니라 유저가 요청한 정보를 요청받은 시점에 알맞은 자원(페이지)를 만들어서 반환하게 됩니다.

서버에 없는 페이지를 유저들에게 매 요청때마다 동적으로 만들어서 보여줍니다.

웹으로 어플리케이션과 같은 요구사항을 처리하기 위해서는 웹 서버여러 언어로 개발된 프로그램을 연결하여 유저의 요청을 서버를 통해 프로그램으로 전달해야합니다. 이렇게 웹 서버와 프로그램 사이를 연결해주는 방식을 CGI(Common Gateway Interface)라고하며 여러 언어로 개발되어있습니다. 그 중 Java 에서는 Web Server 요청/반환과 Java Application 사이를 연결해주는 Servlet 객체가 등장합니다. Servlet 은 유저 요청 하나마다 하나씩 생성되기 때문에 여러 요청에 따른 Servlet 자원 관리가 필요합니다. 이 역할을 하는것이 Web Container 이며 Servlet 입장에선 Servlet Container 로 부르기도 합니다. 유저의 요청/반환을 관할하는 Web Server + 요청에 따른 적합한 Java Application 구동을 위한 Servlet 관리자 Web Container 이 둘을 합쳐 Web Application(웹 어플리케이션) 이라고 부릅니다.

Web Application = Web Server + Web Container(= Servlet Container)

Web Container 는 유저의 요청에 따라 Servlet 자원에 대한 생명주기를 관리합니다

  • 생성(init) -> 처리(service) -> 파기(destory)

Tomcat 의 요청/처리 흐름

요청 처리시

웹 어플리케이션의 예로 tomcat 에서는 유저 요청을 아래의 과정으로 처리합니다.

웹 서버 그림과 비교했을때 웹 서버 아래에 추가된것은 모두 웹 컨테이너에 관련된 것입니다. Web Container 를 시작으로 아래서 위로 역순으로 살펴보겠습니다. 옆에 회색으로 표시한 명칭은 실제 클래스/인터페이스명입니다.

ServletContext (Web Container)

‘Servlet 객체 주기 관리를 위한 웹 컨테이너’에 해당합니다. 관리라는 의미로 Context 를 사용했습니다.
모든 요청에 대한 Servlet 생명주기는 이 ServletContext가 모두 관리합니다.

ServletContextListener

‘Servlet 객체 주기 관리를 위한 웹 컨테이너’ ServletContext 최초 구동시(Listener) 수행할 작업을 정의합니다.

web.xml (Deployment Description)

Deployment Description 이라는 명칭에서 알 수 있듯이 웹 컨테이너 구동시, Servlet 을 위한 2가지 설정을 합니다.

  • B) ServletContextListener 인터페이스 구현체 (어떤것을 실행할지)
  • A) ‘어떤 요청’에 ‘어떤 타입’의 Servlet 객체를 생성할지

추가된 요소들을 살펴보았으니 위 웹 어플리케이션 그림의 유저 요청 처리 방식을 따라가보겠습니다.

최초 구동시

  • tomcat 웹 어플리케이션이 최초 구동시 가장 먼저 웹 컨테이너(ServletContext)를 구동합니다.
  • B) ServletContext 구동 시 web.xml 에 설정한 ServletContextListener 를 같이 수행합니다.

요청 처리시

  • 유저는 웹 서버에게 특정 페이지(index.html)를 요청합니다.
  • 웹 서버index.html 검색 후, 존재하지 않기 때문에 웹 컨테이너(ServletContext)에게 요청을 이관합니다.
  • A) ServletContext 는 web.xml 에서 index.html 요청에 맞는 타입의 Servlet 를 생성합니다.
  • 생성된 Servlet 은 유저가 요청한 페이지를 동적으로 생성하여 유저에게 반환 후 파기(destory)됩니다.

Spring MVC Framework

Java Servlet 을 활용한 웹 어플리케이션 개발이 활성화되면서 여러 디자인 패턴들을 적용하여 Java 웹 개발을 더 쉽게 도와주는 Spring Framework 가 등장하게됩니다. 초기 웹 어플리케이션이 페이지를 동적으로 렌더링하기 위해 각 요청마다 Servlet 을 할당하여 요청을 처리하였다면, Spring 은 각 요청마다 Servlet 보다 작은 단위인 Bean 을 할당하여 요청을 처리합니다.

요청을 처리하는 단위가 Servlet 이라면 Servlet 관리를 위한 Servlet Container
요청을 처리하는 단위가 Bean 이라면 Bean 관리를 위한 Bean Container 가 필요합니다.
이 Bean Container 를 Spring Container 로 부릅니다.

Servlet Container 는 각 URL 요청들을 Serlvet 을 단위로 처리하지만
Spring Container 는 각 URL 요청들을 Bean 을 단위로 처리합니다.

Spring 은 기본적으로 MVC 모델로 Model, View, Controller 세 그룹의 역할로 분리 개발을 돕는 프레임워크이기에 아무리 디자인 패턴에 대한 지식이 전무한 개발자일지라도 유지보수성, 재사용성이 뛰어난 웹 어플리케이션을 만들 수 있습니다. 또한 데이터베이스 접근을 위한 JPA, 트랜잭션, 보안 등 웹 어플리케이션에서 필요로하는 모든것을 Bean 설정으로 제공하기 때문에 어떤 초보자라도 탄탄한 이해만 바탕이 된다면 웹 어플리케이션을 손쉽게 만들 수 있습니다. 디자인 패턴이 실무적으로 어떻게 적용되었는지 공부하는데엔 Spring 만한것이 없는것같다는 어느 시니어의 말씀이 기억에 남습니다.


Spring + Web Application

Spring MVC 동작 과정을 쉽게 이해하기 위해서는 MVCFront Controller 패턴 (2-레벨 Controller) 만 알면 됩니다.

MVC

Model, View, Controller 로써 유저의 요청을 효율적으로 처리하기 위한 모델입니다. 유저가 어떤 페이지를 요청하면

  1. 요청에 적합한 Controller 가 요청을 받아
  2. 요청 페이지에 필요로 하는 정보인 Model 을 조회/생성하고
  3. 조회/생성한 Model 을 통해 최종 페이지인 View 를 생성하여 유저에게 반환하는 모델입니다.

Front Controller 패턴

요청을 받는 부분을 Controller 라고 하였는데 tomcat 은 요청을 Servlet 이라는 Controller 에서 처리하고, Spring 은 요청을 Bean 이라는 Controller 에서 처리합니다. 2-레벨 Controller 의 의미는 (1) 맨 앞의 tomcat 이 모든 요청을 단일 Servlet으로 먼저 받아, 요청 URL 이 무엇인지에 따라서 (2) Spring 의 Controller Bean 에 재할당해주게 됩니다. 가장 앞의 (1) tomcat 단일 Servlet 을 ‘요청을 가장 앞에서 먼저 받는다’는 의미에서 Front Controller 라 부르고, 그 뒤에 (2) Spring Controller Bean 을 실제 페이지 생성에 사용된다는 의미에서 Page Controller 라고 부릅니다.

Spring MVC 의 요청/처리 흐름

최초 구동시

Spring + tomcat 에서는 유저 요청을 어떻게 처리하는지 알아보기에 앞서, tomcat 과 Spring 이 처음 구동될때 어떤 객체들이 생성되어 준비되는지 먼저 알아보겠습니다. Web Container 아래에 Spring Container 가 새로 추가된것을 볼 수 있습니다.

위 그림과 같이 tomcat 에 Spring 을 연결하여 사용하려면 tomcat 설정파일인 web.xml 에 2 가지 설정이 필요합니다.

web.xml (Deployment Description)

  • B) ServletContextListener 인터페이스 구현체 - Root WebApplicationContext
    -> Spring 공용 Bean (@Service, @Repository, @Component…) 객체들을 미리 생성해놓기 위함
  • A) ‘모든 요청’은 Front Controller 에 해당하는 단일 Servlet 객체(DispatcherServlet)가 처리한다.

최초 구동시

  • tomcat 웹 어플리케이션이 최초 구동시 가장 먼저 웹 컨테이너(ServletContext)를 구동합니다.
  • B) ServletContext 구동 시 web.xml 에 설정한 Spring Root WebApplicationContext가 동시에 구동됩니다.

요청 처리시

위 최초 구동 후 tomcat 은 모든 요청을 단일 Servlet(명칭은 DispatcherServlet) 으로 받을 준비가 완료되었고, Spring 도 Controller Bean 이 결과를 반환하기 위해 필요로하는 모든 Bean 들이 Root WebApplicationContext 로 준비가 완료되었습니다. 이제 유저가 요청을 보내면 tomcat 과 Spring 이 어떻게 처리하여 결과를 반환하는지 아래 그림으로 살펴보겠습니다.

Spring 의 키워드는 IoC, DI 라고 할 수 있는데, 간단하게 설명하자면 기존에는 개발자가 new 를 통해 객체를 직접 생성하고, 직접 주입해줬다면 Spring 에서는 어떤 인터페이스, 클래스를 사용할것인지만 표기해놓으면 ApplicationContext(BeanFactory 상속) 라고 불리는 Spring Container 가 객체를 Bean 이라는 단위로 알아서 생성하고 알아서 주입해주는 개념입니다. 이렇게 Spring 에서는 Java 의 모든 객체를 Bean 으로 부르며 사용합니다.

Spring Container = ApplicationContext

Spring 에서 Bean 은 웹 어플리케이션 관점에서 크게 2 개의 타입으로 구분될 수 있습니다.
그에 따라 Bean 의 생명주기를 관리하는 Spring Container 도 2 개의 타입으로 나뉘어집니다.

  • 요청이 들어왔을때 적합한 처리를 위해 요청과 상관없이 모든 Servlet 들이 공유하는 공용 Bean
    • 예: @ComponentScan 으로 등록된 @Service, @Repository, @Component
    • 생명주기 관리: Spring Container 1 (Root WebApplicationContext)
  • 요청이 들어왔을때 할당되는 Servlet 처럼, 요청이 들어왔을때만 생성하면 되는 Bean
    • 예: @ComponentScan 으로 등록된 @Controller, @Interceptor
    • 생명주기 관리: Spring Container 2 (Servlet WebApplicationContext)

위 그림을 보면 최초 구동시에 생성된 Spring Container 1 아래에 또 하나의 Spring Container 2 가 생겨난걸 볼 수 있습니다. parent 와 child 라고 써져있는것은 두 컨테이너 간 계층이 있다는 의미이며, 단순히 child 인 Servlet WebApplicationContext 의 Bean 들은 부모인 Root WebApplicationContext 의 Bean 들을 참조할 수 있지만 그 반대로는 참조할 수 없음을 의미합니다. Root WebApplicationContext 이 모든 Servlet 들이 공유하는 Bean 생명주기를 관리하는것이라 생각하면 당연한것입니다.

요청 처리시

  • 유저는 웹 서버에게 특정 페이지(index.html)를 요청합니다.
  • 웹 서버index.html 검색 후, 존재하지 않기 때문에 웹 컨테이너(ServletContext)에게 요청을 이관합니다.
  • A) ServletContext 는 web.xml 에서 어떤 요청이든 / 단일 DispatcherServlet 을 생성합니다.
  • DispatcherServlet 은 유저가 요청한 페이지에 해당하는 Spring Controller가 있는지 HandlerMapping 을 탐색합니다.
    • Spring Controller 를 Handler 라고 부릅니다
  • DispatcherServlet 은 찾은 Spring Controller BeanHandlerAdapter를 통해 호출합니다.
  • HandlerAdapterHelloController Bean를 호출합니다.
  • HelloController는 Root WebApplicationContext 의 여러 Bean 들을 활용하여 결과를 DispatcherServlet에 반환합니다.
  • DispatcherServlet는 Controller 로부터 받은 결과로 ViewResolver, View에서 결과 페이지(index.html)를 생성합니다.
  • DispatcherServletViewResolver, View가 만든 결과 페이지(index.html)를 유저에게 반환합니다.

위 과정의 코드레벨에서의 흐름은 다음 블로그 링크^1에 잘 정리되어있어 참고하시면 상세히 알 수 있습니다.

이렇게 Spring MVC 에서 어떻게 유저의 요청을 받아서 처리하고 반환하는지를 그림으로 알아보았습니다. 요청 URL 에 따라 Controller Bean 이 할당된다는것은 알았지만, 이렇게 상세하게 알아보니 컨트롤러나 서비스에서 Exception 이 발생하였을때 로그에 남는 Stacktrace 의 메서드와 클래스들의(invoke, DispatcherServlet, preHandle, postHandle 등) 의미를 좀 더 알 수 있었습니다.


Spring Interceptor 와 Filter 의 차이점

Spring 을 활용하여 개발한 웹 어플리케이션들은 일부 혹은 모든 사용자에게 오픈되어 서비스를 제공하기때문에 보안이 필요합니다. Spring Security 는 기본적으로 로그인과 세션에 관련된 모듈 및 설정을 손쉽게 사용가능하도록 제공하지만, 웹 어플리케이션에 인입되는 모든 요청에 따로 개발한 인증 모듈을 적용하거나, 요청 URL 에 따라 다른 처리 등이 필요하다면, 개발자가 해당 로직들을 직접 만들어 유저 요청이 실제 Spring Controller 에게 전달되기 전에 수행되도록 해당 로직을 추가해야합니다. 이때 사용되는것이 InterceptorFilter 입니다.

우리는 앞서 Spring 을 사용한 웹 어플리케이션은 크게 Tomcat (Web Container)Spring (Spring Container) 의 2개로 구성된다는것을 배웠습니다. InterceptorFilter 도 Spring Controller 에 요청이 도달하기 이전에 원하는 중간 작업을 위해 사용된다는 목적에선 동일하지만, 관리주체 및 실행시간이 TomcatSpring 으로 나뉘어집니다.

Filter 는 Servlet 스펙의 일부이고 Servlet(Tomcat)에 의해 호출되지만
Interceptor 는 Spring 에 의해 호출됩니다.
(It’s perfectly fine as Filter’s are part of Servlet specification. Filters are called by your Server(tomcat). while Interceptors are called by Spring^2)

아래는 Interceptor 와 Filter 의 관리주체 및 실행시간을 이해하기 쉽게 표현한 그림입니다.

Filter (Tomcat)

  • Servlet (J2EE 7 표준)스펙에 정의
    • 웹 어플리케이션(tomcat) Deployment Descriptor(web.xml)에 설정
      • 이 부분에 대한것도 최신 Spring 에서 설정 가능
  • 1개의 함수로 DispatcherServlet 이전/이후에 호출
    • 함수명: doFilter()
      • 요청이 DispatcherServlet.service() 에 진입하기 직전(init() 후)에 호출
      • 결과를 DispatcherServlet.service() 가 반환하는 직후(destroy() 전)에 호출
  • doFilter 함수가 요청 진입시 & 결과 반환시, 2번 호출되기 때문에, 암/복호화같은 요청 전 & 반환 후 두 곳에 전역적으로 처리해야하는 로직에 적합합니다.

Interceptor (Spring)

  • Spring Framework 스펙에 정의
    • Spring WebApplicationContext에 설정
  • 3개의 함수로 Controller 이전/이후에 호출
    • 함수명: preHandle()
      • 요청이 Controller 에 진입하기 직전호출
    • 함수명: postHandle()
      • 결과를 Controller 가 반환하는 직후호출
    • 함수명: afterCompletion()
      • Controller 결과에 따라 View 를 생성한 직후호출
  • 컨트롤러 진입 혹은 결과 반환 시점에 디테일하게 처리해야하는 로직에 적합합니다. 예를 들어 특정 URL 로 진입되는 요청에 대해서는 컨트롤러 진입 직전에 해당 URL 에 특화된 정보들을 미리 세션에 설정하여 컨트롤러 내부 로직에서 활용할 수 있게 할 수 있습니다. 다른 URL 이라면 본 로직을 수행하지 않도록 조건을 추가할 수도 있습니다.

필터와 인터셉터는 관리주체가 다르기 때문에 다음과 같은 상황이 발생합니다.

  • 필터는 Spring Container 관리주체가 아니기 때문에 필터 로직 내부에서 Spring 의 Bean 을 사용하려면 @Autowired 같은 빈 주입이 아닌, 먼저 Spring WebApplicationContext 객체를 가져와서 그 안에 설정된 Bean 을 하드코딩을 통해 직접 가져와서 사용해야합니다.

다수 Interceptor 와 Filter 의 호출 순서

필터와 인터셉터는 상황에 따라 여러개를 지정하여 사용할 수 있습니다. 다수의 필터 혹은 인터셉터 사용시 각각의 호출 순서는 설정에 따라 바꿀 수 있는데, 필터도 사실은 DispatcherServlet 호출 전/후에 호출되는 Servlet 설정이기 때문에 tomcat 에서 관리하는것이라 하더라도 인터셉터와 마찬가지로 Spring 설정을 통해 설정할 수 있습니다.

2개의 필터2개의 인터셉터를 사용할때 어떻게 동작하는지 순서를 살펴보기 위해 DispatcherServlet 과 HandlerAdaptor 를 중점적으로 살펴보면 아래와 같습니다.

정확한 순서는 아래 간략하게 요약한 그림으로 알 수 있습니다.

  1. doFilter (F1)
  2. doFilter (F2)
  3. preHandler (I1)
  4. preHandler (I2)
  5. Controller 요청 처리
  6. postHandler (I2)
  7. postHandler (I1)
  8. View 렌더링
  9. afterCompletion (I2)
  10. afterCompletion (I1)
  11. doFilter (F2)
  12. doFilter (F1)

웹 서버에서 웹 어플리케이션, 웹 서버와 웹 어플리케이션을 연결하기 위한 CGI 의 예로 Servlet 그리고 Container 를 알아보고, Spring Container 와 Filter, Interceptor 의 차이 그리고 실행 순서에 대해 알아보았습니다. Spring 을 공부하시거나 사용하시는 다른 개발자 분들에게 본 글이 도움이 되었길 바랍니다. 참조한 글들도 좋은 글들이니 시간이 되시면 한번씩 훑어보시는걸 추천드립니다.


출처:

  1. Spring 동작 원리 #1:
    https://asfirstalways.tistory.com/334
  2. Spring 동작 원리 #2:
    https://devpad.tistory.com/24
  3. Spring 동작 원리 #3:
    https://taes-k.github.io/2020/02/16/servlet-container-spring-container/
  4. Tomcat 이 Spring 호출하는 방법:
    http://www.deroneriksson.com/tutorial-categories/java/spring/introduction-to-the-spring-framework
  5. Java Servlet:
    https://mangkyu.tistory.com/14
  6. Web Server, Web Application 차이:
    https://gmlwjd9405.github.io/2018/10/27/webserver-vs-was.html
  7. Spring DispatcherServlet 동작 원리 #1:
    https://jess-m.tistory.com/15
  8. Spring DispatcherServlet 동작 원리 #2:
    https://dynaticy.tistory.com/entry/Spring-MVC-Dispatcher-Servlet-%EB%82%B4%EB%B6%80-%EC%B2%98%EB%A6%AC-%EA%B3%BC%EC%A0%95-%EB%B6%84%EC%84%9D
  9. Spring web.xml 설명 #1:
    https://sphere-sryn.tistory.com/entry/%EC%8A%A4%ED%94%84%EB%A7%81%ED%94%84%EB%A0%88%EC%9E%84%EC%9B%8C%ED%81%AC-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EC%9D%98-%EA%B0%80%EC%9E%A5-%EA%B8%B0%EB%B3%B8%EC%84%A4%EC%A0%95-%EB%B6%80%EB%B6%84%EC%9D%B8-webxml%EC%97%90-%EB%8C%80%ED%95%98%EC%97%AC-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90
  10. Spring web.xml 설명 #2:
    https://gmlwjd9405.github.io/2018/10/29/web-application-structure.html
  11. Spring 2개 타입의 ApplicationContext:
    https://jaehun2841.github.io/2018/10/21/2018-10-21-spring-context/#web-application-context
  12. Servlet Container & Spring Container:
    https://velog.io/@16616516/%EC%84%9C%EB%B8%94%EB%A6%BF-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88%EC%99%80-%EC%8A%A4%ED%94%84%EB%A7%81-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88
  13. Spring MVC 코드 기반 동작 원리:
    https://galid1.tistory.com/526
Author

Aaron Ryu

Posted on

2021-02-14

Updated on

2021-03-14

Licensed under

Comments