본문 바로가기
Java/Spring

스프링의 내부구조 파헤치기

by oneny 2023. 9. 25.

스프링 내부구조

 

Web Server

Client는 HTTP 프로토콜을 이용하여 요청을 보내게 된다. 웹 서버는 이를 해석해 요청에 맞는 데이터를 보내주어야 한다. 그에 맞는 데이터 형식으로 보내주는 것이 Web Server가 할 일이다. 웹 서버는 단순히 요청에 대한 데이터를 수정없이(static, 정적) 클라이언트에게 보내주기만 하면 된다. 초창기 인터넷에서는 정적 데이터에 대한 수요가 높았기 때문에 기능적으로 WAS를 따로 나누지 않고 웹 서버라는 개념을 통칭해서 사용했다.

 

WAS(Web Application Server)

WAS는 J2EE 스펙을 구현하여, 서블릿이나 JSP로 작성된 애플리케이션을 실행하는 소프트웨어이다.

 

참고: J2EE(Java 2 Platform, Enterprise Edition)
Servlet, JSP, EJB(Enterprise JavaBeans), JDBC, JNDI, JMS(Java Message Service), JTA(Java Transaction API) 등을 구현하고 썬 마이크로시스템즈가 J2EE 명으로 발표한 분산 애플리케이션 개발목적의 산업 표준 플랫폼이다. 기업용 애플리케이션을 개발/실행하기 위한 기술과 환경을 제공한다.

 

Web Server와 Web Application Server의 차이점

Web Server와 WAS의 사용 목적은 다르다.

  • Web Server: HTML, CSS, 이미지 요청 등 정적 데이터 요청을 처리하는데 빠르다.
  • WAS: Servlet, JSP 등 비즈니스 로직을 수행하는데 적합하다.

그렇다고 WAS가 HTML, 이미지 등의 요청을 처리하지 못한다는 말은 아니다. 다만, 처리속도가 Web Server에 비해 느리다. 따라서 서로의 다른 강점을 합해서 사용하기 위해 Web Server와 WAS를 연동하여 서비스를 하는 것이 대부분이다.

 

Servlet Container

아파치 톰캣(Apache Tomcat), 제티(Jetty), 그리즐리(Grizzly) 등은 서블릿 컨테이너로 현재 널리 사용되고 있다. 아파치 톰캣은 오랫동안 서블릿 기술 명세에 대한 참조 구현체였으며, 가장 잘 알려진 오픈 소스 계열의 서블릿 컨테이너이다.

서블릿 컨테이너의 주요 목표는 서블릿을 동작시키는 데 있다. 사용자가 원하는 비즈니스 로직을 서블릿 인터페이스 안에서 구현하고, 서블릿 컨테이너에 해당 서블릿 구현을 배치라는 특별한 절차를 거쳐 등록하면 서블릿 컨테이너는 다음과 같은 기능을 수행한다.

  • 네트워크 통신: 응답을 위한 소켓을 만드는 역할을 수행한다.
  • 서블릿의 생명주기 관리
  • 스레드 기반의 병렬 처리: 요청마다 스레드를 생성해 요청을 처리하기 위한 스레드풀을 관리한다.

 

서블릿 컨테이너 상에서 요청이 처리되는 과정

해당 서블릿 컨테이너도 결국 자바프로그램이기 때문에 별도의 JVM이 동작하고, 웹 서버와 다른 프로세스인 WAS에서 실행된다.

  1. 웹 서버: 클라이언트에게 동적 요청이 오면, 이를 서블릿 컨테이너에 위임한다.
  2. 서블릿 컨테이너: 요청을 처리하기에 알맞은 서블릿을 찾고, 해당 서블릿에서 요청을 처리하기 위한 메서드를 호출한다.
  3. 서블릿: 요청을 처리한 후, 결과를 서블릿 컨테이너에 반환한다.
  4. 서블릿 컨테이너: 웹 서버에 서블릿 프로그램이 반환한 결과를 전달한다.
  5. 웹 서버: 서블릿 컨테이너가 전달한 결과를 클라이언트에게 응답으로 반환한다.

