- MVC, DB 접근 기술 등 많은 기능을 제공하고 다양한 문제를 해결할 수 있음
- 다양한 라이브러리들을 편리하게 사용할 수 있도록 통합함
- 개발자의 생산성이 높아짐
스프링 프레임워크를 사용하면, 스프링 부트가 제공하는 편의 기능이 너무 좋기 때문에 이제는 무조건 스프링 부트를 필수적으로 사용해야 한다. 하지만 스프링 부트는 스프링 프레임워크를 쉽게 사용할 수 있게 도와주는 도구일 뿐이고, 본질은 스프링 프레임워크다. 스프링 데이터, 세션, 시큐리티, 배치, 클라우드 등의 기능은 선택사항이다.
전통적인 방식
자바로 웹 애플리케이션 개발 시에는 먼저 서버에 톰캣 같은 웹 애플리케이션 서버를 설치 (WAS)
-> 그 안에서 동작하도록 서블릿 스텍에 맞춰서 코드를 작성하고 WAR라는 형식으로 빌드해서, war 파일을 생성한다.
-> 만들어진 war 파일을 WAS에 전달해서 배포하는 방식
이러한 방식은 인텔리제이, 이클립스 같은 개발환경에서 WAS와 연동하는 추가 설정이 들어가야 한다.
최근 방식
스프링 부트가 내장 톰캣을 포함하고 있다. 즉 애플리케이션 코드 안에 WAS가 내장되어 있고, 코드 작성 시 Jar로 빌드하고 원하는 위치에서 실행하기만 하면 별도 설정 없이 WAS가 실행된다. 메인 메서드만 실행하면 웹 애플리케이션을 실행할 수 있게 된다.
JAR
자바는 여러 클래스,리소스를 묶어서 JAR라는 압축 파일을 만들 수 있다. JVM 위에서 직접 실행되거나 다른 곳에서 사용하는 라이브러리고 제공된다. 직접 실행하는 경우 메인 메서드가 필요하고, MANIFEST.MF 파일에 실행할 메인 메서드가 있는 클래스를 지정해두어야 한다.
WAR
WAR 파일은 웹 애플리케이션에 배포할 때 사용하는 파일이다. 스프링에서 파일을 빌드하고 생성된 WAR 파일의 압축을 풀어보면 구조가 다음과 같은 것을 알 수 있다.
서블릿은 ServletContainerIntializer라는 초기화 인터페이스를 통해 서블릿 컨테이너를 초기화하는 기능을 제공한다.
public interface ServletContainerIntializer {
public void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException;
}
ServletContext ctx: 서블릿 컨테이너로, 필터나 서블릿을 등록할 수 있다.
웹 애플리케이션을 실행하게되면, 서블릿 컨테이너 실행 시점에 MyContainerInitV1 메서드를 실행하게 된다.
먼저 HelloServlet이라는 서블릿을 등록해준다.
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!");
}
}
서블릿 컨테이너는 애플리케이션 초기화라는 기능을 지원하는데, 먼저 AppInit이라는 인터페이스를 만들어준다.
public interface Applnit {
void onStartUp(ServletContext servletContext);
}
public class AppInitServlet implements AppInit {
@Override
public void onStartUp(ServletContext servletContext) {
System.out.println("AppInitServlet.onStartUp");
// 순수 서블릿 코드 등록
ServletRegistration.Dynamic helloServlet =
servletContext.addServlet("HelloServlet", new HelloServlet());
helloServlet.addMapping("/hello-servlet");
}
}
HelloServlet이라는 서블릿을 서블릿 컨테이너에 직접 등록하게 된다. /hello-servlet 요청을 보내면 서블릿이 실행된다. @WebServlet 애노테이션을 통해 서블릿을 등록할 수도 있지만, 프로그래밍 방식을 사용해서 등록할 수도 있다.
애노테이션 하나로 서블릿을 등록할 수 있다는 장점이 있지만, 하드코딩된 것처럼 동작하기 때문에 유연하게 변경하는 것이 어렵다는 단점이 있다. 프로그래밍 방식은 코딩을 더 많이 해야하지만 무한한 유연성을 제공한다. HTTP 요청 경로를 상황에 따라 위처럼 변경할 수 있고, 서블릿 자체도 if문으로 분기해서 등록하거나 빼는 것이 가능하다. 서블릿을 직접 생성하기 때문에 생성자에 필요한 정보를 넘길 수도 있다.
@HandlesTypes(AppInit.class) // 애플리케이션 초기화 인터페이스 지정
public class MyContainerInitv2 implements ServletContainerInitializer {
@Override
public void onStartUp(Set<Class<?>> c, ServletContext ctx) throws ServletException {
System.out.println("MyContainerInitv2.onStartUp");
System.out.println("MyContainerInitv2 c " + c);
System.out.println("MyContainerInitv2 ctx = " + ctx);
for (Class<?> appInitClass : c) {
try{
AppInit appInit = (AppInit) appInitClass.getDeclaredConstructor().newInstance();
appInit.onStartUp(ctx);
}catch (Exception e) {
throw new RuntimeException(e);
}
}
}
}
@HandlesTypes
: 애플리케이션을 초기화하는 인터페이스를 지정한다. Set<Class<?>> c
에 AppInit의 구현체들이 모두 넘어오게 되는데, 이 코드에서는 AppInitServlet의 정보가 넘어오게 된다.
객체 인스턴스가 아닌 클래스가 넘어오기 때문에 객체를 생성해서 사용해야 한다.
appInit.onStartUp(ctx)
: 애플리케이션 초기화 코드를 직접 실행하는 부분이고, 서블릿 컨테이너 정보가 담긴 ctx도 함께 전달한다.
애플리케이션 초기화까지 별도로 하게 되면 다음과 같은 장점이 있다.
편리성: 서블릿 컨테이너 초기화 시 ServletContainerInitializer 인터페이스 구현한 코드를 만들고 추가로
META-INF/services/jarkarta.servlet.ServletContainerInitializer 파일에 코드를 직접 지정해줘야 한다. 애플리케이션 초기화의 경우 AppInit 인터페이스를 만들어두면 필요할 때마다 구현체를 만들어서 사용하면 되기 때문에 편리하다.
의존성: 애플리케이션 초기화에서, 서블릿 컨테이너에 상관없이 원하는 모양으로 인터페이스를 만들 수 있기 때문에 서블릿 컨테이너에 대한 의존을 줄일 수 있다는 장점이 있다.
앞의 서블릿 컨테이너 초기화와 애플리케이션 초기화를 활용해서 스프링 컨테이너를 등록할 수 있다. 과정은 다음와 같다.
먼저 컨트롤러를 생성해준다.
@RestController
public class HelloController {
@GetMapping("/hello-spring")
public String hello() {
System.out.println("Hellocontroller.hello");
return "hello spring";
}
}
config 파일을 하나 생성해서 빈으로 등록해준다.
@Configuration
public class HelloConfig {
@Bean
public HelloController helloController() {
return new HelloController();
}
}
public class AppInitSpring implements AppInit{
@Override
public void onStartUp(ServletContext servletContext) {
System.out.println("AppInitSpring.onStartUp");
// 스프링 컨테이너 생성
AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext();
applicationContext.register(HelloConfig.class);
// MVC 디스패처 서블릿 생성, 스프링 컨테이너 연결
DispatcherServlet dispatcher = new DispatcherServlet(applicationContext);
// 디스패처 서블릿을 서블릿 컨테이너에 등록
ServletRegistration.Dynamic servlet = servletContext.addServlet("dispatcher", dispatcher);
// /spring/* 요청이 디스패처 서블릿을 통하도록 설정
servlet.addMapping("/spring/*");
}
}
초기화 인터페이스인 AppInit 구현체를 하나 만들어주면 실행 시에 구현체도 실행되게 된다.
스프링 컨테이너 생성 : AnnotationConfigWebApplicationContext가 스프링 컨테이너이고, appContext.register(HelloConfig.class);
에서 컨테이너에 스프링 설정을 추가한다.
스프링 MVC 디스패처 서블릿 생성, 스프링 컨테이너 연결: dispatcher라는 디스패처 서블릿을 생성하고, 앞서 생성한 applicationContext라는 스프링 컨테이너를 전달하면 스프링 컨테이너가 디스패처 서블릿에 연결된다.
디스패처 서블릿을 서블릿 컨테이너에 등록: addServlet을 통해 디스패처 서블릿을 서블릿 컨테이너에 등록하게 된다. 그러면 /spring/* 및 그 하위 요청들이 해당 서블릿을 통하게 된다.
스프링 MVC을 사용하면 서블릿 컨테이너 초기화 과정을 생략하고 애플리케이션 초기화 코드만 작성하면 된다.
스프링 MVC에서 지원하는 초기화 작업을 사용하려면 WebApplicationInitializer 인터페이스만 구현하면 된다.
spring-web 라이브러리에서 해당 작업을 구현하고 있는 것이다.
일반적으로는 스프링 컨테이너를 하나 만들고, 디스패처 서블릿도 하나만 생성하고 매핑 경로도 /
로 설정해서 하나의 디스패처 서블릿을 통해 모든 것을 처리하도록 한다.