스프링부트 개발환경 구성-2 - MVC 환경구성

지난번에는 스프링부트 프로젝트의 생성부터 실행까지 해보았고, 정적인 웹컨텐츠 - html, js ,css등- 가 스프링부트에서 어떻게 관리되는지 살펴보았다. 이번에는 자바기반의 Server-Side 웹어플리케이션을 개발할때 많이 사용하는 Spring-MVC구성을 어떻게 하는지에 대해서 알아본다.

이 글은 JSP를 사용하기 위한 MVC구성에 관련된 글이기 때문에 스트링부트 환경구성의 기본전제는 다음과 같다.

  • SprngBoot 버전 - 1.5.7.RELEASE (현재는 2.1.X 까지 릴리지가 된 상태이지만 이 글을 작성한 시점에는 1.5버전대였다)

  • SpringBoot의 패키징 형식 - war

  • 임베디드 톰캣이 아닌 외부 톰캣을 사용

임베디드 톰캣기반의 jar 패키징 방식에서의 JSP 사용법은 다루지 않는다. 기본적으로 현재의 스프핑부트는 JSP 패키징을 지원하지 않는 흐름으로 바뀌는 추세이며, 이 글은 필연적으로 외부톰캣을 사용해야 하는 환경에서 기존 웹어플리케이션의 구조를 최대한 유지하면서 스프링부트로 MVC설정을 어떻게 하지는지에 대해서 다루고 있기 떄문이다.


스프링부트의 환경설정 파일 application.propeties의 설정

1부에서 프로젝트를 생성할때 만들어진 application.properties 파일을 연다. {base_path}/src/resources에 있다.

아무런 설정도 하지 않았다면 내용이 없는 빈 파일이 열릴 것이다. 참고로 SpringBoot에서부터 지원하는 application.yml 을 사용할 수도 있다. yaml파일에서의 속성 표기법은 구글링을 하면 쉽게 찾아볼 수 있으니 궁금하신 분들은 찾아보시길 바란다. 이 포스트는 application.properties 파일작성을 기준으로 설명한다.

아래와 같이 MVC 속성을 추가한다. port설정은 신경쓰지 말자. 설정하지 않아도 톰캣의 기본포트인 8080로 설정된다. 본인은 다른 포트를 써야만 하는 환경이라 추가하였다.

server.port=8888 #기본포트외의 다른 포트를 사용하고 싶으면 별도의 포트를 지정하자.

spring.mvc.view.prefix=/WEB-INF/views/
spring.mvc.view.suffix=.jsp

톰캣기반 자바 웹어플리케이션에서는 보안상 jsp의 위치를 URL로 직접 접근할 수 없는 WEB-INF폴더 아래에 위치시킨다. 아래에서 다시 언급하겠지만 이 설정들은 스프링부트가 AutoConfiguration을 통해서 이미 자동으로 설정한 속성값들을 위의 값으로 오버라이드를 하는 개념이다.

물론 spring mvc을 사용하기 위해서는 아래의 springboot web dependency가 pom.xml에 추가되어 있어야 한다.

<dependency>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-starter-web</artifactid>
</dependency>

이렇게 설정하고 웹어플리케이션을 구동하면 스프링부트는 web킷에 포함되어 있는 라이브러리들 중 MVC에 관련된 라이브러리들을 AutoConfiguration을 통해 기본설정값으로 세팅하면서 올라오게 된다.

앞서 얘기했던대로 스프링부트는 웹어플리케이션 개발환경에 필요한 대부분의 기본적인 설정들을 pom.xml 에 설정된 의존성라이브러리들을 대상으로 자동으로 설정해준다. 스프링부트의 메인 어플리케이션에 필수로 존재하는 @SpringBootApplication 이 이 역할을 담당한다.


SpringBoot의 핵심 - AutoConfiguration

SpringBoot Main Application

package com.springboot.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;

@SpringBootApplication
public class SpringbootRestDemoApplication {

   public static void main(String[] args) {
       SpringApplication.run(SpringbootRestDemoApplication.class, args);
  }
}

