자바 기반 환경 설정, 혹은 xml 기반 환경 설정으로 Spring MVC project를 구성하는 법에 대해 배워 볼 것이다.
그런데 일단, Spring MVC가 뭔지가 궁금할 것이다.
Model-View-Controller, 혹은 MVC pattern 기반의 애플리케이션을 위한 Spring framework 내의 모듈이라고 생각하면 된다.
MVC pattern이 그럼 뭐냐고 할 수 있는데 이를 간략하게 잘 설명한 글이 있으니 참고. 앞의 글에서 저 pattern이 가지는 의의와 웹 애플리케이션의 사용도 잘 설명하고 있지만, 좀더 규격화된 내용이 궁금하다면 위키백과를 참고해도 좋다.
어쨌든 저건 프로그래밍 방식, 즉 패턴을 제안한거고 그걸 실제로 적용하는 것은 프로젝트 자체에서 직접 구현해야 한다. 즉 Spring MVC 프레임워크가 MVC pattern 기반의 애플리케이션을 위한 모듈이라는 것은, MVC pattern 기반의 프로그래밍이 가능하도록 셋업을 이미 했다는 것이다.
그러면 그 셋업은 어떻게 했는지 궁금할 수 있다. 결론만 말하자면 DispatcherServlet이라는 고유 class를 활용해 front controller pattern을 통해 Spring MVC를 구현했다. 이에 관한 Baeldung 글은 다음에 자세히 다뤄보도록 하겠다.
이 글에서는 이정도만 알면 된다.
DispatcherServlet의 instance가 담당한다.이 글은 template engine으로 JSP를 사용한다.
@EnableWebMvc라는 annotation을 달면 된다.@EnableWebMvc
@Configuration
public class WebConfig {
/// ...
}
@EnableWebMvc이 annotation을 달면 WebMvcConfigurationSupport라는 곳에서의 기본 Spring MVC configuration을 받게 된다. WebMvcConfigurationSupport javadoc
즉 Web application 관련 MVC pattern 구현과 관련된 configuration 정보를 전달하는건 알겠는데 정확히 무슨 정보를 전달하는냐... 다음과 같다.
@RequestMapping, @GetMapping 등)@NotNull등이 대표적@ExceptionHandler을 활용해 본인 예외 처리가 가능하고, @ControllerAdvice를 활용해 모든 controller에서 처리 되지 않는 예외를 처리하는 방식도 지정하는 것이 가능하다. 이에 대한 정보도 들어있다.위의 configuration에 몇가지를 더 추가하고 싶은 경우 annotated된 class에서 WebMvcConfigurer을 implement 한다음에 몇개를 override하거나 @Bean method를 더 만들면 된다..
그리고 온전히 configuration을 다 직접 정의하고 싶은 경우 이 annotation을 사용하지 말고 WebMvcConfigurationSupport라는 class를 extend해가지고 직접 다 구현을 하면 된다. Baeldung에서는 전자의 방식으로 configuration을 조절했는데 이에 대해 더 알아보겠다.
WebMvcConfigurer앞에 말했지만 이 인터페이스는 MVC 관련 configuration에 몇가지 요소를 더 추가하고 싶을 때 쓰이는 인터페이스다.
저 인터페이스의 method를 구현하면 관련해가지고 몇가지 작업을 추가로 하게되는데 Baeldung의 경우 addViewControllers를 override하고 viewResolver이라는 bean 생성 메서드를 만들었다. 후자 메서드는 인터페이스에 있는게 아니며 @Configuration을 활용해서 context에 추가하는 것이라고 생각하면 된다.
@EnableWebMvc
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("index");
}
@Bean
public ViewResolver viewResolver() {
InternalResourceViewResolver bean = new InternalResourceViewResolver();
bean.setViewClass(JstlView.class);
bean.setPrefix("/WEB-INF/view/");
bean.setSuffix(".jsp");
return bean;
}
}
addViewControllers : controller을 넣는건데, 용도가 뭐냐면 간단하고 자동으로 돌아가며 미리 어느정도 환경 설정이 된 controller을 추가하는 것이다...라고 말하면 뭔 소린가 싶을거다. 그래서 이 녀석이 주로 사용되는 용도를 말하자면 request를 controller이 받았는데 딱히 프로그래밍적 조작 없이 바로 view를 전달만 하면 될 때, 그 용도의 controller을 만들 때 사용된다. 반환하는 status code까지 자동으로 구성이 된다. 보다시피 담당할 request랑 그 때 사용할 view를 설정하는게 전부라 구성하기 엄청 쉬워서 리다이렉션이나 정적 페이지 등에 자주 사용된다. 이를 Baeldung에서는 View랑 URL 이름 사이에 controller이 관여하지 않는다고 표현했다는 점 유의.
저런 controller들을 추가하는 방법은 그냥 registry.addViewController(...).setViewName의 형식이 일반적이다. registry가 이제 View-Controller 관계를 저장하는 곳이고, addViewController은 담당하는 request, setViewName은 전달할 view다. redirection의 경우 forward:같은 것을 사용한다는 점도 유의.
viewResolver : 앞에서 말했지만 우리가 직접 만든 bean 생성 메서드로 InternalResourceViewResolver에 해당하는 bean을 만든다. InternalResourceviewResolver은 view name symbol을 URL, 그니까 파일 위치로 바로 해석해주는 UrlBasedViewResolver이라는 인터페이스를 implement했으며 JSP wrapper로 쓰이는 InternalResourceView라는 형태의 view를 지원하는 resolver이다. UrlBasedViewResolver은 view 이름에서 url을 바로 형성할 수 있을 때 많이 유용하기에 보통 view 이름과 파일 이름이 직접적으로 연관이 될 때 자주 쓰이는 ViewResolver이다.
setViewClass는 여기서 생성하는 view들의 class가 뭔지를 지정한다. JstlView는 InternalResourceView의 subclass인데 JSTL page를 활용하는 view다. 참고로 JSTL page는 JSP standard tag library를 활용하는 JSP다.setPrefix랑 setSuffix는 URL 형성 때 view name들 앞/뒤에 붙는 접두사/접미사를 지정하는 메서드다.이전의 @ComponentScan에 대해서 기억하는가? 이걸로 직접 정의한 controller class들을 탐색하는 것도 가능하다. @ComponentScan이 @Component를 탐지한다고 했는데 @Controller이 @Component를 포함하고 있기 때문이다. 여기에 탐색할 패키지를 지정할 수 있는 기능을 통해 controller이 정의되어 있는 패키지를 지정해서 controller class들만 탐지하는 것도 가능하다. 그게 밑의 코드.
@EnableWebMvc
@Configuration
@ComponentScan(basePackages = { "com.baeldung.web.controller" })
public class WebConfig implements WebMvcConfigurer {
// ...
}
WebApplicationInitializer저 root web application context는 모든 servlet이 공유하는 context고, servlet별로 또 고유로 가질 configuration을 설정하려면 web application context를 별도로 생성해야 한다.
이 때 보통 web.xml을 가지고 root web application context를 구성하지만, 프로그래밍을 통해 구성하는 방법이 있는데 WebApplicationInitializer의 구현체를 활용하는 것이다.
public class MainWebAppInitializer implements WebApplicationInitializer {
@Override
public void onStartup(final ServletContext sc) throws ServletException {
AnnotationConfigWebApplicationContext root =
new AnnotationConfigWebApplicationContext();
root.scan("com.baeldung");
sc.addListener(new ContextLoaderListener(root));
ServletRegistration.Dynamic appServlet =
sc.addServlet("mvc", new DispatcherServlet(new GenericWebApplicationContext()));
appServlet.setLoadOnStartup(1);
appServlet.addMapping("/");
}
}
참고로 이 인터페이스를 구현한 class는 SpringServletContainerInitializer이라는 녀석이 자동으로 탐지한다. 저 class는 또 servlet을 관리하는 servlet container에서 자동으로 탐지해서 수행. 즉 다음과 같은 단계를 거친다.
ServletContainerInitializer의 implementation인 SpringServletContainerInitializer을 자동으로 탐지해서 실행WebApplicationInitializer implementation을 자동으로 탐지해서 위의 onStartup을 실행.방금 단계 설명에서 나왔듯이 이 인터페이스는 onStartup이라는 메서드를 가지고 있다. 여기서 root web application context랑 servlet 별 web application context를 구성하면 된다. 먼저 전자를 해볼건데, AnnotationConfigWebApplicationContext를 활용하는 것을 볼 수 있다.
AnnotationConfigWebApplicationContext는 방금 우리가 설정한 configuration 관련 요소들을 스캔해가지고 Spring IoC를 활용해 관련 bean들을 전부 생성하는데 사용된다. 이 bean 생성 스캐닝을 위해 scan 메서드를 사용한다. 그러면 해당 패키지에 있는 @Component 계열과 @Configuration을 전부 스캔하고 생성한다. (@Service 등)
예전에
WebApplicationContext라는 class에 대해 잠깐 설명한적이 있는데AnnotationConfigWebApplicationContext는 그것의 파생 class다. 이번에 소개한 녀석과AnnotationConfigApplicationContext와 같이 annotation 기반의 context들은scan메서드를 활용해 Spring IoC container에서 등록할 bean들을 추적하는 것이 가능하다는 점 유의. xml 기반은 이 기능이 없다.
참고로 위와 같은 scanning 방식은 일반적인 application에서의
ApplicationContext의 loading과 차이가 좀 있다. 일반적으로 우리가ApplicationContext를 load할때는 main method에다가AnnotationConfigApplicationContext의 instance를 생성, 생성자에 classpath같은것을 넣어서 scan을 하게 하거나scanmethod를 써가지고 bean들을 생성을 한다. 그런데 web application의 경우 위와 같이 복잡한 과정을 거치는 이유가 servlet container의 동작방식이랑 호환되도록 하기 위해서 그렇다.애플리케이션 시작과 관련된
mainmethod가 여기서 등장하지 않는 것도 이 때문이다. servlet container에서 프로그램 전체 동작을 관리하기 때문이다. servlet 관리, servlet container 관리 (는 사실 본인 관리), 그리고 그 외의 요소들 관리를 다 거기서 책임지기 때문. 만일mainmethod를 여기서 우리가 추가로 만들면 위의 initializer과 같이 동작하면서 bean 관리 방식 전체가 꼬이게 될 수 있다.
boot의 경우 위와 같은 context 셋업따위 집어치우고 그냥 자동을 configure하는 것도 가능하지만 이는 나중 이야기.
ContextLoadListenerServletContext측에 위 AnnotationConfigWebApplicationContext에 대한 listener을 추가한다. ContextLoaderListener이라고 되어 있는데, javadoc을 보면 저 class의 생성자의 용도가 WebApplicationContext의 lifecycle을 파악하는 것이다. 즉 servlet context는 root application context의 life cycle이 어떻게 변하는지를 파악하는 것이다. 본인이 모든 bean의 관리를 담당하기 때문에 그렇다. 그래서 web application 시작시에 얘가 생성을 해주고, 반대로 종료 시에는 얘가 관련 context를 다 미할당해버린다.ServletContext를 가진 servlet container의 관리 하에 작업을 할 서블릿을 추가하고 있다. 이를 위해선 ServletContext의 addServlet이라는 method를 활용해야 한다. 이름은 mvc, 타입은 Spring에서 쓰는 DispatcherServlet인 servlet을 추가하고 있다. 이 때 반환하는 타입인 ServletRegistration.Dynamic은 추후 해당 Servlet과 관련 설정을 할 때 사용할 수 있는 instance라고 documentation에 나와 있다. 실제로 밑의 코드에서 몇가지 method를 통해 추가 설정을 하고 있음을 볼 수 있다.GenericWebApplicationContext이때 해당 servlet에서 독자적으로 사용할 web application context, 정확히는 WebApplicationContext를 만들어야 하는데 직접 xml로 정의한 configuration을 활용하지 않고 GenericWebApplicationContext를 사용하는 것을 볼 수 있다.
그래 뭐 그건 좋은데... 뭐 아무 parameter도 없고 그냥 마법같이 WebApplicationContext를 만드는 것인가? 뭐 사실 안된다. 마법따위 존재 안한다.
사실 저 GenericWebApplicationContext는 프로그래밍을 통해 WebApplicationContext를 만들 때 쓰인다. 실제로 저거보다 더 general한 녀석으로 ApplicationContext를 이런 식으로 만들 때 쓰이는 GenericApplicationContext라는 것도 존재한다. 여기서 프로그래밍을 통해 context를 구성한다는 것은 bean을 직접 등록한다든가 (registerBean), parent context를 누구로 한다든가 (setParent), 또 GenericWebApplicationContext 한정으로 ServletContext를 등록해가지고 servlet 환경 차원에서 정의된 환경 변수들에 접근을 한다 등이 있다.
즉 사실 위의 경우 그런 추가 환경 설정들이 없고, 또 무엇보다 ServletContext에다가 (아까 root web application context처럼) listener을 등록한것도 아니어서 servlet container 자체에서도 관리를 하지 못하는 web application context가 된다. 즉
결론은, 틀린 코드라는 것이다. 이것이 잘 동작하게 만들려면 다음과 같이 코딩하는게 오히려 낫다. 지금 Servlet을 여러개를 운용하는게 아니니 그냥 root web application context를 servlet 개개인의 web application context로 설정하는 것이다. 이는 추후 Spring REST project에서도 활용하는 방식이니 참고.
ServletRegistration.Dynamic dispatcher =
container.addServlet("mvc", new DispatcherServlet(context));
그렇게 방금 만든 서블릿의 startup 우선순위도 지정, mapping도 더하고 있는데 이는 이 servlet이 담당할 request들이다. /및 하위 request를 다 여기서 처리하는데, 정확히는 front controller의 역할을 하며 이를 또 실제로 handle할 수 있는 controller에 전달한다고 생각하면 된다. 그 controller들은? @Controller로 표기한 녀석들이나 아까 config에서 설정한 녀석들이 해당된다. mapping 하는 기준은? @Controller에 있는 RequestMapping annotation들을 가지고 판단. 방금 이 정보들을 다 파악했으니 충분히 누구에게 어떤 상황에 전달할지 판단이 가능하다. 이 기본적인 작동 원리를 잘 파악하도록 하자.
위의 과정들은 Spring 5 이전에서는 안된다. Spring 5 이전의 경우 WebMvcConfigurerAdapter을 사용해야 한다고...
DispatcherServlet의 forwarding 대상인 controller을 직접 만드는 방법은? 간단한 controller은 다음과 같이 만드는게 가능하다.@Controller
public class SampleController {
@GetMapping("/sample")
public String showForm() {
return "sample";
}
}
ViewResolver, 정확히는 InternalResourceViewResolver에게 전해져서 적절한 view를 찾게 된다. 그게 뭘까? 앞에서 환경설정한 것 때문에 /WEB-INF/view/sample.jsp가 된다.
WEB-INF폴더에 넣음으로서 단순 URL을 통해 해당 파일들을 접근하지는 못하고, Spring application을 통해 요청을 해야만 받을 수 있는 것이 보장된다. Java에서 저 폴더에 있는 내용들이 브라우저에서 접근하지 못하도록 설정하라고 servlet container을 만들 때 규칙으로 지정했기 때문이다. 그래서 어떤 servlet container을 쓰든 이건 보장된다.
WEB-INF를 servlet들이 접근할 수 있는 이유는ServletContext관련 정보를 가지고 있기에 이를 활용하는 것이다.
또 이 controller은 /sample만 담당한다. 앞에서 /의 담당은 WebConfig에서 만든 controller이 담당하고 이 경우 /Web-Inf/view/index.jsp를 바로 요청자에게 전시한다는 점 유의.
참고로 여기서 사용한 sample.jsp는 다음과 같다.
<html>
<head></head>
<body>
<h1>This is the body of the sample view</h1>
</body>
</html>
@Controller annotation과 @RequestMapping에 관한건 이 글 참고. @RequestMapping으로 뭘 사용할 수 있는지는 이것을 참고하면 좋다.앞에는 전부 Spring의 기능이고, 이제 Spring boot에서 제공하는 기능을 알아보자.
알다시피 Spring Boot는 좀 더 빠르고 쉽게 동작하는 Spring application을 만드는 것이 목표다. 그래서 그거랑 관련된 기능들이 많이 있다. 한 번 보자면...
먼저 유용한 'starter'이라는 이름이 달린 dependency를 제공해준다. 이 dependency가 뭐냐면 그냥 동작하는 application들을 위해 필요로 하는 dependency를 죄다 모은 것이다.
사용 방법은... 그냥 다음과 같은 pom.xml을 만들면 된다. (maven)
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
</parent>
동작하는 application을 위해 dependency가 여러개 있으면 생기는 문제점은 각각의 어떤 버전 조합이 서로 충돌 문제 없이 애플리케이션을 동작할 수 있게 해주냐는 것이다.
그런데 위와 같은 starter들은 문제를 안일으키는 것이 확실한 버전 조합들을 찾아가지고 모은 것이기 때문에 문제 없이, 의도하는 기능의 애플리케이션을 만드는 것을 가능하게 해준다. 단 각 dependency의 버전을 마음대로 조작하지 못한다는 단점이 있다.
Spring Boot의 application들은 무조건 @SpringBootApplication이 달리고 main method가 달린 class를 필요로 한다. 이를 main entry point라고 한다.
@SpringBootApplication이 @Configuration, @EnableAutoConfiguration, @ComponentScan을 합한것이라고는 자주 말했다. 2번째 annotation도 설명한 적이 있다.
이 상태에서 thymeleaf나 JSP를 활용하는 프론트엔드를 위한 MVC controller을 이제는 어떻게 만드냐고요? 간단하다. spiring-boot-starter-thymeleaf를 집어넣고 아까 만든 controller을 넣으면 끝이다. configuration 관련 코드는 하나도 넣을 필요 없다. 앞의 ViewResolver을 넣는 것, WebConfig 구성하는 것, initializer 만드는 것 전부 다 필요 없다(...) 이걸 다 spring boot에서 해주기 때문... 그래도 앞의 내용들을 알아서 처리한다는거지 저걸 몰라도 된다는 것은 아니니 참고.