Baeldung의 이 글을 정리 및 추가 정보를 넣은 글입니다.

1. Overview

2. What Is Spring MVC?

  • 그런데 일단, 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 글은 다음에 자세히 다뤄보도록 하겠다.

  • 이 글에서는 이정도만 알면 된다.

    • Controller역할은 DispatcherServlet의 instance가 담당한다.
    • Model 역할은 그냥... 우리 애플리케이션의 데이터다.
    • View 역할은 다양한 템플릿 엔진들이 담당한다. Thymeleaf, Java Server Page (JSP) 등 다양한데 이에 관해서도 다음에 자세히 다뤄보도록 하겠다. Spring에서 지원해주는 외부 엔진들을 보통 활용한다.

이 글은 template engine으로 JSP를 사용한다.

3. Spring MVC Using Java Configuration

  • Java기반으로 configuration을 할 때 Spring MVC도 활용하고 싶으면 그냥 해당 configuration class에 @EnableWebMvc라는 annotation을 달면 된다.
@EnableWebMvc
@Configuration
public class WebConfig {

    /// ...
}

@EnableWebMvc

  • javadoc

  • 이 annotation을 달면 WebMvcConfigurationSupport라는 곳에서의 기본 Spring MVC configuration을 받게 된다. WebMvcConfigurationSupport javadoc

  • 즉 Web application 관련 MVC pattern 구현과 관련된 configuration 정보를 전달하는건 알겠는데 정확히 무슨 정보를 전달하는냐... 다음과 같다.

    • 애플리케이션에서 사용한 controller bean
    • 위의 각 controller들이 어떤 request를 담당하는지에 대한 mapping 정보 (@RequestMapping, @GetMapping 등)
    • Validation rule정보. controller에서 데이터 처리하기 전에 특정 조건에 맞는지 확인하는 규칙들이 뭔지에 대한 정보. @NotNull등이 대표적
    • 타입 전환자에 대한 정보. 데이터가 들어오면 그것을 적절한 Java object들로 변환하는 것을 담당하는 녀석들이다.
    • 메세지 전환자에 대한 정보. JSON, XML등의 다양한 양식으로 오가는 메세지를 변환하는 것을 담당하는 녀석들이다.
    • 예외 처리 관련 정보. controller 내부에서 @ExceptionHandler을 활용해 본인 예외 처리가 가능하고, @ControllerAdvice를 활용해 모든 controller에서 처리 되지 않는 예외를 처리하는 방식도 지정하는 것이 가능하다. 이에 대한 정보도 들어있다.
  • 위의 configuration에 몇가지를 더 추가하고 싶은 경우 annotated된 class에서 WebMvcConfigurer을 implement 한다음에 몇개를 override하거나 @Bean method를 더 만들면 된다..

  • 그리고 온전히 configuration을 다 직접 정의하고 싶은 경우 이 annotation을 사용하지 말고 WebMvcConfigurationSupport라는 class를 extend해가지고 직접 다 구현을 하면 된다. Baeldung에서는 전자의 방식으로 configuration을 조절했는데 이에 대해 더 알아보겠다.

WebMvcConfigurer

  • javadoc

  • 앞에 말했지만 이 인터페이스는 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가 뭔지를 지정한다. JstlViewInternalResourceView의 subclass인데 JSTL page를 활용하는 view다. 참고로 JSTL page는 JSP standard tag library를 활용하는 JSP다.
    • setPrefixsetSuffix는 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

  • configuration을 완료 했다고 다 끝나는게 아니다. 이 설정을 IoC container에서 scan 해가지고 bean으로 만든 다음에 application context에다가 laod 할 수 있어야 한다. 다만 이 경우에는 만든 애플리케이션이 웹 애플리케이션이다보니 root web application에다가 load를 해야 한다.

