자바 기반 환경 설정, 혹은 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을 하게 하거나scan
method를 써가지고 bean들을 생성을 한다. 그런데 web application의 경우 위와 같이 복잡한 과정을 거치는 이유가 servlet container의 동작방식이랑 호환되도록 하기 위해서 그렇다.애플리케이션 시작과 관련된
main
method가 여기서 등장하지 않는 것도 이 때문이다. servlet container에서 프로그램 전체 동작을 관리하기 때문이다. servlet 관리, servlet container 관리 (는 사실 본인 관리), 그리고 그 외의 요소들 관리를 다 거기서 책임지기 때문. 만일main
method를 여기서 우리가 추가로 만들면 위의 initializer과 같이 동작하면서 bean 관리 방식 전체가 꼬이게 될 수 있다.
boot의 경우 위와 같은 context 셋업따위 집어치우고 그냥 자동을 configure하는 것도 가능하지만 이는 나중 이야기.
ContextLoadListener
ServletContext
측에 위 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에서 해주기 때문... 그래도 앞의 내용들을 알아서 처리한다는거지 저걸 몰라도 된다는 것은 아니니 참고.