서클릿 컨테이너는 사용자의 요청이 오면 서블릿을 매핑하여 실행한다. 이 매핑 정보는 다음과 같은 두 가지 방식으로 작성할 수 있고, 지정된 클래스를 발견하면 요청에 맞는 서블릿을 매핑할 수 있다.

  • 배포 서술자(web.xml) 파일의 <servlet> 태그 아래 서블릿 매핑 정보를 작성
  • @WebServlet 어노테이션을 사용

 

ThreadPool

서블릿 컨테이너는 하나의 요청 처리 과정을 하나의 스레드로 관리한다. 즉, 요청 하나 당 스레드 하나가 할당되며, 각 스레드는 요청을 처리하기 위해 서블릿의 service() 메서드를 호출하고 그 결과를 반환하며, 결과가 반환되면 작업이 종료된다.

서블릿 컨테이너는 스레드를 생성하는 비용을 비싸기 때문에 미리 스레드 풀에 스레드를 일정 개수 생성해놓고, 요청이 올 때마다 스레드풀에 있는 스레드가 하나씩 맡아서 처리한 뒤, 요청 처리가 종료되면 스레드풀에 반납하도록 하는 방식을 사용한다. 이 때문에 서블릿 컨테이너에서는 최소 및 최대 스레드 수를 결정하는게 주요 튜닝 포인트이기도 하다.

 

Servlet

원칙적으로 javax.servlet.Servlet 인터페이스를 구현한 것이 서블릿이다. 일반적인 자바 독립 실행 프로그램은 public static void main(String[] args) 메서드가 있어서, 해당 메서드가 시작되면 프로그램 수행을 시작하며 main 메서드가 끝나면 프로그램 역시 종료된다. 하지만 서블릿은 서블릿 컨테이너에 등록된 후 서블릿 컨테이너에 의해 생성, 호출, 소멸이 이뤄진다. 다시 말해, 서블릿은 독립적으로 실행되지 않고, main 메서드가 필요하지 않으며, 서블릿 컨테이너에 의해 서블릿의 상태가 바뀐다. 이러한 서블릿의 목적은 HTTP 프로토콜을 사용해 자바로 웹 서비스를 제공하는 것이라고 할 수 있다. 

 

생명 주기 관련 메서드

서블릿 최상위 인터페이스로, 서블릿 실행의 생명 주기와 연관된 메서드, 서블릿 설정, 관련 정보를 알기 위한 메서드를 정의한다. 생명 주기와 연관된 다음의 메서드들이 중요하다.

  • init: 서블릿 객체를 생성한다. 서블릿 컨테이너에 의해 호출된다.
  • service: 요청을 처리하고 응답을 반환한다.
    • 인자: HttpServletRequest, HttpServletResponse
    • 서블릿의 핵심인 요청 처리 시 호출되는 메서드
  • destroy: 서블릿 객체를 제거한다. 서블릿 컨테이너에 의해 호출된다.

 

Servlet 생명주기

서블릿은 자신의 상태 변경 시점을 알아내 적절한 리소스 획득/반환 등의 처리를 해야 하므로 Servlet 인터페이스에 init/destory 메서드가 정의된다. 다시 말해 서블릿 컨테이너는 서블릿의 생명주기에 따라 서블릿의 상태를 변경하면 서블릿 인터페이스에 정의된 각 메서드를 불러준다.

 

  1. 요청이 오면, Servlet 클래스가 로딩되어 요청에 대한 Servlet 객체가 생성된다.
  2. 서버는 init() 메서드를 호출해서 Servlet을 초기화한다.
  3. service() 메서드를 호출해서 Servlet이 브라우저의 요청을 처리하도록 한다.
  4. service() 메서드는 특정 HTTP 요청(GET, POST 등)을 처리하는 메서드(doGet(), doPost() 등)를 호출한다.
  5. 서버는 destory() 메서드를 호출하여 Servlet을 제거한다.

 