저 root web application context는 모든 servlet이 공유하는 context고, servlet별로 또 고유로 가질 configuration을 설정하려면 web application context를 별도로 생성해야 한다.

  • 이 때 보통 web.xml을 가지고 root web application context를 구성하지만, 프로그래밍을 통해 구성하는 방법이 있는데 WebApplicationInitializer의 구현체를 활용하는 것이다.

  • WebApplicationInitializer javadoc

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에서 자동으로 탐지해서 수행. 즉 다음과 같은 단계를 거친다.

    • 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

  • 이후 parameter로 전달받은 ServletContext측에 위 AnnotationConfigWebApplicationContext에 대한 listener을 추가한다. ContextLoaderListener이라고 되어 있는데, javadoc을 보면 저 class의 생성자의 용도가 WebApplicationContext의 lifecycle을 파악하는 것이다. 즉 servlet context는 root application context의 life cycle이 어떻게 변하는지를 파악하는 것이다. 본인이 모든 bean의 관리를 담당하기 때문에 그렇다. 그래서 web application 시작시에 얘가 생성을 해주고, 반대로 종료 시에는 얘가 관련 context를 다 미할당해버린다.
  • 이후 동적으로 저 ServletContext를 가진 servlet container의 관리 하에 작업을 할 서블릿을 추가하고 있다. 이를 위해선 ServletContextaddServlet이라는 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가 된다.

    • root web application context에 보유하는 bean 접근도 (parent 설정이 안되어서) 접근이 안되고
    • 이 녀석의 lifecycle 관리를 servlet container 차원에서 못한다.
  • 결론은, 틀린 코드라는 것이다. 이것이 잘 동작하게 만들려면 다음과 같이 코딩하는게 오히려 낫다. 지금 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을 사용해야 한다고...

4. Spring MVC Using XML Configuration

  • 여기서는 생략하겠다.

5. Controller and Views

  • 그러면 DispatcherServlet의 forwarding 대상인 controller을 직접 만드는 방법은? 간단한 controller은 다음과 같이 만드는게 가능하다.
@Controller
public class SampleController {
    @GetMapping("/sample")
    public String showForm() {
        return "sample";
    }

}
  • 여기서 상호작용이 매우 중요하다. request가 오면 먼저 controller이 이를 받고, 거기서 return하고 있는 string은 사실 view의 이름이다. 이게 앞에서 만든 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>

6. Spring MVC With Boot

  • 앞에는 전부 Spring의 기능이고, 이제 Spring boot에서 제공하는 기능을 알아보자.

  • 알다시피 Spring Boot는 좀 더 빠르고 쉽게 동작하는 Spring application을 만드는 것이 목표다. 그래서 그거랑 관련된 기능들이 많이 있다. 한 번 보자면...

6.1 Spring Boot Starters

  • 먼저 유용한 'starter'이라는 이름이 달린 dependency를 제공해준다. 이 dependency가 뭐냐면 그냥 동작하는 application들을 위해 필요로 하는 dependency를 죄다 모은 것이다.

  • 사용 방법은... 그냥 다음과 같은 pom.xml을 만들면 된다. (maven)

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
</parent>

starter

  • 동작하는 application을 위해 dependency가 여러개 있으면 생기는 문제점은 각각의 어떤 버전 조합이 서로 충돌 문제 없이 애플리케이션을 동작할 수 있게 해주냐는 것이다.

  • 그런데 위와 같은 starter들은 문제를 안일으키는 것이 확실한 버전 조합들을 찾아가지고 모은 것이기 때문에 문제 없이, 의도하는 기능의 애플리케이션을 만드는 것을 가능하게 해준다. 단 각 dependency의 버전을 마음대로 조작하지 못한다는 단점이 있다.

6.2 Spring Boot Entry Point

  • 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에서 해주기 때문... 그래도 앞의 내용들을 알아서 처리한다는거지 저걸 몰라도 된다는 것은 아니니 참고.

profile
안 흔하고 싶은 개발자. 관심 분야 : 임베디드/컴퓨터 시스템 및 아키텍처/웹/AI

0개의 댓글