2023. 5. 2. 11:32ㆍSpring Framework/Web on Servlet Stack
DispatcherServlet
Spring MVC는 많은 다른 웹 프레임워크처럼 프론트 컨트롤러 패턴을 기반으로 설계되었습니다. 여기서 중앙 서블릿인 DispatcherServlet이 공유된 알고리즘을 사용하여 요청을 처리하고, 실제 작업은 구성 가능한 위임 컴포넌트들에 의해 수행됩니다. 이 모델은 유연하여 다양한 워크플로우를 지원합니다.
DispatcherServlet
은 다른 서블릿과 마찬가지로 서블릿 명세에 따라 선언되고 매핑되어야 합니다. 이를 위해 Java 설정이나 web.xml
을 사용할 수 있습니다. 설정이 완료되면, DispatcherServlet
은 Spring 설정을 통해 필요한 위임 컴포넌트들을 찾습니다. 예를 들어, http request를 처리하는 방법이나 뷰를 어떻게 보여줄지, 예외를 어떻게 처리할지 등을 결정하는 데 필요한 컴포넌트들이 있습니다.
다음은 Java 구성 예제로, 서블릿 컨테이너가 자동으로 감지하는 DispatcherServlet
을 등록하고 초기화하는 방법을 보여줍니다:
public class MyWebApplicationInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext servletContext) {
// Spring 웹 애플리케이션 구성 로드
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
context.register(AppConfig.class);
// DispatcherServlet 생성 및 등록
DispatcherServlet servlet = new DispatcherServlet(context);
ServletRegistration.Dynamic registration = servletContext.addServlet("app", servlet);
registration.setLoadOnStartup(1);
registration.addMapping("/app/*");
}
}
이 예제는 서블릿 컨테이너에서 DispatcherServlet
을 자동으로 감지하고 설정하는 과정을 보여줍니다.
- AnnotationConfigWebApplicationContext: Spring 설정을 불러오는 역할을 합니다.
- DispatcherServlet: 이 서블릿이 모든 요청을 처리하며, Spring의 다양한 컴포넌트를 사용해 실제 작업을 수행합니다.
- 등록 과정: 서블릿 컨테이너에 이 서블릿을 등록하고,
/app/*
경로에 매핑합니다.
이렇게 하면 Spring MVC 애플리케이션의 요청 처리가 가능해집니다.
`ServletContext` API를 직접 사용하는 것 외에도 `AbstractAnnotationConfigDispatcherServletInitializer`를 확장하고 특정 메서드를 오버라이드할 수 있습니다(컨텍스트 계층 구조 예제 참고).
프로그램적으로 사용하는 경우, `GenericWebApplicationContext`를 `AnnotationConfigWebApplicationContext` 대신 사용할 수 있습니다. 자세한 내용은 `GenericWebApplicationContext` javadoc을 참조하세요.
다음 web.xml
구성 예제는 DispatcherServlet
을 등록하고 초기화합니다:
<web-app>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/app-context.xml</param-value>
</context-param>
<servlet>
<servlet-name>app</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value></param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>app</servlet-name>
<url-pattern>/app/*</url-pattern>
</servlet-mapping>
</web-app>
Spring Boot는 다른 초기화 순서를 따릅니다. 서블릿 컨테이너의 라이프사이클에 연결하는 대신 Spring Boot는 Spring 구성을 사용해 스스로와 내장 서블릿 컨테이너를 부트스트랩합니다. 필터와 서블릿 선언은 Spring 구성에서 감지되어 서블릿 컨테이너에 등록됩니다. 자세한 내용은 Spring Boot 문서를 참조하세요.
Context Hierarchy
`DispatcherServlet`은 자체 구성을 위해 `WebApplicationContext`(일반 `ApplicationContext`의 확장 버전)를 필요로 합니다. `WebApplicationContext`는 `ServletContext` 및 해당 서블릿과 연결되어 있으며, 정적 메서드를 통해 애플리케이션에서 필요할 때 접근할 수 있습니다.
많은 애플리케이션에서는 단일 WebApplicationContext
만 있으면 충분합니다. 하지만 컨텍스트 계층 구조를 설정할 수도 있는데, 이는 하나의 루트 WebApplicationContext
가 여러 DispatcherServlet
또는 다른 서블릿 인스턴스와 공유되고, 각 서블릿은 별도의 자식 WebApplicationContext
를 가집니다. 루트 컨텍스트는 여러 서블릿이 공유하는 인프라 빈(예: 데이터 리포지토리, 비즈니스 서비스 등)을 포함하고, 자식 컨텍스트는 각 서블릿에 고유한 빈을 포함합니다. 자식 컨텍스트에서는 루트 컨텍스트의 빈을 상속받아 사용할 수 있지만, 필요시 재정의할 수도 있습니다.
이 구조는 여러 서블릿 간에 공통 리소스를 공유하면서도, 각 서블릿이 고유한 설정을 유지할 수 있도록 해줍니다.
다음 예제는 WebApplicationContext 계층 구조를 구성합니다:
public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class<?>[] { RootConfig.class };
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class<?>[] { App1Config.class };
}
@Override
protected String[] getServletMappings() {
return new String[] { "/app1/*" };
}
}
애플리케이션 컨텍스트 계층 구조가 필요하지 않은 경우, 모든 설정을 `getRootConfigClasses()` 메서드를 통해 반환하고,
getServletConfigClasses()\
메서드에서는 `null`을 반환할 수 있습니다.
The following example shows the web.xml equivalent:
<web-app>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/root-context.xml</param-value>
</context-param>
<servlet>
<servlet-name>app1</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/app1-context.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>app1</servlet-name>
<url-pattern>/app1/*</url-pattern>
</servlet-mapping>
</web-app>
애플리케이션 컨텍스트 계층 구조가 필요하지 않은 경우, 애플리케이션은 "루트" 컨텍스트만 구성하고
contextConfigLocation
서블릿 파라미터를 비워둘 수 있습니다.
Special Bean Types
`DispatcherServlet`은 http request를 처리하고 적절한 http response을 렌더링하기 위해 특별한 빈에 위임합니다. 여기서 "Special Bean"이란 Spring이 관리하는 객체 인스턴스를 의미하며, 이들은 프레임워크 contact\[interface\]을 구현합니다. 이들 빈은 일반적으로 기본 인터페이스를 갖고 있지만, 속성을 커스터마이징하거나 확장 또는 교체할 수 있습니다.
다음 표는 DispatcherServlet
에 의해 감지되는 Speical Bean을 나열합니다:
빈 타입 | 설명 |
---|---|
HandlerMapping | 요청을 핸들러에 매핑하고, 요청 전후에 실행될 인터셉터 목록을 연결합니다. 매핑은 특정 기준에 따라 이루어지며, 각 HandlerMapping 구현에 따라 세부 사항이 다를 수 있습니다. 주로 사용하는 두 가지 구현은 다음과 같습니다: - RequestMappingHandlerMapping: @RequestMapping 주석이 달린 메서드를 지원하여 RESTful 요청을 처리합니다. - SimpleUrlHandlerMapping: URI 경로 패턴을 핸들러에 명시적으로 등록하여 요청을 처리합니다. |
HandlerAdapter | 요청에 매핑된 핸들러를 호출하는 데 도움을 줍니다. 핸들러가 어떻게 호출되는지와 관계없이 DispatcherServlet이 핸들러를 호출할 수 있도록 해줍니다. 예를 들어, 주석이 달린 컨트롤러를 호출하려면 주석을 해석해야 합니다. HandlerAdapter 의 주요 목적은 이러한 세부사항으로부터 DispatcherServlet 을 보호하는 것입니다. |
HandlerExceptionResolver | 예외를 해결하기 위한 전략을 제공합니다. 핸들러에 매핑하거나 HTML 오류 뷰 또는 기타 대상을 반환할 수 있습니다. 이 빈은 애플리케이션에서 발생한 예외를 처리하는 데 중요한 역할을 합니다. |
ViewResolver | 핸들러에서 반환된 문자열 기반의 논리적 뷰 이름을 실제 뷰로 변환하여 응답을 렌더링하는 데 사용합니다. 뷰 해상도(View Resolution) 및 뷰 기술에 대한 세부사항은 여기에서 다룹니다. |
LocaleResolver, LocaleContextResolver | 클라이언트가 사용하는 로케일과 시간대를 확인하여 국제화된 뷰를 제공할 수 있게 해줍니다. 이를 통해 사용자에게 맞춤형 콘텐츠를 제공할 수 있습니다. |
ThemeResolver | 웹 애플리케이션에서 사용할 수 있는 테마를 해결합니다. 예를 들어, 개인화된 레이아웃을 제공할 수 있습니다. |
MultipartResolver | 브라우저 폼 파일 업로드와 같은 멀티파트 요청을 분석하기 위한 추상화입니다. 멀티파트 파싱 라이브러리를 사용하여 요청을 처리합니다. |
FlashMapManager | "입력" 및 "출력" FlashMap을 저장하고 검색하여, 주로 리다이렉트를 통해 한 요청에서 다른 요청으로 속성을 전달하는 데 사용합니다. Flash 속성에 대한 처리를 지원합니다. |
이 표는 각 빈의 역할과 기능을 명확하게 나타내어, DispatcherServlet
의 작동 방식을 이해하는 데 도움이 됩니다.
Web MVC Config
애플리케이션은 요청 처리를 위해 필요한 특별한 빈 유형(Special Bean Types)을 선언할 수 있습니다. DispatcherServlet
은 각 특별한 빈에 대해 WebApplicationContext
를 확인합니다. 만약 해당하는 빈 유형이 없다면, 기본적으로 DispatcherServlet.properties
에 나열된 기본 유형으로 대체됩니다.
대부분의 경우, MVC 구성(MVC Config)이 가장 좋은 출발점입니다. 이는 필요한 빈을 Java 또는 XML로 선언하고, 이를 사용자 정의하기 위한 더 높은 수준의 구성 콜백 API를 제공합니다.
Spring Boot는 Spring MVC를 구성하기 위해 MVC Java 구성을 기반으로 하며, 여러 가지 추가적인 편리한 옵션을 제공합니다.
Servlet Config
Servlet 환경에서는 web.xml
파일을 사용하는 것 외에도 Servlet 컨테이너를 프로그래밍 방식으로 구성할 수 있는 옵션이 있습니다. 아래는 DispatcherServlet
을 등록하는 예제입니다.
DispatcherServlet 등록 예제
Java 예제:
import org.springframework.web.WebApplicationInitializer;
public class MyWebApplicationInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext container) {
XmlWebApplicationContext appContext = new XmlWebApplicationContext();
appContext.setConfigLocation("/WEB-INF/spring/dispatcher-config.xml");
ServletRegistration.Dynamic registration = container.addServlet("dispatcher", new DispatcherServlet(appContext));
registration.setLoadOnStartup(1);
registration.addMapping("/");
}
}
Kotlin 예제:
import org.springframework.web.WebApplicationInitializer
class MyWebApplicationInitializer : WebApplicationInitializer {
override fun onStartup(container: ServletContext) {
val appContext = XmlWebApplicationContext()
appContext.setConfigLocation("/WEB-INF/spring/dispatcher-config.xml")
val registration = container.addServlet("dispatcher", DispatcherServlet(appContext))
registration.setLoadOnStartup(1)
registration.addMapping("/")
}
}
WebApplicationInitializer
WebApplicationInitializer
는 Spring MVC가 제공하는 인터페이스로, 구현이 자동으로 감지되어 Servlet 3 컨테이너를 초기화하는 데 사용됩니다.
- AbstractDispatcherServletInitializer라는 추상 기본 클래스는
DispatcherServlet
을 등록하는 과정을 간소화합니다. 이 클래스의 메서드를 오버라이드하여 서블릿 매핑 및 설정 위치를 지정할 수 있습니다.
Java 기반 Spring 구성의 경우
public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return null; // 루트 설정 클래스 없음
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class<?>[] { MyWebConfig.class }; // 서블릿 설정 클래스
}
@Override
protected String[] getServletMappings() {
return new String[] { "/" }; // 매핑 경로
}
}
XML 기반 Spring 구성의 경우
public class MyWebAppInitializer extends AbstractDispatcherServletInitializer {
@Override
protected WebApplicationContext createRootApplicationContext() {
return null; // 루트 애플리케이션 컨텍스트 없음
}
@Override
protected WebApplicationContext createServletApplicationContext() {
XmlWebApplicationContext cxt = new XmlWebApplicationContext();
cxt.setConfigLocation("/WEB-INF/spring/dispatcher-config.xml");
return cxt; // XML 설정 사용
}
@Override
protected String[] getServletMappings() {
return new String[] { "/" }; // 매핑 경로
}
}
필터 추가
AbstractDispatcherServletInitializer
는 필터 인스턴스를 추가하고 이를 자동으로 DispatcherServlet
에 매핑할 수 있는 편리한 방법도 제공합니다.
public class MyWebAppInitializer extends AbstractDispatcherServletInitializer {
@Override
protected Filter[] getServletFilters() {
return new Filter[] {
new HiddenHttpMethodFilter(),
new CharacterEncodingFilter()
}; // 필터 배열 반환
}
}
각 필터는 그 유형에 따라 기본 이름으로 추가되며 자동으로 DispatcherServlet
에 매핑됩니다.
비동기 지원
AbstractDispatcherServletInitializer
의 isAsyncSupported
메서드는 DispatcherServlet
과 이를 매핑된 모든 필터에 대해 비동기 지원을 활성화하는 단일 위치를 제공합니다. 기본적으로 이 플래그는 true로 설정됩니다.
DispatcherServlet 사용자 정의
DispatcherServlet
자체를 추가로 사용자 정의해야 하는 경우, createDispatcherServlet
메서드를 오버라이드하여 설정할 수 있습니다.
Processing
DispatcherServlet 요청 처리 과정
DispatcherServlet
은 다음과 같은 단계로 요청을 처리합니다:
- WebApplicationContext 검색 및 바인딩
- 요청 처리 과정에서
WebApplicationContext
가 검색되어 요청의 속성으로 바인딩됩니다. 이 컨텍스트는 컨트롤러 및 다른 구성 요소들이 사용할 수 있습니다. 기본적으로DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE
라는 키로 바인딩됩니다.
- 요청 처리 과정에서
- locale resolver 바인딩
- 요청에 로케일 리졸버가 바인딩되어, 요청 처리 시 어떤 로케일을 사용할지 결정합니다(예: 뷰 렌더링, 데이터 준비 등). 로케일 처리가 필요하지 않은 경우, 로케일 리졸버는 필요하지 않습니다.
- theme resolver 바인딩
- 요청에 테마 리졸버가 바인딩되어, 뷰와 같은 요소들이 사용할 테마를 결정합니다. 테마를 사용하지 않는 경우, 이를 무시할 수 있습니다.
- multipart file resolver 검사
- 멀티파트 파일 리졸버를 지정한 경우, 요청이 멀티파트인지 검사합니다. 멀티파트가 발견되면, 요청은
MultipartHttpServletRequest
로 래핑되어 다른 요소들이 추가 처리를 할 수 있도록 합니다. 멀티파트 처리에 대한 자세한 내용은Multipart Resolver
를 참조하십시오.
- 멀티파트 파일 리졸버를 지정한 경우, 요청이 멀티파트인지 검사합니다. 멀티파트가 발견되면, 요청은
- Handler 검색
- 적절한 핸들러를 검색합니다. 핸들러가 발견되면, 핸들러와 관련된 실행 체인(전처리기, 후처리기, 컨트롤러)이 실행되어 렌더링할 모델을 준비합니다. 또는, 주석이 달린 컨트롤러의 경우, 뷰를 반환하는 대신
HandlerAdapter
내에서 응답을 렌더링할 수 있습니다.
- 적절한 핸들러를 검색합니다. 핸들러가 발견되면, 핸들러와 관련된 실행 체인(전처리기, 후처리기, 컨트롤러)이 실행되어 렌더링할 모델을 준비합니다. 또는, 주석이 달린 컨트롤러의 경우, 뷰를 반환하는 대신
- Model 반환 및 View Rendering
- 모델이 반환되면 뷰가 렌더링됩니다. 반대로, 모델이 반환되지 않는 경우(전처리기나 후처리기에 의해 요청이 차단되었을 수 있음), 뷰는 렌더링되지 않습니다. 이는 요청이 이미 처리되었을 수 있음을 의미합니다.
- 예외 해결
- 요청 처리 중에 발생한 예외는
WebApplicationContext
에 선언된HandlerExceptionResolver
빈을 사용하여 해결합니다. 이러한 예외 리졸버는 예외를 처리하기 위한 사용자 정의 로직을 허용합니다. 자세한 내용은Exceptions
를 참조하십시오.
- 요청 처리 중에 발생한 예외는
- HTTP 캐싱 지원
- 핸들러는
WebRequest
의checkNotModified
메서드를 사용하여 HTTP 캐싱을 지원할 수 있으며, 주석이 달린 컨트롤러에 대한 추가 옵션이 제공됩니다. 이와 관련된 내용은HTTP Caching for Controllers
를 참조하십시오.
- 핸들러는
DispatcherServlet 초기화 파라미터
DispatcherServlet
인스턴스를 사용자 정의하려면, web.xml
파일의 서블릿 선언에 Servlet 초기화 파라미터(init-param)를 추가할 수 있습니다. 다음 표는 지원되는 파라미터를 설명합니다.
파라미 | 설명 |
---|---|
contextClass | ConfigurableWebApplicationContext 를 구현하는 클래스. 기본적으로 XmlWebApplicationContext 가 사용됩니다. |
contextConfigLocation | 컨텍스트 인스턴스에 전달되는 문자열로, 컨텍스트가 위치하는 곳을 나타냅니다. 여러 문자열을 사용하여 여러 컨텍스트를 지원할 수 있으며, 동일한 빈이 정의된 경우 최신 위치가 우선합니다. |
namespace | WebApplicationContext 의 네임스페이스입니다. 기본값은 [servlet-name]-servlet 입니다. |
throwExceptionIfNoHandlerFound | 요청에 대한 핸들러가 발견되지 않았을 때 NoHandlerFoundException 을 발생시킬지 여부를 결정합니다. 이 예외는 HandlerExceptionResolver 로 처리할 수 있습니다. (예: @ExceptionHandler 메서드 사용) 현재 6.1 버전에서 기본값은 true이며, 더 이상 사용되지 않습니다. |
기타 주의사항 | 기본 서블릿 처리가 구성된 경우, 해결되지 않은 요청은 항상 기본 서블릿으로 전달되며, 404 오류는 발생하지 않습니다. |
이렇게 DispatcherServlet
은 요청을 처리하고, 요청 처리 과정에서 발생하는 다양한 요소들을 바인딩하고 관리합니다.
Path Matching
Servlet API
는 전체 요청 경로를 requestURI
로 노출하며, 이를 contextPath
, servletPath
, pathInfo
로 세분화합니다. 이 값들은 서블릿이 어떻게 매핑되었는지에 따라 달라집니다. Spring MVC는 이러한 입력을 바탕으로 핸들러 매핑에 사용할 조회 경로(lookup path)를 결정해야 하며, 이 때 contextPath
와 서블릿 매핑 접두사를 제외해야 합니다.
경로 처리의 문제점
- 서블릿 경로와 경로 정보의 디코딩
servletPath
와pathInfo
는 디코딩되어 있어, 전체requestURI
와 직접 비교하여 조회 경로를 유도하기가 어렵습니다. 그래서requestURI
를 디코딩해야 합니다. 하지만 이는 보안 문제를 유발할 수 있는 인코딩된 예약 문자가 포함된 경로 구조를 변경할 수 있습니다.- 예를 들어, 예약 문자인 "/" 또는 ";"가 디코딩될 경우, 경로 구조가 변경될 수 있습니다. 게다가, 서블릿 컨테이너는
servletPath
를 다양한 정도로 정규화하므로requestURI
와의 startsWith 비교가 어려워집니다.
- 서블릿 경로 사용의 불리함
- 이러한 이유로, 서블릿 경로에 대한 의존도를 피하는 것이 좋습니다. 만약
DispatcherServlet
이 기본 서블릿으로 "/" 또는 "/*"로 매핑되어 있고 서블릿 컨테이너가 4.0 이상인 경우, Spring MVC는 서블릿 매핑 유형을 감지하고servletPath
와pathInfo
의 사용을 피할 수 있습니다. - 3.1 서블릿 컨테이너의 경우, 동일한 서블릿 매핑 유형을 사용하면, MVC 구성에서
alwaysUseFullPath=true
를 통해UrlPathHelper
를 제공하여 동일한 효과를 얻을 수 있습니다.
- 이러한 이유로, 서블릿 경로에 대한 의존도를 피하는 것이 좋습니다. 만약
- 요청 URI 디코딩의 문제점
- 기본 서블릿 매핑 "/"는 좋은 선택이지만, 여전히 요청 URI를 디코딩해야 컨트롤러 매핑과 비교할 수 있습니다. 그러나 이는 예약 문자를 디코딩함으로써 경로 구조를 변경할 수 있다는 위험이 있습니다. 이러한 문자가 예상되지 않는 경우(예: Spring Security HTTP 방화벽처럼) 이를 거부하거나,
UrlPathHelper
를urlDecode=false
로 구성할 수 있습니다. 그러나 이 경우 컨트롤러 매핑은 인코딩된 경로와 일치해야 하므로 잘 작동하지 않을 수 있습니다. - 게다가,
DispatcherServlet
이 다른 서블릿과 URL 공간을 공유해야 하거나 매핑이 필요할 경우 접두사로 매핑해야 할 수 있습니다.
- 기본 서블릿 매핑 "/"는 좋은 선택이지만, 여전히 요청 URI를 디코딩해야 컨트롤러 매핑과 비교할 수 있습니다. 그러나 이는 예약 문자를 디코딩함으로써 경로 구조를 변경할 수 있다는 위험이 있습니다. 이러한 문자가 예상되지 않는 경우(예: Spring Security HTTP 방화벽처럼) 이를 거부하거나,
PathPatternParser의 도입
이러한 문제는 Spring MVC의 5.3 버전부터 사용 가능한 PathPatternParser
와 파싱된 패턴을 사용함으로써 해결됩니다. 이는 기본적으로 6.0 버전에서 활성화됩니다.
- PathPatternParser의 장점
AntPathMatcher
와 달리,PathPatternParser
는 조회 경로를 디코딩하거나 컨트롤러 매핑을 인코딩할 필요 없이 패턴을 매칭할 수 있습니다. 이는 요청 경로(RequestPath)라고 불리는 경로의 파싱된 표현에 기반하여 경로의 각 세그먼트를 개별적으로 매칭합니다.- 이를 통해 경로 세그먼트 값을 개별적으로 디코딩하고 정리할 수 있으며, 경로 구조를 변경할 위험 없이 처리할 수 있습니다.
- 또한, 파싱된 패턴은 서블릿 경로 매핑을 접두사로 사용하는 것을 지원하며, 이 때 접두사는 간단해야 하며 인코딩된 문자가 없어야 합니다.
이렇게 `PathPatternParser`와 파싱된 패턴을 사용함으로써 경로 일치의 문제를 효과적으로 해결할 수 있습니다. 이는 Spring MVC의 경로 처리 방식에 유연성과 안정성을 추가합니다.
Interception
인터셉션은 모든 HandlerMapping
구현에서 지원되며, 요청 간에 기능을 적용할 때 유용합니다. HandlerInterceptor
를 사용하여 특정 요청 처리 전후에 코드를 실행할 수 있습니다. 주요 메서드는 다음과 같습니다:
preHandle(..)
- 이 메서드는 실제 핸들러가 실행되기 전에 호출됩니다.
- 반환값이
true
이면, 요청 처리 체인이 계속 진행되고, 핸들러가 호출됩니다. - 반환값이
false
이면, 나머지 실행 체인이 건너뛰어지며 핸들러가 호출되지 않습니다. - 주로 요청 인증, 세션 검사 등의 기능을 구현하는 데 사용됩니다.
postHandle(..)
- 이 메서드는 핸들러가 실행된 후에 호출됩니다.
- 이 시점에서 모델 데이터나 뷰 정보를 조작할 수 있습니다.
- 주로 로깅이나 추가 데이터 추가 등을 위한 용도로 사용됩니다.
afterCompletion(..)
- 이 메서드는 요청 처리가 완전히 종료된 후에 호출됩니다.
- 주로 리소스 정리, 로그 기록 등을 위한 용도로 사용됩니다.
주의사항
- @ResponseBody 및 ResponseEntity 사용 시:
@ResponseBody
또는ResponseEntity
를 사용하는 컨트롤러 메서드의 경우, 응답은HandlerAdapter
내에서 작성되고 커밋됩니다. 이 때문에postHandle
이 호출되기 전에 이미 응답이 완료되어, 추가 헤더를 추가하는 등의 변경은 불가능합니다.- 이러한 경우에는
ResponseBodyAdvice
를 구현하고 이를Controller Advice
빈으로 선언하거나RequestMappingHandlerAdapter
에 직접 구성하여 사용해야 합니다.
인터셉터 구성
- 인터셉터를 구성하는 방법에 대한 예시는 MVC 구성의 Interceptors 섹션을 참고하면 됩니다.
- 또한, 개별
HandlerMapping
구현에서 setter를 사용하여 인터셉터를 직접 등록할 수도 있습니다.
경고
- 인터셉터는 보안 계층으로 사용하는 것이 이상적이지 않습니다. 왜냐하면 주석 기반 컨트롤러의 경로 매칭과의 불일치가 발생할 수 있기 때문입니다.
- 일반적으로 Spring Security를 사용하는 것을 추천하며, 대안으로 Servlet 필터 체인과 통합된 유사한 접근 방식을 조기에 적용하는 것이 좋습니다.
인터셉터는 요청의 전처리, 후처리 및 요청 완료 후 처리를 가능하게 하여, 코드 재사용성과 요청 흐름의 제어를 쉽게 해줍니다. 하지만 보안 기능을 구현할 때는 다른 메커니즘을 사용하는 것이 바람직합니다.
Exceptions
Spring MVC에서 요청 매핑 중에 예외가 발생하거나 요청 핸들러(@Controller)에서 예외가 던져지면, DispatcherServlet
은 HandlerExceptionResolver
빈 체인에 위임하여 예외를 처리합니다. 이 과정에서 대개는 오류 응답을 제공합니다.
HandlerExceptionResolver 구현 목록
다음 표는 사용 가능한 HandlerExceptionResolver
구현 목록입니다:
HandlerExceptionResolver | 설명 |
---|---|
SimpleMappingExceptionResolver | 예외 클래스 이름과 오류 뷰 이름 간의 매핑을 제공합니다. 브라우저 애플리케이션에서 오류 페이지를 렌더링하는 데 유용합니다. |
DefaultHandlerExceptionResolver | Spring MVC에서 발생한 예외를 해결하고 이를 HTTP 상태 코드에 매핑합니다. Alternative ResponseEntityExceptionHandler 및 오류 응답을 참조하십시오. |
ResponseStatusExceptionResolver | @ResponseStatus 주석이 있는 예외를 해결하고, 주석의 값에 따라 HTTP 상태 코드에 매핑합니다. |
ExceptionHandlerExceptionResolver | @Controller 또는 @ControllerAdvice 클래스 내의 @ExceptionHandler 메서드를 호출하여 예외를 해결합니다. |
예외 해결 체인 (Chain of Resolvers)
여러 HandlerExceptionResolver
빈을 Spring 구성에 선언하고 순서 속성을 설정하여 예외 해결 체인을 형성할 수 있습니다. 순서 속성이 높을수록 해당 예외 해결자가 늦게 위치하게 됩니다.
HandlerExceptionResolver
계약에 따르면, 다음을 반환할 수 있습니다:
- 오류 뷰를 가리키는
ModelAndView
. - 해결자가 예외를 처리한 경우 빈
ModelAndView
. - 예외가 해결되지 않은 경우
null
을 반환하여 다음 해결자가 시도할 수 있도록 합니다. 최종적으로 예외가 해결되지 않으면 Servlet 컨테이너로 다시 전파됩니다.
MVC 구성은 기본 Spring MVC 예외, @ResponseStatus 주석이 있는 예외, @ExceptionHandler 메서드에 대한 지원을 자동으로 선언합니다. 이 목록을 사용자 정의하거나 대체할 수 있습니다.
컨테이너 오류 페이지 (Container Error Page)
어떤 HandlerExceptionResolver
에 의해서도 예외가 해결되지 않으면, 예외가 전파되거나 응답 상태가 오류 상태(예: 4xx, 5xx)로 설정됩니다. 이 경우, Servlet 컨테이너는 HTML 기본 오류 페이지를 렌더링할 수 있습니다. 기본 오류 페이지를 사용자 정의하려면 web.xml
에 오류 페이지 매핑을 선언해야 합니다. 다음 예시는 이를 보여줍니다:
<error-page>
<location>/error</location>
</error-page>
위 예제에 따라, 예외가 전파되거나 응답에 오류 상태가 설정되면 Servlet 컨테이너는 구성된 URL(예: /error
)로 ERROR 디스패치를 수행합니다. 이후 DispatcherServlet
에 의해 처리되며, 해당 URL이 @Controller에 매핑될 수 있습니다. 이 컨트롤러는 오류 뷰 이름과 모델을 반환하거나 JSON 응답을 렌더링할 수 있습니다. 다음 예시는 이를 보여줍니다:
@RestController
public class ErrorController {
@RequestMapping(path = "/error")
public Map<String, Object> handle(HttpServletRequest request) {
Map<String, Object> map = new HashMap<>();
map.put("status", request.getAttribute("jakarta.servlet.error.status_code"));
map.put("reason", request.getAttribute("jakarta.servlet.error.message"));
return map;
}
}
팁
Servlet API에서는 Java에서 오류 페이지 매핑을 생성하는 방법을 제공하지 않습니다. 그러나 WebApplicationInitializer와 최소한의 web.xml을 함께 사용할 수 있습니다.
Spring MVC에서 예외가 발생했을 때, 다양한 \`HandlerExceptionResolver\`를 통해 예외를 처리하며, 이를 통해 사용자에게 적절한 오류 응답을 제공합니다. 또한, 컨테이너에서 기본 오류 페이지를 사용자 정의하여 발생한 오류에 대한 정보를 사용자에게 제공할 수 있습니다.
View Resolution
Spring MVC는 ViewResolver
및 View
인터페이스를 정의하여 특정 뷰 기술에 구애받지 않고 모델을 브라우저에 렌더링할 수 있도록 합니다. ViewResolver
는 뷰 이름과 실제 뷰 간의 매핑을 제공합니다. View
는 특정 뷰 기술에 전달하기 전에 데이터를 준비하는 역할을 합니다.
ViewResolver 구현 목록
다음 표는 ViewResolver의 구현 목록입니다:
ViewResolver | 설명 |
---|---|
AbstractCachingViewResolver | 이 클래스의 서브클래스는 해결한 뷰 인스턴스를 캐시하여 성능을 개선합니다. cache 속성을 false로 설정하여 캐시를 끌 수 있습니다. 특정 뷰를 런타임에 새로 고쳐야 할 경우, removeFromCache(String viewName, Locale loc) 메서드를 사용할 수 있습니다. |
UrlBasedViewResolver | ViewResolver 인터페이스의 간단한 구현으로, 논리 뷰 이름을 URL에 직접 매핑합니다. 명시적인 매핑 정의가 필요하지 않습니다. |
InternalResourceViewResolver | UrlBasedViewResolver의 편리한 서브클래스로, InternalResourceView(즉, Servlets 및 JSP)와 JstlView 등의 서브클래스를 지원합니다. 모든 뷰에 대해 뷰 클래스를 설정하려면 setViewClass(..) 를 사용할 수 있습니다. |
FreeMarkerViewResolver | FreeMarkerView 및 사용자 정의 서브클래스를 지원하는 UrlBasedViewResolver의 편리한 서브클래스입니다. |
ContentNegotiatingViewResolver | 요청 파일 이름 또는 Accept 헤더에 따라 뷰를 해결하는 ViewResolver 인터페이스의 구현입니다. Content Negotiation을 참조하십시오. |
BeanNameViewResolver | 현재 애플리케이션 컨텍스트에서 뷰 이름을 bean 이름으로 해석하는 ViewResolver 구현입니다. 매우 유연한 변형으로, 서로 다른 뷰 유형을 믹스앤매치할 수 있습니다. 각 뷰는 XML 또는 구성 클래스에서 bean으로 정의될 수 있습니다. |
뷰 해상도 처리 (Handling)
여러 개의 ViewResolver 빈을 선언하고 필요에 따라 순서 속성을 설정하여 뷰 해상도를 체인할 수 있습니다. 순서 속성이 높을수록 뷰 리졸버는 체인에서 나중에 위치하게 됩니다.
ViewResolver
계약에 따르면, 뷰를 찾을 수 없는 경우 null
을 반환할 수 있습니다. 그러나 JSP 및 InternalResourceViewResolver
의 경우, JSP가 존재하는지 확인하려면 RequestDispatcher를 통해 디스패치해야 합니다. 따라서 InternalResourceViewResolver
는 전체 뷰 리졸버 순서에서 마지막에 배치해야 합니다.
뷰 해상도를 구성하는 것은 Spring 구성에 ViewResolver 빈을 추가하는 것만큼 간단합니다. MVC Config는 View Resolvers에 대한 전용 구성 API를 제공하며, 컨트롤러 로직 없이 HTML 템플릿 렌더링에 유용한 로직 없는 View Controllers를 추가하는 데 도움이 됩니다.
리다이렉트 (Redirecting)
특별한 redirect:
접두사를 뷰 이름에 추가하면 리다이렉트를 수행할 수 있습니다. UrlBasedViewResolver
및 그 하위 클래스는 이를 리다이렉트가 필요하다는 지시로 인식합니다. 뷰 이름의 나머지는 리다이렉트 URL입니다.
이는 컨트롤러가 RedirectView
를 반환한 것과 동일한 효과를 내며, 이제 컨트롤러는 논리적 뷰 이름으로 작업할 수 있습니다. 예를 들어, redirect:/myapp/some/resource
는 현재 Servlet 컨텍스트에 대해 상대적으로 리다이렉트하고, redirect:https://myhost.com/some/arbitrary/path
는 절대 URL로 리다이렉트합니다.
포워딩 (Forwarding)
UrlBasedViewResolver
및 그 하위 클래스에서 궁극적으로 해결된 뷰 이름에 대해 특별한 forward:
접두사를 사용할 수도 있습니다. 이는 InternalResourceView
를 생성하며, RequestDispatcher.forward()
를 수행합니다. 따라서 이 접두사는 InternalResourceViewResolver
및 InternalResourceView
(JSP)에서는 유용하지 않지만, 다른 뷰 기술을 사용할 때도 Servlet/JSP 엔진에 의해 리소스를 강제로 포워드하도록 하는 데 유용합니다. 여러 개의 뷰 리졸버를 체인할 수도 있습니다.
콘텐츠 협상 (Content Negotiation)
ContentNegotiatingViewResolver
는 뷰를 직접 해결하지 않고, 다른 뷰 리졸버에 위임하여 클라이언트가 요청한 표현과 유사한 뷰를 선택합니다. 표현은 Accept 헤더 또는 쿼리 파라미(예: "/path?format=pdf")에서 결정할 수 있습니다.
ContentNegotiatingViewResolver
는 요청 미디어 유형을 각 뷰 리졸버와 관련된 뷰가 지원하는 미디어 유형(Content-Type)과 비교하여 적절한 뷰를 선택합니다. 호환 가능한 Content-Type을 가진 첫 번째 뷰가 클라이언트에게 표현을 반환합니다. 만약 뷰 리졸버 체인에서 호환 가능한 뷰를 제공할 수 없다면, DefaultViews 속성을 통해 지정된 뷰 목록을 참조합니다. 이 옵션은 논리적 뷰 이름에 관계없이 현재 리소스의 적절한 표현을 렌더링할 수 있는 싱글턴 뷰에 적합합니다. Accept 헤더는 와일드카드(예: text/*)를 포함할 수 있으며, 이 경우 Content-Type이 text/xml인 뷰가 호환 가능한 매치가 됩니다.
Spring MVC는 다양한 ViewResolver 구현을 통해 뷰 이름을 실제 뷰로 매핑할 수 있도록 하며, 이를 통해 사용자는 다양한 뷰 기술을 선택하여 모델을 브라우저에 렌더링할 수 있습니다. 리다이렉트 및 포워딩을 통해 요청의 흐름을 제어할 수 있으며, 콘텐츠 협상을 통해 클라이언트의 요청에 맞는 적절한 응답을 제공합니다.
Locale
Spring 프레임워크는 다국어를 지원하는 다양한 기능을 제공합니다. 특히, Spring Web MVC는 클라이언트의 로케일에 따라 메시지를 자동으로 해석할 수 있는 기능을 갖추고 있습니다. 이를 통해 DispatcherServlet
은 요청이 들어올 때 로케일 리졸버를 찾아 로케일을 설정합니다. 로케일은 언어와 지역 정보를 포함하여, 다국어 지원과 관련된 기능을 가능하게 합니다.
LocaleResolver
로케일 리졸버는 요청을 처리하는 데 사용되며, 다음과 같은 객체들이 포함되어 있습니다:
- Header Resolver: 클라이언트가 보낸 요청의
Accept-Language
헤더를 검사하여 로케일을 결정합니다. 이 헤더는 일반적으로 클라이언트 운영 체제의 로케일을 포함합니다. 하지만, 이 리졸버는 시간대 정보를 지원하지 않습니다. - Cookie Resolver: 클라이언트의 쿠키를 검사하여 Locale이나 TimeZone이 지정되어 있는지 확인합니다. 지정된 경우, 해당 정보를 사용하여 로케일을 설정합니다. 쿠키의 이름과 최대 유효 기간을 설정할 수 있습니다.
<bean id="localeResolver" class="org.springframework.web.servlet.i18n.CookieLocaleResolver"> <property name="cookieName" value="clientlanguage"/> <property name="cookieMaxAge" value="100000"/> </bean>
- Session Resolver: 세션에 저장된 Locale과 TimeZone을 사용할 수 있는
SessionLocaleResolver
를 제공합니다. 이 방법은 사용자의 요청에 대한 세션 내에서 로케일 설정을 저장합니다. 세션이 종료되면 이 설정은 사라집니다. - Locale Interceptor:
LocaleChangeInterceptor
를 추가하여 요청 파라미터에 따라 로케일을 변경할 수 있습니다. 예를 들어,siteLanguage
라는 파라미터가 요청에 포함되면, 이 값을 통해 로케일을 변경할 수 있습니다. <bean id="localeChangeInterceptor" class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor"> <property name="paramName" value="siteLanguage"/> </bean> <bean id="localeResolver" class="org.springframework.web.servlet.i18n.CookieLocaleResolver"/> <bean id="urlMapping" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping"> <property name="interceptors"> <list> <ref bean="localeChangeInterceptor"/> </list> </property> <property name="mappings"> <value>/**/*.view=someController</value> </property> </bean>
Time Zone (시간대)
클라이언트의 로케일 외에도 시간대를 아는 것이 유용할 수 있습니다. LocaleContextResolver
인터페이스는 시간대 정보를 포함할 수 있는 더 풍부한 LocaleContext
를 제공하는 로케일 리졸버를 제공합니다. 사용자의 시간대는 RequestContext.getTimeZone()
메서드를 사용하여 가져올 수 있습니다. 이 정보는 Spring의 ConversionService
에 등록된 모든 날짜/시간 변환기 및 형식 지정기에서 자동으로 사용됩니다.
정리
- locale resolver는 클라이언트의 요청에 따라 적절한 로케일을 설정하는 역할을 합니다.
- locale interceptor를 사용하면 요청 파라미터에 따라 로케일을 변경할 수 있습니다.
- tiem zone는 클라이언트의 로케일 외에 유용하게 사용되며, 날짜와 시간 형식 지정에 자동으로 적용됩니다.
이러한 기능들을 통해 Spring MVC는 다국어 웹 애플리케이션을 효과적으로 지원할 수 있습니다.
Themes
Spring Web MVC 프레임워크에서는 웹 애플리케이션의 전체적인 디자인과 느낌을 설정하기 위해 테마 기능을 제공합니다. 테마는 일반적으로 스타일 시트(CSS)와 이미지로 구성된 정적 자원의 집합으로, 애플리케이션의 시각적 스타일을 변경합니다. 하지만, 주의할 점은 Spring 6.0부터 테마 지원이 더 이상 권장되지 않으며, CSS 사용으로 대체되었습니다.
Multipart Resolver
MultipartResolver
는 Spring 프레임워크의 org.springframework.web.multipart
패키지에서 제공하는 인터페이스로, 파일 업로드를 포함한 멀티파트 요청을 처리하기 위한 전략입니다. Spring 6.0부터는 Apache Commons FileUpload에 기반한 구식 CommonsMultipartResolver
는 더 이상 사용되지 않으며, 대신 StandardServletMultipartResolver
가 제공됩니다.
멀티파트 처리 활성화
멀티파트 파일 업로드를 처리하기 위해서는 DispatcherServlet의 Spring 구성에 multipartResolver
라는 이름의 MultipartResolver
빈을 선언해야 합니다. DispatcherServlet은 이를 감지하여 들어오는 요청에 적용합니다.
- POST 요청 처리:
Content-Type
이multipart/form-data
인 POST 요청이 수신되면, 멀티파트 리졸버는 요청 내용을 분석하여HttpServletRequest
를MultipartHttpServletRequest
로 래핑합니다. 이를 통해 파일과 요청 파라미터에 접근할 수 있습니다.
Servlet 멀티파트 파싱
Servlet 멀티파트 파싱을 활성화하려면, Servlet 컨테이너의 설정이 필요합니다. 두 가지 방법으로 설정할 수 있습니다:
- Java 코드에서 설정:
MultipartConfigElement
를 Servlet 등록에 설정합니다.
public class AppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { // ... @Override protected void customizeRegistration(ServletRegistration.Dynamic registration) { // maxFileSize, maxRequestSize, fileSizeThreshold를 선택적으로 설정할 수 있습니다. registration.setMultipartConfig(new MultipartConfigElement("/tmp")); } }
- web.xml에서 설정:
- Servlet 선언에
<multipart-config>
섹션을 추가합니다.
<servlet> <servlet-name>yourServlet</servlet-name> <servlet-class>your.package.YourServlet</servlet-class> <multipart-config> <location>/tmp</location> <max-file-size>10485760</max-file-size> <!-- 10 MB --> <max-request-size>52428800</max-request-size> <!-- 50 MB --> <file-size-threshold>5242880</file-size-threshold> <!-- 5 MB --> </multipart-config> </servlet>
- Servlet 선언에
주의 사항
- 서블릿 컨테이너 구성:
StandardServletMultipartResolver
는 서블릿 컨테이너의 멀티파트 파서를 그대로 사용하므로, 컨테이너 구현 차이에 노출될 수 있습니다. 기본적으로 모든multipart/
콘텐츠 유형을 분석하려고 하지만, 이는 모든 서블릿 컨테이너에서 지원되지 않을 수 있습니다. - 설정 및 옵션: 이 리졸버는 다양한 구성 옵션을 제공하므로, 필요에 따라 설정할 수 있습니다. 자세한 내용은
StandardServletMultipartResolver
의 Javadoc에서 확인할 수 있습니다.
요약
MultipartResolver
는 파일 업로드를 처리하는 전략입니다.- 멀티파트 파일 업로드를 위해서는
multipartResolver
빈을 선언해야 하며, Servlet 설정이 필요합니다. - Java 코드나 web.xml 파일에서 멀티파트 파싱을 설정할 수 있습니다.
StandardServletMultipartResolver
는 서블릿 컨테이너의 멀티파트 파서를 사용하여 요청을 분석합니다.
이러한 설정을 통해 Spring MVC 애플리케이션에서 파일 업로드 기능을 손쉽게 구현할 수 있습니다.
Logging
Spring MVC에서의 로깅은 애플리케이션의 상태를 추적하고 디버깅하는 데 중요한 역할을 합니다. Spring MVC는 다양한 로깅 레벨을 제공하며, 그 중 DEBUG와 TRACE 레벨의 로깅에 대해 설명하겠습니다.
DEBUG-Level Logging
- 목적: DEBUG 레벨의 로깅은 간결하고 인간 친화적이며, 유용한 정보 조각들을 중심으로 구성됩니다. 이러한 정보는 문제를 해결하는 데 유용한 반복적으로 사용할 수 있는 정보를 제공합니다.
- 특징:
- 로그 메시지는 최소화되어 가독성이 좋습니다.
- 자주 발생하는 상황에 대한 정보를 강조합니다.
- 특정 문제를 디버깅할 때 도움이 되는 정보에 집중합니다.
TRACE-Level Logging
- 목적: TRACE 레벨의 로깅은 DEBUG와 유사한 원칙을 따르지만, 더욱 상세한 정보를 제공합니다. 이 레벨은 특정 문제를 해결하기 위한 디버깅에 유용하게 사용됩니다.
- 특징:
- TRACE 로그는 DEBUG 로그보다 더 많은 세부 정보를 포함할 수 있습니다.
- 디버깅이 필요한 모든 상황에서 사용될 수 있습니다.
좋은 로깅
좋은 로깅은 로그를 실제로 사용해 본 경험에서 비롯됩니다. 로그가 목표에 부합하지 않거나 문제가 발생한 경우, 사용자들은 이를 피드백으로 제공할 수 있습니다. 이는 시스템을 개선하는 데 도움이 됩니다.
Sensitive Data (민감한 데이터)
- DEBUG 및 TRACE 레벨의 로깅은 민감한 정보를 기록할 수 있습니다. 이로 인해 요청 파라미터 및 헤더는 기본적으로 마스킹됩니다.
- 만약 이러한 세부 정보를 전체적으로 기록하고자 한다면,
DispatcherServlet
의enableLoggingRequestDetails
속성을 사용하여 설정을 활성화해야 합니다.
예제: Java Configuration
아래는 Java 설정을 통해 요청 세부 정보를 기록하도록 설정하는 예제입니다:
public class MyInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return ... ; // Root config classes를 반환합니다.
}
@Override
protected Class<?>[] getServletConfigClasses() {
return ... ; // Servlet config classes를 반환합니다.
}
@Override
protected String[] getServletMappings() {
return ... ; // Servlet mappings를 반환합니다.
}
@Override
protected void customizeRegistration(ServletRegistration.Dynamic registration) {
registration.setInitParameter("enableLoggingRequestDetails", "true"); // 요청 세부 정보 로깅 활성화
}
}
- 설명:
MyInitializer
클래스는 Spring의AbstractAnnotationConfigDispatcherServletInitializer
를 확장하여 Servlet 초기화를 설정합니다.customizeRegistration
메서드에서enableLoggingRequestDetails
초기화 파라미를"true"
로 설정하여 요청 세부 정보의 로깅을 활성화합니다.
요약
- DEBUG와 TRACE 레벨의 로깅은 문제를 해결하고 애플리케이션 상태를 모니터링하는 데 유용한 도구입니다.
- 요청 파라미터 및 헤더는 기본적으로 마스킹되어 있지만, 이를 변경하여 전체 정보를 로깅할 수 있습니다.
- 이를 위해
DispatcherServlet
의enableLoggingRequestDetails
속성을 설정해야 합니다.
이러한 설정을 통해 Spring MVC 애플리케이션에서 효과적인 로깅 전략을 구현하고, 문제를 더 쉽게 추적하고 해결할 수 있습니다.
참고 : [https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-servlet.html]
'Spring Framework > Web on Servlet Stack' 카테고리의 다른 글
@RequestHeader (0) | 2024.10.09 |
---|---|
Annotated Controllers[1] (1) | 2024.10.06 |
Filter (0) | 2023.05.19 |
WebDataBinder (0) | 2023.05.01 |
Servlet (0) | 2023.04.17 |