본문 바로가기
Application/Java

Spring Web MVC Framework 동작방식 이해하기

by wrynn 2022. 6. 3.

 Java의 대표적인 프레임워크인 Spring 다양한 하위 프레임워크의 집합으로 이루어집니다. 그 중 가장 널리 쓰이는 것이 개발자로 하여금 웹 애플리케이션을 MVC 패턴으로 쉽게 구현할 수 있도록 도와주는 Spring Web MVC 프레임워크입니다. 본문에서는 Spring Web MVC 프레임워크의 동작 방식에 대해 알아보겠습니다. 이 프레임워크의 동작 원리를 이해하기 위해서는 먼저 Java Servlet에 대한 개념이 필요합니다. 

 

Servlet

 Servlet은 클라이언트의 요청을 쉽게 처리하고 응답할 수 있도록 만든 Java 인터페이스입니다. 여러 프로토콜을 지원할 수 있도록 만들었지만 그 중에서도 HTTP 요청을 처리하는 Servlet을 가장 많이 사용하며, 이는 HttpServlet이라는 추상 클래스를 상속하여 구현합니다. 주요 메소드는 다음과 같습니다.[2]

  • init() : Servlet 객체 생성 시에만 단 한 번 호출되며, 초기화를 수행합니다. 
  • service() : HTTP 요청 타입(GET, POST 등)에 따라 실행할 메소드를 정의합니다. 예를 들어 GET 요청의 경우, doGet() 메소드를 실행하도록 정의되어 있습니다. 따라서 일반적으로 service()를 오버라이딩할 필요는 없으며 doGet(), doPost() 등의 doXXX() 메소드를 오버라이딩하여 비즈니스 로직을 구현합니다. 이 메소드들은 모두 공통으로 클라이언트의 HTTP 요청을 표현하는 HttpServletRequest와 반환할 HTTP 응답을 담을 HttpServletResponse를 인자로 가집니다. 이들은 HTTP 요청 및 응답을 Java 언어로 쉽게 다룰 수 있도록 구현해 둔 클래스입니다. 
  • destory() : Servlet 객체 소멸 시에 단 한 번 호출됩니다. DB Connection 중단, 스레드 중지 등 Servlet이 실행되는 동안 사용했던 자원을 반환합니다. destroy() 호출 이후 해당 서블릿은 GC 대상에 포함됩니다.

 

Servlet Container (= Web Container)

 이제 우리는 Servlet으로 클라이언트의 HTTP 요청을 처리하는 비즈니스 로직을 구현할 수 있게 되었지만, 이것만으로는 사용자의 요청을 처리하기에 충분하지 않습니다. 적절한 타이밍에 Servlet을 생성하고 호출하며 삭제해주는 부분도 구현해야 합니다. 또한 클라이언트와 통신하기 위해 특정 포트를 수신하거나 소켓을 열고, 클라이언트의 HTTP 요청을 Java 객체로 변환하는 등의 기능 역시 필요합니다. 매번 개발할 때마다 이런 부분을 구현하는 것이 쉽지도 않을 뿐더러, 이런 부분을 제각각 구현한다면 시스템의 유지보수는 굉장히 어려워 질 것입니다. 그래서 Servlet Container가 등장합니다.

 Servlet Container는 JakartaEE (혹은 JavaEE)에 명시된 Servlet Specification을 구현한 서버 측 프로그램으로 Servlet의 생명주기를 관리합니다. Servlet 생명 주기를 관리한다는 것을 풀어서 말하자면 Servlet 객체의 init(), service(), destroy() 메소드를 개발자가 직접 호출하지 않고 Servlet Container에 의해 자동으로 관리가 이루어 진다는 것을 의미합니다. 게다가 Servlet Container는 소켓 생성, 포트 바인딩 등 클라이언트 연결과 관련된 다양한 기능을 포함하고 있습니다. 대표적인 Servlet Container로 Tomcat이 있습니다.

 Servlet Container를 사용하면 Servlet의 관리와 네트워크 관련 구성요소를 Servlet Container에 위임하므로 개발자는 Servlet의 비즈니스 로직 구현에만 집중할 수 있고, 이 덕분에 다른 관심사들과 Servlet을 분리시켜 재사용이 용이하도록 만든다는 이점이 있습니다.

 각각의 HTTP 요청에 대한 Servlet Container의 처리 절차는 다음과 같습니다.

  1. 주어진 HTTP 요청에 대한 HttpServletRequest 객체와, 응답을 담을 HttpServletResponse 객체 생성 
  2. 요청의 URI 패턴을 보고, 요청을 처리할 Servlet 탐색 (web.xml 파일에 전체 URI와 Servlet 사이 매핑 정보를 미리 정의) 
  3. 찾아낸 Servlet 객체가 이미 생성되어 있지 않다면 생성 (생성 시 init() 호출)
  4. 스레드를 하나 생성하고 요청을 처리할 Servlet의 service() 실행
  5. HttpServletResponse 객체를 HTTP로 변환 후 응답
  6. HttpServletRequest, HttpServletResponse 객체 소멸

 Servlet 객체가 생성될 조건을 주목해보면, Servlet 객체는 최대 1개까지만 생성될 수 있다는 것을 알 수 있습니다. 여러 사용자의 요청을 동시에 처리할 때에는 새로운 Servlet 객체를 생성하는 것이 아니라, 4번 단계에서 표현한 것 처럼 새로운 스레드를 생성하여 각 스레드가 service() 메소드를 실행하는 방식으로 처리합니다. 