Servlet 객체를 생성하고 초기화하는 작업은 비용이 많은 작업이므로, 다음에 또 요청이 올 때를 대비하여 이미 생성된 Servlet 객체는 메모리에 남겨둔다. 또 서블릿 컨테이너는 종료되기 전이나 reload 전에 모든 Servlet을 제거한다. 이렇게 서블릿 컨테이너는 자원을 아끼면서 Servlet을 관리하고 있다.

 

GenericServlet

void destroy()
java.lang.String getInitParameter(java.lang.String name)
java.util.Enumeration<java.lang.String> getInitParameterNames()
ServletConfig getServletConfig()
ServletContext getServletContext()
java.lang.String getServletInfo()
java.lang.String getServletName()
void init()
void init(ServletConfig config)
void log(java.lang.String msg)
void log(java.lang.String message, java.lang.Throwable t)
abstract void service(ServletRequest req, ServletResponse res)

서블릿 인터페이스만 구현하면 서블릿 컨테이너가 생성, 소멸 등의 생명주기 관리 작업을 수행할 수 있다. 그런데 서블릿 컨테이너가 서블릿 관리를 위해 필요한 기능은 서블릿 스펙에 모두 정의돼 있으므로 서블릿 명세는 서블릿에 필요한 구현을 미리 작성해 GenericServlet이란 이름으로 제공한다. 이 GenericServlet 클래스는 추상 클래스지만 abstract void service(ServletRequest req, ServletResponse res)를 제외하고는 모두 구현된 일종의 서블릿을 위한 어댑터 역할(서블릿 상태 변경 이벤트 리스너, 서블릿 초기화 매개변수 등 정보성 데이터에 대한 접근 등)을 제공한다.

 

HttpServlet

protected void doDelete(HttpServletRequest req, HttpServletResponse resp)
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
protected void doHead(HttpServletRequest req, HttpServletResponse resp)
protected void doOptions(HttpServletRequest req, HttpServletResponse resp)

일반적으로 서블릿이라 하면 거의 대부분 이 HttpServlet을 상속받은 서블릿을 의미한다. HttpServlet은 GenericServlet을 상속받으며, GenericServlet의 유일한 추상 메서드인 service를 HTTP 프로토콜 요청 메서드에 적합하게 재구현한 것이다. HTTP 프로토콜의 요청 메서드인 Delete, Get, Head, Options, Post, Put, Trace를 처리하는 메서드가 모두 정의되어 있다.

 

HttpServlet의 service 메서드

서블릿 컨테이너는 받은 요청에 대해 서블릿을 선택한 후 Servlet 인터페이스에 정의된 service(ServletRequest, ServletResponse)를 호출한다. 그러면 클래스 상속 위계에 따라 그 처리가 부모 클래스인 GenericServlet에서 자식 클래스인 HttpServlet으로 넘어온다. HttpServlet의 service 메서드 내에서는 HTTP 요청 메서드에 의해 여러 doXXX 메서드로 분기되어 처리된다. HttpServlet을 상속받아 웹 프로그래머스 작성한 서블릿은 doXXX 메서드를 다시 오버라이딩해 HTTP 메서드별로 서로 다른 처리를 수행한다. 

 

DispatcherServlet

DispatcherServlet은 웹 애플리케이션의 진입점이며, 모든 클라이언트 요청을 받아들이고 적절한 컨트롤러로 라우팅하면서 뷰를 렌더링하여 클라이언트에게 응답을 제공하는 주요한 서블릿이다. DispatcherServlet은 Servlet WebApplicationContext를 생성하고 관리한다. 또한, Root WebApplicationContext와 연계하여 전체 애플리케이션 컨텍스트를 형성하며, 필요한 빈들은 Root WebApplicationContext에서 가져와 사용할 수 있다.