스프링부트의 메인어플리케이션 클래스는 @SpringBootApplication 클래스어노테이션을 필수로 가지고 있다. 이 어노테이션은 실제로는 3개의 어노테이션으로 구성되어 있고, 그 중 하나가 @EnableAutoConfiguration 이다.

@SpringBootApplication 을 클릭하면 아래와 같이 SpringBootApplication.class파일을 코드를 볼 수 있다.

package org.springframework.boot.autoconfigure;

import java.lang.annotation.Documented;
...

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
   excludeFilters = {@Filter(
   type = FilterType.CUSTOM,
   classes = {TypeExcludeFilter.class}
), @Filter(
   type = FilterType.CUSTOM,
   classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {
   @AliasFor(
       annotation = EnableAutoConfiguration.class
  )
   Class<?>[] exclude() default {};

   @AliasFor(
       annotation = EnableAutoConfiguration.class
  )
   String[] excludeName() default {};

   @AliasFor(
       annotation = ComponentScan.class,
       attribute = "basePackages"
  )
   String[] scanBasePackages() default {};

   @AliasFor(
       annotation = ComponentScan.class,
       attribute = "basePackageClasses"
  )
   Class<?>[] scanBasePackageClasses() default {};
}

상단의 클래스 어노테이션을 보면 3개의 어노테이션으로 구성된 것을 확인할 수 있다. 스프링부트의 핵심 어노테이션이다.

  • @SpringBootConfiguration

  • @EnableAutoConfiguration

  • @ComponentScan

기존 Java 또는 Spring기반의 개발환경을 드라마틱한 수준으로 간소화, 간편화 시킨 점에서 보면, 스프링부트의 핵심은 @EnableAutoConfiguration 이라 할 수 있다. 사실 스프링부트의 자동설정에 대해서 완벽한 수준으로 이해하려고 하면 상당한 이해력과 비용이 발생된다. 개인적으로는 그 정도까지 파고들 욕심이나 자신은 없으니, 관심있는 분들은 스프링의 레퍼런스 문서를 참조하길 바란다.

아래의 화면은 스프링부트의 자동설정클래스 - 기본적으로 제공되는 자동설정 클래스의 수도 상당히 많다 - 중 스프링부트 스타터킷중 web에 관련된 클래스중에서 WebMvcAutoConfiguration 을 담당하는 클래스에 대한 코드 일부이다.

// WebMvcAutoConfiguration.class의 일부

@Configuration
@ConditionalOnWebApplication(
   type = Type.SERVLET
)
@ConditionalOnClass({Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class})
@ConditionalOnMissingBean({WebMvcConfigurationSupport.class})
@AutoConfigureOrder(-2147483638)
@AutoConfigureAfter({DispatcherServletAutoConfiguration.class, ValidationAutoConfiguration.class})
public class WebMvcAutoConfiguration {
   public static final String DEFAULT_PREFIX = "";
   public static final String DEFAULT_SUFFIX = "";
   private static final String[] SERVLET_LOCATIONS = new String[]{"/"};

   public WebMvcAutoConfiguration() {
  }

   @Bean
   @ConditionalOnMissingBean({HiddenHttpMethodFilter.class})
   public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
       return new OrderedHiddenHttpMethodFilter();
  }

   @Bean
   @ConditionalOnMissingBean({HttpPutFormContentFilter.class})
   @ConditionalOnProperty(
       prefix = "spring.mvc.formcontent.putfilter",
       name = {"enabled"},
       matchIfMissing = true
  )
   public OrderedHttpPutFormContentFilter httpPutFormContentFilter() {
       return new OrderedHttpPutFormContentFilter();
  }

   static class OptionalPathExtensionContentNegotiationStrategy implements ContentNegotiationStrategy {
       private static final String SKIP_ATTRIBUTE = PathExtensionContentNegotiationStrategy.class.getName() + ".SKIP";
       private final ContentNegotiationStrategy delegate;

       OptionalPathExtensionContentNegotiationStrategy(ContentNegotiationStrategy delegate) {
           this.delegate = delegate;
      }

       public List<MediaType> resolveMediaTypes(NativeWebRequest webRequest) throws HttpMediaTypeNotAcceptableException {
           Object skip = webRequest.getAttribute(SKIP_ATTRIBUTE, 0);
           return skip != null && Boolean.parseBoolean(skip.toString()) ? MEDIA_TYPE_ALL_LIST : this.delegate.resolveMediaTypes(webRequest);
      }
  }
// WebMvcProperties.class의 일부
package org.springframework.boot.autoconfigure.web.servlet;

import java.time.Duration;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.http.MediaType;
import org.springframework.validation.DefaultMessageCodesResolver.Format;

@ConfigurationProperties(
   prefix = "spring.mvc"
)
public class WebMvcProperties {
   private Format messageCodesResolverFormat;
   private Locale locale;
   private WebMvcProperties.LocaleResolver localeResolver;
   private String dateFormat;
   private boolean dispatchTraceRequest;
   private boolean dispatchOptionsRequest;
   private boolean ignoreDefaultModelOnRedirect;
   private boolean throwExceptionIfNoHandlerFound;
   private boolean logResolvedException;
   private String staticPathPattern;
   private final WebMvcProperties.Async async;
   private final WebMvcProperties.Servlet servlet;
   private final WebMvcProperties.View view;
   private final WebMvcProperties.Contentnegotiation contentnegotiation;
   private final WebMvcProperties.Pathmatch pathmatch;

   public WebMvcProperties() {
       this.localeResolver = WebMvcProperties.LocaleResolver.ACCEPT_HEADER;
       this.dispatchTraceRequest = false;
       this.dispatchOptionsRequest = true;
       this.ignoreDefaultModelOnRedirect = true;
       this.throwExceptionIfNoHandlerFound = false;
       this.logResolvedException = false;
       this.staticPathPattern = "/**";
       this.async = new WebMvcProperties.Async();
       this.servlet = new WebMvcProperties.Servlet();
       this.view = new WebMvcProperties.View();
       this.contentnegotiation = new WebMvcProperties.Contentnegotiation();
       this.pathmatch = new WebMvcProperties.Pathmatch();
  }

이 두개의 클래스를 통해 스프링부트는 Spring MVC 환경에 필요한 기본적인 속성값들을 AutoConfiguration을 통해 자동을 설정하게 된다. 위에서 application.propeties파일에서 spring.mvc. 까지 프로퍼티를 입력하게 되면 자동완성기능을 통해 에디터에 표시되는 속성들은 모두 WebMvcProperties.class에 정의된 속성들을 보여주는 것이고, 우리가 새롭게 정의하는 값으로 오버라이딩이 되는 것이다.

우리는 위에서 두가지 속성, spring.mvc.view.prefix 와 spring.mvc.view.suffix 을 새롭게 정의하였다. 나머지 mvc관련 속성들은 이 클래스의 기본설정값으로 적용된다.

이전 글에서 자동 JDBC설정을 피하기 위해 메인클래스에 아래의 코드를 추가를 했었는데 기본적으로 스프링부트는 등록된 의존성라이브러리에 대해서 기본적으로 AutoConfiguration이 활성화 되어 있어 특정 Configuration 만 제외처리를 했던 것이다.


@EnableAutoConfiguration(exclude={DataSourceAutoConfiguration.class})

스프링부트가 나오기 전까지는 ViewResolver를 설정하기 위해서 application-context.xmlservlet-context.xml에 추가하거나, JavaConfig 기반의 환경일 경우, @Configuration 클래스 - WebMvcConfig등의 클래스 - 에 ViewResolver설정을 해줘야 했었다.

스프링부트가 나옴으로 인해서 WebMvc, JDBC, JPA, LOGGING등 Spring기반 웹어플리케이션 구동에 필요한 대부분의 설정들은 위의 @EnableAutoConfiguration에 의해 자동설정되고 외부로는 - 개발자가 확인가능한 코드 - 들어나지 않는다. 다만, 필요한 속성값만 오버라이딩해서 사용하면 된다.

MVC를 설명하면서 스프링부트의 자동설정 부분을 조금 깊게 언급한 것 같은데, 앞으로 스프링부트 기반의 어플리케이션을 커스터마이징하는 수준으로 개발하기 위해서는 자동설정에 대한 기본적인 이해는 필수라고 생각한다. AutoConfiguration이 작동하는 원리정도는 꼭 알아두는 것이 좋겠다.


MVC 기반의 요청 및 응답

지금까지 MVC설정을 아주 힘들게(?) 했으니 실제로 확인을 해보자. 먼저 간단한 Controller 클래스를 만드는데, 클래스가 만들어지는 위치가 중요하다.

스프링부트는 컴포넌트 스캔을 할 때, 기본적으로 메인클래스가 있는 위치를 기준으로 컴포넌트 스캔을 하게된다.

만약, AutoScan이 되어야 하는 컴포넌트 클래스들 - 대표적으로 @Controller, @Service, @Repository, @Component등-의 위치가 메인클래스가 위치한 패키지보다 상위 패키지에 있거나, 하위가 아닌 다른 패키지에 있는 경우, 스캔이 되지 않는다. 물론 이 설정도 임의의 패키지로 스캔이 되도록 따로 지정할 수 있다.

package com.springboot.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;

@SpringBootApplication
@EnableAutoConfiguration(exclude={DataSourceAutoConfiguration.class})
@ComponentScan(basePackages = "지정할 베이스 패키지")
public class SpringbootRestDemoApplication {

   public static void main(String[] args) {
       SpringApplication.run(SpringbootRestDemoApplication.class, args);
  }
}

스프링부트를 처음 시작할때 이 부분을 몰라 몇시간을 헤맨적이 있었다. 클래스의 생성위치를 꼭 확인하고 basePackage로 지정된 패키지 하위에 위치시키도록 하자.

MVC설정이 제대로 동작하는지 확인하기 위해서 아래와 같이 컨트롤러클래스를 간단하게 만들자

package com.springboot.demo.controller;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;


@Controller
public class HelloSpringBootController {

 @RequestMapping(value="/hello")
 public String hellSpringBoot() {
   return "hello";
}
}

스프링부트 어플리케이션을 구동하고 브라우저를 통해 해당 URL을 접속해 보자. hello.jsp가 호출되면 정상이다.

다시 얘기하지만, 외부톰캣구동 방식의 war패키징 구조로 설정되어 있어야 한다. 이 글에서는 pom.xml파일을 외부톰캣 구동방식으로 어떻게 설정하는지에 대해서는 다루지 않는다.



지금까지의 설정으로 MVC기반의 웹어플리케이션을 개발할 수 있는 아주 기본적인 환경설정이 되었다고 볼 수 있다.

사실, 현재의 스프링부트는 view의 영역에 대한 JSP 지원이 계속 없어지고 있는 추세이다. 서버사이드와 클라이언트의 로직이 모두 공존하는 JSP기반의 UI구현은 이제 전문 프런트앤드 라이브러리-VueJs,Reactive,Angular등-를 통해 구현되는 추세로 빠르게 대체되고 있고, 서버사이드 어플리케이션은 클라이언트 요청의 결과를 전달하는 역할만을 담당하는 형태로 서로의 Role이 명확히 구분되고 있다.


이런 기술적인 변화로 보면 이 글에서의 view설정에 대한 부분은 현재 스프링부트를 적용해야 하는 상황에서는 시대착오적일 수 있으나, 스프링부트의 기본원리를 이해하는데 도움이 될 만한 다른 내용들도 포함하고 있어 지금 스프링부트를 처음 접하는 분들에게는 일부 의미있는 정보이지 않을까 생각한다.


참고문서

  • 네이버 블러그 공유하기
  • 네이버 밴드에 공유하기
  • 페이스북 공유하기
  • 카카오스토리 공유하기