더 알아보기) Middleware 성능 튜닝
 새로운 thread를 생성하는 것은 메모리 할당과 해제 등의 오버헤드가 발생하므로 다수의 요청을 처리할 수 있도록 미리 thread를 여러 개 생성해 두는 것을 thread pool 이라고 합니다. 하지만 thread pool을 너무 크게 설정할 경우 context switch로 인해 성능 저하가 발생할 수 있습니다. 때문에 다수의 요청을 처리해야 할 경우 thread 수 뿐만 아니라 Web Application Server(서블릿 컨테이너를 실행하는 서버)의 수를 늘리는 것도 고려해 보아야 합니다. 실제 엔터프라이즈 환경에서는 thread pool의 최소/최대값을 OS kernal parameter와 함께 적절하게 설정해 주어야 합니다.

 지금까지 Servlet을 통해 클라이언트의 HTTP 요청을 처리하는 방법과, 이 Servlet을 효율적으로 다루기 위한 Servlet Container에 대해 알아보았습니다. 그렇다면 Spring Web MVC 프레임워크에서는 사용자의 요청을 어떻게 처리할까요?


Spring Web MVC

 Servlet Container의 동작 과정을 다시 한 번 생각해봅시다. Servlet Container는 요청을 수신하고, 해당 요청을 URI에 따라 적절한 Servlet으로 보내 요청을 처리하게 합니다. 하지만 이렇게 하기 위해서는 각 URI마다 이에 대응하는 Servlet을 구현해야 하고, 동시에 URI와 Servlet의 매핑 정보 또한 함께 관리해야 하는 불편함이 뒤따릅니다.

 Spring Web MVC에서는 이런 불편함을 해결하기 위해 Servlet Container 내에 HTTP 요청을 받고 공통 기능을 처리할 단일 Servlet을 둡니다. 이 때 중앙에서 요청을 받는 Servlet을 DispatcherServlet이라고 하며, 이렇게 중앙화된 하나의 컴포넌트에서 요청을 다루는 디자인 패턴을 Front Controller 패턴이라고 합니다. 

 또한 비즈니스 로직의 처리는 Servlet이 아닌 Controller에 보내 처리하도록 구현하였습니다. DispatcherServlet이 호출할 각각의 Controller는 클라이언트의 HTTP 호출을 직접 받지 않으므로, 더 이상 HTTP 요청을 처리하는 Servlet이 아니어도 됩니다. 대신 POJO(Plain Old Java Object)라고도 불리우는 일반적인 Java 클래스와 메소드로 구현할 수 있어 추상 클래스 구현에 따라 오버라이딩을 해 주어야 하는 불편함이 사라졌습니다. 또한 DispatcherServlet은 더 이상 URI를 Servlet과 연결짓지 않고 메소드와 연결하므로 URI마다 Servlet 혹은 Controller를 구현하지 않아도 됩니다.