클래스 내부에 핸들러, 어댑터, 리졸버 등을 가지고 있어 요청에 대한 응답이 가능하다. 이들을 인터페이스로 가지고 있으며 생성되는 시점에 ApplicationContext에서 빈들을 주입받아 생성된다. 즉, Spring이 생성하는 Servlet이다.

  • HandlerMapping: 클라이언트의 HTTP 요청을 어떤 Controller가 처리할 것인지를 결정하는 역할을 한다.
  • HandlerAdapter: HandlerMapping을 통해 선택된 Controller를 실행하는 역할을 한다.
    • 다양한 종류의 컨트롤러(Controller) 타입을 지원하며, 각 컨트롤러 타입에 따라 요청을 처리하는 방식을 다르게 구현할 수 있다.
  • ViewResolver: 컨트롤러가 반환한 뷰 이름을 실제 View 객체로 변환하여 제공하는 역할을 한다.
    • 뷰는 클라이언트에게 보여질 화면을 나타내며, 주로 JSP, Thymeleaf와 같은 템플릿 엔진을 사용하여 생성된다.

 

Dispatcher Servlet의 흐름

DispatcherServlet도 부모 클래스에서 HttpServlet을 상속받아서 사용하고, 서블릿으로 동작한다. 스프링 MVC는 DispatcherServlet의 부모님 FrameworkServlet에서 service() 메서드를 오버라이딩해두었다. FrameworkServlet.service()를 시작으로 여러 메서드가 호출되면서 DispatcherServlet.doDispatch()가 호출된다.

 

참고: DispatcherServlet의 상속관계
DispatcherServlet -> FramworkServlet -> HttpServletBean -> HttpServlet

 

DispatchServlet.doDispatch()

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
    HttpServletRequest processedRequest = request;
    HandlerExecutionChain mappedHandler = null;
    ModelAndView mv = null;
    
    // 1. 핸들러 조회
    mappedHandler = getHandler(processedRequest); if (mappedHandler == null) {
        noHandlerFound(processedRequest, response);
        return;
    }
    
    // 2.핸들러 어댑터 조회-핸들러를 처리할 수 있는 어댑터
    HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

    // 3. 핸들러 어댑터 실행 -> 4. 핸들러 어댑터를 통해 핸들러 실행 -> 5. ModelAndView 반환
    mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
    
    processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}

private void processDispatchResult(HttpServletRequest request, HttpServletResponse response, HandlerExecutionChain mappedHandler, ModelAndView mv, Exception exception) throws Exception {
         
    // 뷰 렌더링 호출
    render(mv, request, response);
}
  
protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
    View view;
    String viewName = mv.getViewName(); // 6. 뷰 리졸버를 통해서 뷰 찾기,7.View 반환
    view = resolveViewName(viewName, mv.getModelInternal(), locale, request);
    
    // 8. 뷰 렌더링
    view.render(mv.getModelInternal(), request, response);
}

 

  1. 핸들러 조회: 핸들러 매핑을 통해 요청 URL에 매핑된 핸들러(컨트롤러)를 조회한다.
  2. 핸들러 어댑터 조회: 핸들러를 실행할 수 있는 핸들러 어댑터를 조회한다.
  3. 핸들러 어댑터 실행: 핸들러 어댑터를 실행한다. 이 때, 핸들러 어댑터에 핸들러(컨트롤러)를 넘긴다.
  4. ModelAndView 반환: 핸들러 어댑터를 핸들러가 반환하는 정보를 ModelAndView로 변환해서 반환한다.
  5. viewResolver 호출: 뷰 리졸버를 찾고 실행한다.
  6. View 반환: 뷰 리졸버는 뷰의 놀리 이름을 물리 이름으로 바꾸고, 렌더링 역할을 담당하는 뷰 객체를 반환한다.
  7. 뷰 렌더링: 뷰를 통해서 뷰를 렌더링한다.

 

@ResponseBody 애노테이션 작동원리

 

  1. 클라이언트가 HTTP 요청을 보낸다.
  2. Spring MVC는 DispatcherServlet을 통해 요청을 받아들인다.
  3. DispatcherServlet은 요청을 처리할 핸들러(컨트롤러)를 찾는다.
  4. @ResponseBody 애노테이션이 적용된 메서드를 찾으면, 해당 메서드의 반환값을 HTTP 응답본문으로 사용하기 위해 Spring MVC의 HTTP 메시지 컨버터를 활용한다.
  5. HTTP 메시지 컨버터는 반환값을 적절한 형식(JSON, XML 등)으로 변환한다.
  6. 변환된 데이터는 HTTP 응답으로 클라이언트에게 전송된다.

