[Spring] 서블릿 컨테이너와 스프링 컨테이너 개발하기 (with IntelliJ)
스프링 프레임워크의 작동 원리를 이해하기 위해 서블릿 컨테이너의 초기화 기능을 알아보고 이어서 이 초기화 기능을 활용해 스프링 컨테이너를 만들고 연결해보자.
개발 환경
- JAVA : 17
- IntelliJ : 2024.02
- Tomcat : 10.1.30
인텔리J 톰캣 설정
개발 단계에서는 war 파일을 만들고, 이것을 서버에 복사해서 배포하는 과정을 자동화해보자
smart tomcat 플러그인
설정 -> 플러그인에 들어가 smart tomcat
을 다운받아준다.
gradle 설정
dependencies {
//서블릿
implementation 'jakarta.servlet:jakarta.servlet-api:6.0.0'
//스프링 MVC 추가
implementation 'org.springframework:spring-webmvc:6.1.11'
}
//war 풀기, 인텔리J 무료버전 필요
task explodedWar(type: Copy) {
into "$buildDir/exploded"
with war
}
톰캣 설정
Run -> Edit Configuration 에 들어가 smart tomcat 설정을 해준다
- Tomcat server : 다운받은 톰캣이 있는 폴더
- Deployment direction : 나는 index.html 파일이 있는 webapp 폴더로 지정해줬다.
- Context path : /
서블릿 컨테이너 등록
서블릿을 등록하는 2가지 방법이 있다.
- @WebServlet 애노테이션
- 프로그래밍 방식
@WebServlet
@WebServlet
애너테이션을 이용해 클래스를 서블릿으로 선언하고 URL 패턴 /test
에 매핑한다.
/**
* https://localhost:8080/test
*/
@WebServlet(urlPatterns = "/test")
public class TestServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("TestServlet.service");
resp.getWriter().println("TestServlet.service");
}
}
https://localhost:8080/test 에 접근하면, 브라우저에 TestServlet.service 라는 메시지가 나타난다.
@WebServlet
애노테이션을 사용하면 서블릿을 매우 간편하게 등록할 수 있다는 장점이 있지만, 단점으로는 유연하게 변경하기가 어렵다는 문제가 있다. 애노테이션을 통해 경로를 설정하면 하드코딩된 것처럼 동작하기 때문에, 경로를 변경하려면 코드를 직접 수정해야 한다.
반면에, 서블릿을 XML 설정 파일(web.xml)을 통해 등록하는 프로그래밍 방식은 더 많은 코드를 작성해야 하지만, 다음과 같은 유연성을 제공한다.
프로그래밍 방식
WAS(Web Application Server)를 실행할 때, 초기화 작업은 매우 중요하다. 서비스가 시작되기 전에 필수적인 서블릿, 필터를 등록하고, Spring을 사용하는 경우에는 스프링 컨테이너와 디스패처 서블릿(DispatcherServlet)을 설정하여 WAS가 정상적으로 동작하도록 해야 한다.
과거에는 이러한 초기화 작업을 web.xml 파일을 사용하여 처리했지만, 최근 서블릿 스펙에서는 자바 코드를 사용하여 이러한 초기화를 직접 할 수 있는 방법도 제공하고 있다.
HelloServlet
TestServlet 과 마찬가지로 HelloServlet을 생선해준다. 단, @WebServlet 애너테이션을 이용하지 않고 프로그래밍 방식으로 작성할거다.
public class HelloServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("HelloServlet.service");
resp.getWriter().println("hello servlet!");
super.service(req, resp);
}
}
AppInit
애플리케이션 초기화를 진행하려면 먼저 인터페이스를 만들어야 한다. 내용과 형식은 상관없다.
public interface AppInit {
void onStartup(ServletContext context);
}
AppInitV1Servlet
AppInit 인터페이스를 구현하여 서블릿을 등록해주자.
public class AppInitV1Servlet implements AppInit{
@Override
public void onStartup(ServletContext context) {
System.out.println("AppInitV1Servlet.onStartUp");
// 순수 서블릿 등록
ServletRegistration.Dynamic helloServlet = context.addServlet("helloServlet", new HelloServlet());
helloServlet.addMapping("/hello-servlet");
}
}
MyContainerInitV2
애플리케이션 초기화 과정을 진행하는 코드이다.
@HandlesTypes(AppInit.class)
public class MyContainerInitV2 implements ServletContainerInitializer {
@Override
public void onStartup(Set<Class<?>> set, ServletContext servletContext) throws ServletException {
System.out.println("MyContainerInitV2.onStartup");
System.out.println("MyContainerInitV2 set = "+set); // 유연한 초기화 기능 제공
System.out.println("MyContainerInitV2 servletContext = "+servletContext);
for (Class<?> appInitClass : set) {
try{
//new AppInitV1Servlet()과 같은 코드
AppInit appInit = (AppInit) appInitClass.getDeclaredConstructor().newInstance();
appInit.onStartup(servletContext);
}catch (Exception e){
throw new RuntimeException(e);
}
}
}
}
애플리케이션 초기화 과정은 다음과 같다.
-
@HandlesTypes: 컨테이너가 애플리케이션을 시작할 때 특정 타입(여기서는 AppInit 인터페이스)을 구현한 모든 클래스를 찾아 처리하도록 지시한다.
- 서블릿 컨테이너 초기화( ServletContainerInitializer )는 파라미터로 넘어오는 Set<Class<?» c 에 애플리케이션 초기화 인터페이스의 구현체들을 모두 찾아서 클래스 정보로 전달 한다.
- 여기서는 AppInit.class 의 구현체인 AppInitV1Servlet.class 정보가 전달된다.
- appInitClass.getDeclaredConstructor().newInstance() : 리플렉션을 사용해서 객체를 생성한다. = new AppInitV1Servlet()
- appInit.onStartup(servletContext) : 애플리케이션 초기화 코드를 직접 실행하면서 서블릿 컨테이너 정보가 담긴 servletContext 도 함께 전달한다.
MyContainerInitV2 등록
MyContainerInitV2 를 실행하려면 서블릿 컨테이너에게 알려주어야 한다.
resources/META-INF/services/jakarta.servlet.ServletContainerInitializer 파일을 생성해주자.
jakarta.servlet.ServletContainerInitializer 안에는 MyContainerInitV2을 추가한다.
https://localhost:8080/hello-servlet 에 접근하면, “HelloServlet.service” 을 확인할 수 있다!
WAS 실행과정 정리
초기화는 다음 순서로 진행된다.
- 서블릿 컨테이너 초기화 실행
- resources/META-INF/services/jakarta.servlet.ServletContainerInitializer
- 애플리케이션 초기화 실행
- @HandlesTypes(AppInit.class)
스프링 컨테이너 등록
WAS와 스프링을 통합해보자. 서블릿 컨테이너 초기화와 애플리케이션 초기화를 활용하면 된다.
- 스프링 컨테이너 만들기
- 스프링MVC 컨트롤러를 스프링 컨테이너에 빈으로 등록하기
- 스프링MVC를 사용하는데 필요한 디스패처 서블릿을 서블릿 컨테이너 등록하기
HelloController
“Hello Spring” 을 반환하는 간단한 스프링 컨트롤러를 생성해준다.
@RestController
public class HelloController {
@GetMapping("/hello-spring")
public String hello() {
System.out.println("HelloController.hello");
return "Hello Spring";
}
}
HelloConfig
helloController 를 스프링 빈으로 직접 등록해준다.
@Configuration
public class HelloConfig {
@Bean
public HelloController helloController(){
return new HelloController();
}
}
스프링 컨테이너 생성
애플리케이션 초기화를 사용해서 서블릿 컨테이너에 스프링 컨테이너를 생성하고 등록하자.
public class AppInitV2Spring implements AppInit{
@Override
public void onStartup(ServletContext context) {
System.out.println("AppInitV2Spring onStartup");
// 스프링 컨테이너 생성
AnnotationConfigWebApplicationContext appContext = new AnnotationConfigWebApplicationContext();
appContext.register(HelloConfig.class);
// 스프링 MVC 디스패쳐 서블릿 생성, 스프링 컨테이너 연결
// 디스패처 서블릿에 HTTP 요청이 오면 디스패처 서블릿은 해당 스프링 컨테이너에 들어있는 컨트롤러 빈들을 호출한다.
DispatcherServlet dispatcher = new DispatcherServlet(appContext);
// 디스패처 서블릿을 서블릿 컨테이너에 등록
// 디스패처 이름은 중복 안됨 (dispatcherV2)
ServletRegistration.Dynamic servlet = context.addServlet("dispatcherV2", dispatcher);
// spring/* 요청이 디스패처 서블릿을 통하도록 설정
servlet.addMapping("/spring/*");
}
}
AppInitV2Spring 는 AppInit 을 구현했으므로 MyContainerInitV2에서 애플리케이션 초기화 코드가 자동으로 실행된다.
- 스프링 컨테이너 생성
- AnnotationConfigWebApplicationContext 가 바로 스프링 컨테이너이다.
- appContext.register(HelloConfig.class) : 컨테이너에 스프링 설정을 추가한다.
- 스프링 MVC 디스패처 서블릿 생성, 스프링 컨테이너 연결
- new DispatcherServlet(appContext) : 스프링 MVC가 제공하는 디스패처 서블릿을 생성하고, 스프링 컨테이너를 전달해 디스패처 서블릿에 스프링 컨테이너를 연결한다.
- 이 디스패처 서블릿에 HTTP 요청이 오면 디스패처 서블릿은 해당 스프링 컨테이너에 들어있는 컨트롤러 빈들을 호출한다.
- 디스패처 서블릿을 서블릿 컨테이너에 등록
- servletContext.addServlet(“dispatcherV2”, dispatcher) : 디스패처 서블릿을 서블릿 컨테이너에 등록한다.
- /spring/* 이렇게 경로를 지정하면 /spring 과 그 하위 요청은 모두 해당 서블릿을 통하게 된다.
주의! 서블릿을 등록할 때 이름은 원하는 이름을 등록하면 되지만 같은 이름으로 중복 등록하면 오류가 발생한다. (dispatcherV2)
HelloController 실행
http://localhost:8080/spring/hello-spring 접근시 Hello Spring
이 실행된다.
- dispatcherV2 디스패처 서블릿은 스프링 컨트롤러를 찾아서 실행한다. ( /hello-spring )
- 이때 서블릿을 찾아서 호출하는데 사용된 /spring 을 제외한 /hello-spring 가 매핑된 컨트롤러 HelloController의 메서드를 찾아서 실행한다.
스프링 MVC를 이용한 서블릿 컨테이너 초기화
지금까지 서블릿 컨테이너를 초기화 하기 위해 ServletContainerInitializer 인터페이스를 구현해서 서블릿 컨테이너 초기화 코드를 만드는 복잡한 과정을 진행했다.
여기에 애플리케이션 초기화를 만들기 위해 @HandlesTypes 애노테이션을 적용하고 ServletContainerInitializer 파일에 서블릿 컨테이너 초기화 클래스 경로를 등록했다.
스프링 MVC는 이러한 서블릿 컨테이너 초기화 작업을 이미 만들어두었다. 개발자는 서블릿 컨테이너 초기화 과정은 생략하고, 애플리케이션 초기화 코드만 작성하면 된다.
WebApplicationInitializer
스프링은 애플리케이션 초기화 인터페이스를 지원한다.
public interface WebApplicationInitializer {
void onStartup(ServletContext servletContext) throws ServletException;
}
WebApplicationInitializer 을 이용해서 애플리케이션 초기화를 진행해보자.
/**
* http://localhost:8080/hello-spring
*
* 스프링 MVC 제공 WebApplicationInitializer 활용
* spring-web
* META-INF/services/jakarta.servlet.ServletContainerInitializer
* org.springframework.web.SpringServletContainerInitializer
*/
public class AppInitV2SpringMVC implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext context) {
System.out.println("AppInitV2SpringMVC onStartup");
// 스프링 컨테이너 생성
AnnotationConfigWebApplicationContext appContext = new AnnotationConfigWebApplicationContext();
appContext.register(HelloConfig.class);
// 스프링 MVC 디스패쳐 서블릿 생성, 스프링 컨테이너 연결
// 디스패처 서블릿에 HTTP 요청이 오면 디스패처 서블릿은 해당 스프링 컨테이너에 들어있는 컨트롤러 빈들을 호출한다.
DispatcherServlet dispatcher = new DispatcherServlet(appContext);
// 디스패처 서블릿을 서블릿 컨테이너에 등록
// 디스패처 이름은 중복 안됨 (dispatcherV3)
ServletRegistration.Dynamic servlet = context.addServlet("dispatcherV3", dispatcher);
// 모든 요청이 디스패처 서블릿을 통하도록 설정
servlet.addMapping("/");
}
}
WebApplicationInitializer 인터페이스를 구현한 부분을 제외하고는 이전의 AppInitV2Spring 과 거의 같은 코드이다.
servlet.addMapping(“/”) 코드를 통해 모든 요청이 해당 서블릿을 타도록 했다. http://localhost:8080/hello-spring 호출하면 / hello-spring 이 매핑된 컨트롤러 메서드가 호출된다.
이해를 돕기 위해 디스패처 서블릿도 2개 만들고, 스프링 컨테이너도 2개 만들었지만 일반적으로는 스프링 컨테이너를 하나 만들고, 디스패처 서블릿도 하나만 만든다.
그리고 디스패처 서블릿의 경로 매핑도 / 로 해서 하나의 디스패처 서블릿을 통해서 모든 것을 처리하도록 한다.
스프링 MVC가 제공하는 서블릿 컨테이너 초기화 원리
WebApplicationInitializer 인터페이스 하나로 애플리케이션 초기화가 가능한 이유가 뭘까?
@HandlesTypes 애노테이션을 적용하고 ServletContainerInitializer 파일에 서블릿 컨테이너 초기화 클래스 경로를 등록하는 과정이 생략되어 보이지만 스프링도 모두 구현해놨다.
spring-web 라이브러리를 열어보면 서블릿 컨테이너 초기화를 위한 등록 파일을 확인할 수 있다.
ServletContainerInitializer 파일에 org.springframework.web.SpringServletContainerInitializer
코드가 등록되어있다 코드를 확인해보면 다음과 같다 .
@HandlesTypes({WebApplicationInitializer.class})
public class SpringServletContainerInitializer implements ServletContainerInitializer {
public SpringServletContainerInitializer() {
}
public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext) throws ServletException {
List<WebApplicationInitializer> initializers = Collections.emptyList();
Iterator var4;
if (webAppInitializerClasses != null) {
initializers = new ArrayList(webAppInitializerClasses.size());
var4 = webAppInitializerClasses.iterator();
while(var4.hasNext()) {
Class<?> waiClass = (Class)var4.next();
if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) && WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
try {
((List)initializers).add((WebApplicationInitializer)ReflectionUtils.accessibleConstructor(waiClass, new Class[0]).newInstance());
} catch (Throwable var7) {
Throwable ex = var7;
throw new ServletException("Failed to instantiate WebApplicationInitializer class", ex);
}
}
}
}
...
코드를 보면 우리가 앞서 만든 서블릿 컨테이너 초기화 코드와 비슷한 것을 확인할 수 있다. @HandlesTypes 의 대상이 WebApplicationInitializer
이다. 그리고 이 인터페이스의 구현체를 생성하고 실행하는 것을 확인할 수 있다.
정리
스프링MVC도 우리가 지금까지 한 것 처럼 서블릿 컨테이너 초기화 파일에 초기화 클래스를 등록해두었다. 그리고 WebApplicationInitializer 인터페이스를 애플리케이션 초기화 인터페이스로 지정해두고, 이것을 생성해서 실행한다.
따라서 스프링 MVC가 제공하는 WebApplicationInitializer 인터페이스만 구현하면 AppInitV3SpringMVC를 에서 본 것 처럼 편리하게 애플리케이션 초기화를 사용할 수 있다.
Reference
댓글남기기