https://terasolunaorg.github.io/guideline/5.0.1.RELEASE/en/Overview/SpringMVCOverview.html

 Spring Web MVC는 그 이름처럼 MVC 패턴을 사용합니다. 데이터를 나타내는 Model, 사용자가 볼 결과물을 렌더링하는 View, 비즈니스 로직을 처리하는 Controller가 분리되어 있으며 DispatcherServlet은 이들을 다루기 위해 다양한 컴포넌트를 활용합니다. DispatcherServlet이 각 컴포넌트를 활용하여 요청을 처리하는 절차는 다음과 같습니다.[3] 

  1. DispatcherServlet이 가장 먼저 요청을 받습니다.
  2. DispatcherServlet은 요청을 HandlerMapping에 전달합니다. HandlerMapping은 요청 URL을 보고 요청을 처리할 적절한 Controller를 선택하여 이를 Handler와 함께 반환 합니다.
  3. DispatcherServlet은 비즈니스 로직을 실행하는 작업을 HandlerAdapter에 전달합니다.
  4. HandlerAdapter가 Controller 비즈니스 로직을 호출합니다.
  5. Controller는 비즈니스 로직을 실행하고 처리 결과를 Model에 설정합니다. 그리고 HandlerAdapter에 결과를 보여줄 적절한 View(i.e. html 형식의 템플릿)의 이름을 DispatcherServlet에 반환합니다.
  6. DispatcherServlet은 View 이름으로부터 실제로 사용할 View 를 선택하는 작업을 ViewResolver에 전달합니다. ViewResolver는 View 이름과 매핑되는 View를 반환합니다.
  7. DispatcherServlet 반환된 View에 렌더링 프로세스를 전달합니다.
  8. View는 Model의 데이터를 렌더링하고 응답을 반환합니다.

 요청을 처리하는 절차가 꽤 길고 복잡해 보일 수도 있지만, 실제로 개발자가 구현하는 부분은 보라색으로 표시된 비즈니스 로직을 수행하는 부분입니다. 나머지는 모두 Spring Web MVC 프레임워크에 의해 처리됩니다.

 지금까지 Spring Web MVC 프레임워크에서 HTTP 요청을 처리하는 전반적인 과정을 살펴보았습니다. 아래 내용은 여기서 더 나아가 Spring에서 객체를 관리하는 방식인 Spring Container에 대해 알아봅니다.


부록) Spring Container

 Spring은 비즈니스 로직의 구현에서도 개발자에게 도움을 줍니다. 위 구조도를 보면 Controller는 Service 객체를 사용합니다. 따라서 정적 메소드를 호출하는 것이 아닌 이상, 이를 구현하려면 Controller 내부에서 Service 객체를 생성하는 절차를 반드시 거쳐야 합니다. 하지만 반복되는 요청을 응답하는 과정에서 매번 Service 객체를 생성하고 요청을 처리하고 난 후 지우는 절차를 반복하는 것 보다, 하나의 객체를 만들어 두고 이를 활용하여 요청 처리만 수행한다면 Service 객체 생성 및 제거에 따르는 오버헤드를 줄일 수 있습니다. Spring은 개발자를 대신하여 이런 객체 사이의 관계를 관리하기 위해 Spring Container를 사용하며, 이렇게 Spring이 관리하는 POJO를 Bean이라고 합니다.

더 알아보기: Bean의 Scope
 각각의 Bean은 객체의 생명 주기를 나타내는 Scope를 가집니다. Scope의 기본값은 프로그램의 실행부터 종료까지 단 하나의 객체만 생성하는 singleton입니다. 이 외에도 매번 getBean() 이 호출될 때 마다 새로 객체를 생성하는 prototype, HTTP 세션마다 객체를 생성하는 session 등을 값으로 가질 수 있습니다.

 

 Spring Container는 애플리케이션을 시작할 때, 각 Bean에 작성된 어노테이션(@Controller, @Service 등)을 확인하는 Component Scan 과정을 거치거나 @Configuration에 정의된 내용을 확인하여 Bean 객체를 생성합니다. (이와 같은 과정 때문에 Spring 애플리케이션을 실행하는 초기에 프로세스의 CPU 사용률이 증가합니다.) 

https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-servlet-context-hierarchy

 웹 애플리케이션에서는 ApplicationContext를 상속한 WebApplicationContext를 Spring Container로 사용합니다. WebApplicationContext는 여러 Servlet이 공유하여 사용할 수 있는 Bean을 저장하는 Root WebApplicationContext와 각각의 DispatcherServlet이 개별적으로 사용하는 Bean을 저장해두는 Servlet WebApplicationContext 두 종류가 존재합니다. (일반적인 Spring 애플리케이션에서 DispatcherServlet은 하나이지만, 몇몇 특수한 경우에는 여러 개의 DispatcherServlet을 가질 수 있습니다.)

 Root WebApplicationContext는 애플리케이션 실행 시점에 ContextLoaderListener에 의해 생성되고 Servlet WebApplicationContext는 각 DispatcherServlet의 init()이 호출되는 과정에서 생성됩니다. DispatcherServlet은 이렇게 생성한 Spring Container 내부의 객체들에게 요청 처리에 필요한 작업을 전달하여 요청을 처리하게 됩니다.


참고자료

[1] https://www.youtube.com/watch?v=calGCwG_B4Y 

[2] https://www.tutorialspoint.com/servlets/servlets-life-cycle.htm

[3] https://terasolunaorg.github.io/guideline/5.0.1.RELEASE/en/Overview/SpringMVCOverview.html

 

'Application > Java' 카테고리의 다른 글

Spring Boot 프로젝트 생성과 프로젝트 구조  (0) 2022.06.02

댓글