우리가 @ResponseBody를 붙이면 위와 같은 절차를 거치게 되고, 이러한 처리를 해주는 것이 바로 DispatcherServlet이다!

 

Spring Container

Spring Container는 웹 애플리케이션의 비즈니스 로직을 관리하고 제어하기 위한 컨테이너이고, 주요 역할은 다음과 같다.

  • 객체의 생성과 소멸, 의존성 주입(Dependency Injection)을 담당한다.
  • 라이프사이클 관리를 통해 빈(Bean) 객체의 초기화와 소멸을 처리한다.
  • AOP(Aspect-Oriented Programming)와 같은 기능을 통해 애플리케이션에 횡단 관심사(Cross-Cutting Concerns)를 적용할 수 있도록 지원한다.
  • 트랜잭션 관리, 보안, 캐시, 인터셉터 등 다양한 기능을 제공하여 개발자가 애플리케이션을 더 쉽게 관리할 수 있도록 한다.

서블릿의 생명주기를 관리하는 것이 서블릿 컨테이너였다면 스프링 컨테이너는 빈의 생명주기를 관리하며 IoC/DI를 제공해주는 역할을 수행한다.

 

WebApplicationContext

애플리케이션 컨텍스트는 IoC 방식을 따라 만들어진 일종의 빈 팩토리라고 생각하면 된다. 여기서 빈 팩토리란 스프링에서 빈의 생성과 관계설정 같은 제어를 담당하는 IoC 오브젝트로 애플리케이션 컨텍스트이 바로 이 빈 팩토리를 확장한 IoC 컨테이너이다.

 

Root WebApplicationContext

Root WebApplicationContext는 전체 웹 애플리케이션에 대한 상위 컨텍스트로 서비스 계층, 데이터 소스 설정, 리포지토리(데이터 엑세스 계층)와 같은 애플리케이션의 핵심 비즈니스 로직 및 데이터 액세스를 담당하는 빈들을 포함하는 컨텍스트이다.

Root WebApplicationContext는 데이터베이스 연결, 트랜잭션 관리, 서비스 빈 등과 같은 애플리케이션 전반에 걸친 빈을 설정하고, 여러 개의 Servlet WebApplicationContext가 공유할 수 있는 빈을 정의하며, 이로 인해 중복 설정을 방지하고 메모리 사용을 최적화한다. 보안, 로깅, 캐싱, 인증 등과 같은 공통 관심 사항을 다룬다.

 

Servlet WebApplcationContext

Servlet WebApplicationContext는 DispatcherServlet에 의해 생성되는 웹 요청마다 하나씩 생성되는 컨텍스트이다. 따라서 각 웹 요청이 자체적인 ApplicationContext를 갖을 수 있다.

이러한 Servlet WebApplicationContext는 주로 웹 요청 처리를 위한 Controller, Interceptor, ViewResolver, HandlerMapping 등과 같은 웹 계층에 필요한 빈들을 정의한다. Root WebApplicationContext를 상속받아, Root WebApplicationContext에 정의된 빈들을 재사용하거나 확장할 수 있다.

 

서버 시작과 요청까지의 단계

  1. Web Server init
  2. Root WebApplicationContext 로딩
  3. Web Server start
  4. Client가 Web Server로 Request
  5. Web Server가 Servlet Container로 전달
  6. Servlet 스레드 생성
  7. 서블릿이 생성안되어 있다면 DispatcherServlet init
  8. 생성된 스레드에서 DispatcherServlet의 service() 메서드 호출
  9. Handlermappding을 통해 컨트롤러 조회
  10. HandlerAdaptor를 통해 컨트롤러에게 전달
  11. Controller -> Service -> 동작 후 응답

 

 

 

출처

웹 프로그래머를 위한 서블릿 컨테이너의 이해

Servlet Container와 Servlet의 관계

Web Server, Application Server, WAS, Web Container(=Servlet Container)

스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술

[Backend] 서블릿

요청처리 내부구조

[JSP/Servlet] Servlet 생명주기(Life Cycle)