1. 스프링 MVC 설정
1. 스프링 MVC 구성요소 직접 빈 등록
앞에서 보았듯이 스프링은 아무설정없이도 dispatcherServlet.properties에 있는 설정
에 따라서 기본 빈을 등록하게 된다. 이 경우 그냥 new를 한 객체를 빈 등록 을 하게
된다
1 |
|
문제는 이런 기본 빈을 등록할때 그 안에서 쓰는
기본 값도 등록이 된다는 것이다. 앞에서 InternalResourceViewResolver의 커스텀 예제
에서 보았듯이 이 빈은 prefix와 suffix를 사용할 수 있지만 기본 값으로 등록 될때는
사용할 수 없는 상태로 등록이 된다
1 |
|
- 위의 뷰리졸버 외의 다른 기본 빈들도 외부에서 커스터마이징 할 수 있는 부분이 많다
1 |
|
- 설명
- 핸들러 인터셉터는 나중에 다시
- 이런식으로 필요한 것들을 설정하려면 결국에는 빈 설정을 직접 해야한다
- 기본 전략에 의존하기는 힘들다
- 그리고 위의 방법은 아주 로우 레벨 설정이며 설정이 힘들어진다
- 현재는 스프링 부트가
- 스프링 부트가 나오기 이전에도 이런식으로 하지는 않았음
- 이것보다 좀 편하게 설정 할 수 있도록 스프링 MVC가 제공해주는 방법이 존재
- 일일이 빈으로 등록하기보 보다 조금 더 자반기반 설정을 이용할 수 있게
그리고 어노테이션 기반의 mvc에서 좀더 편리하도록 제공하는 방법
–> @EnableWebMvc
2. @EnableWebMvc
- 어노테이션 기반 스프링 MVC를 사용할 때 편리한 웹 MVC 기본 설정
- @Configuration이 붙어 있는 자바 설정 클래스에 적용
- 원리
EnableWebMvc의 원리 : 임포트 하는 환경 설정 파일에서 빈들 설정 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65// EnableWebMvc 모습.
Retention(RetentionPolicy.RUNTIME)
//자바 설정 파일을 임포트 한다
public EnableWebMvc {
}
// 임포트 되는 파일은 WebMvcConfigurationSupport를 상속하고 있으며
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
...
}
// 이 클래스 안에서 @Bean으로 여러 필요 bean을 등록하고 있다
public class WebMvcConfigurationSupport implements ApplicationContextAware, ServletContextAware {
//수많은 빈들 등록중에 예로 하나만
/**
* Return a {@link RequestMappingHandlerMapping} ordered at 0 for mapping
* requests to annotated controllers.
*/
public RequestMappingHandlerMapping requestMappingHandlerMapping() {
RequestMappingHandlerMapping mapping = createRequestMappingHandlerMapping();
mapping.setOrder(0);
mapping.setInterceptors(getInterceptors());
mapping.setContentNegotiationManager(mvcContentNegotiationManager());
mapping.setCorsConfigurations(getCorsConfigurations());
PathMatchConfigurer configurer = getPathMatchConfigurer();
Boolean useSuffixPatternMatch = configurer.isUseSuffixPatternMatch();
if (useSuffixPatternMatch != null) {
mapping.setUseSuffixPatternMatch(useSuffixPatternMatch);
}
Boolean useRegisteredSuffixPatternMatch = configurer.isUseRegisteredSuffixPatternMatch();
if (useRegisteredSuffixPatternMatch != null) {
mapping.setUseRegisteredSuffixPatternMatch(useRegisteredSuffixPatternMatch);
}
Boolean useTrailingSlashMatch = configurer.isUseTrailingSlashMatch();
if (useTrailingSlashMatch != null) {
mapping.setUseTrailingSlashMatch(useTrailingSlashMatch);
}
UrlPathHelper pathHelper = configurer.getUrlPathHelper();
if (pathHelper != null) {
mapping.setUrlPathHelper(pathHelper);
}
PathMatcher pathMatcher = configurer.getPathMatcher();
if (pathMatcher != null) {
mapping.setPathMatcher(pathMatcher);
}
Map<String, Predicate<Class<?>>> pathPrefixes = configurer.getPathPrefixes();
if (pathPrefixes != null) {
mapping.setPathPrefixes(pathPrefixes);
}
return mapping;
}
// 밑으로 이런식으로 여러 빈 등록이 존재한다
public xxx xxx() {} - 찾을 수 있는 부분
- HandlerMapping에 추가할 기본적인 인터셉터 등록하는 부분
- 클래스패키지의 jaxb나 jackson, json등의 패키지를 검색해서 패키지가 존재하면
MediaType에 추가하는 부분, 아답터를 추가하는 부분, 컨버터를 추가하는 부분 - MessageConverter를 ExceptionHandlerResolver, HandlerAdapter등에 추가부분
->앞에서 봤듯이 패키지에 따라서 달라지는 컨버터가 추가 된다 - 스프링 부트랑 비슷한 컨셉이나 스프링 부트 훨씬 이전부터 존재
그러면 이전 @EnableWebMvc을 사용하지 않았을때 등록되는 빈들은 어떤 것들이었을까
DispatcherServlet의 initStrategies의 마지막 줄에 breakPoint를 건다
F8(Step over)로 한단계만 끝내면 위 단계로 모든 기본 빈을 등록한 상태이다
결과
- 지금까지 보아왔던 결과와 비슷하지만 비교를 위해서 3가지만 살펴본다
- handlerMapping에는 BeanNameUrlHandlerMapping, RequestMappingHandlerMapping
2개가 등록되며 RequestMappingHandlerMapping안의 interceptors 사이즈는 0
–> 인터셉터가 하나도 등록이 안되어 있음 - HandlerAdapter도 앞에서 보아왔듯이 3개가 등록 되어 있고 그 중 하나인
RequestMappingHandlerAdapter의 MessageConverters에는 컨버터 4개가 등록되어
있다 - viewResolver에는 InternalResourceViewResolver가 위의 예제처럼 prefix,suffix
입력된 채로 등록이 되어 있다
이제 @EnableWebMvc를 적용해 보자
1 |
|
이렇게만 하면 에러가 난다
에러가 안나도록 수정한다
1 | public class WebApplication implements WebApplicationInitializer { |
설명
- onStartup에서 만들어지는 AC에 SC를 set해준다
- @EnableWebMvc가 import하는 환경설정에서 SC를 참조하기 때문
- 따라서 DS가 사용하는 context에 SC가 set되어있어야 한다
디버거시 달라진 점
- HandlerMapping
- RequestMappingHandlerMapping이 BeanNameUrlHandlerMapping보다 먼저 등록
-> order 설정이 명시적으로 set 되어 있음 - RequestMappingHandlerMapping에 interceptors에 2개 인터셉터가 등록되어 있음
- RequestMappingHandlerMapping이 BeanNameUrlHandlerMapping보다 먼저 등록
- HandlerAdapter
- RequestMappingHandlerAdapter가 먼저 등록 되어 있음
–> 이 우선 순위로 성능적으로도 조금 더 이득이 있음 - RequestMappingHandlerAdapter에 MessageConverters에는 컨버터가 6개로
기존 4개보다 2개 더 등록 되어 있음 - 만약 의존성에 json이나 jackson등을 추가하면 추가한 부분을 처리할 컨버터가
추가로 등록된다
- RequestMappingHandlerAdapter가 먼저 등록 되어 있음
- ViewResolver
- 기존 1개보다 1개 더 많은 2개가 등록이 되어 있음
- 기존 1개는 예제에서 커스텀한 InternalResourceViewResolver가 등록
- 새로 등록된 ViewResolverComposite는 가지고 있는 viewResolvers가 0이여서
사실 이 예제에서는 큰 영향이 없다
- HandlerMapping
@EnableWebMvc가 import하는 설정파일 DelegatingWebMvcConfiguration이라는 이름에서
알 수 있듯이 이 설정은 delegation(위임)구조로 되어있다. @EnableWebMvc가 제공하는 기본 빈들의 커스터마이징 할 수 있게 해주는데 그 인터페이스 이름이 WebMvcConfigurer다
3. WebMvcConfigurer 인터페이스
@EnableWebMvc가 제공하는 빈들을 커스터마이징 할 수 있는 기능을 제공
기존 자바 설정파일에서 해당 인터페이스를 implements
굉장히 많은 확장 포인트를 제공한다
예를 들어서 위의 예에서는 prefix,suffix가 들어 있는 viewResolver빈을 @Bean으로
등록 했었다WebMvcConfigurer 인터페이스를 이용하면 viewResolver를 직접 빈으로 등록하지 않고
손쉽게 @EnableWebMvc가 제공해주는 viewResolver를 커스터마이징 해서 같은 결과를 낼 수 있다확장 포인트중 configureViewResolvers()를 사용하였다 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class WebConfig implements WebMvcConfigurer {
/*
//기존 직접 빈 등록 부분
@Bean
public ViewResolver viewResolver() {
InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
viewResolver.setPrefix("/WEB-INF/");
viewResolver.setSuffix(".jsp");
return viewResolver;
}
*/
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.jsp("/WEB-INF/",".jsp");
}
}설명
WebMvcConfigurer를 구현후 필요한 확장 포인트를 오버라이드 해서 사용
위의 경우 configureViewResolvers()를 오버라이드 해서 사용
디버거로 보면 ViewResolver에 ViewResolverComposite 1개만 등록 되어 있고
ViewResolverComposite가 가지는 viewResolvers에 1개의 InternalResourceViewResolver가 들어 있는 것을 확인 할 수 있다InternalResourceViewResolver를 확인하면 위에서 코드화한 prefix,suffix가 set
되어 있다
스프링 부트 사용할 때도 주로 사용 할 수 있는 인터페이스
so 확장 포인트를 잘 알아두는 것이 좋다
위에서 보는 저 모양이 스프링 부트 없이 사용하는 스프링 MVC의 모습 이다
- WebApplicationInitializer로 DS를 코드로 등록하고 SC를 set하고
- @Configuration 자바 설정 파일을 만들어 @EnableWebMvc를 붙이고
- WebMvcConfigurer 인터페이스 확장으로 빈 커스텀 부분을 정의
최근에는 스프링 부트 개발이 많아짐
- 학습을 위해선 스프링에서 제공하는 기능과 스프링 부트에서 제공하는 기능을 구분지어
생각하자
- 학습을 위해선 스프링에서 제공하는 기능과 스프링 부트에서 제공하는 기능을 구분지어
- ContentNegotiatingViewResolver (그냥)
- 가장 복잡한 viewResolver
- 다른 viewResolver에게 위임하면서 동작
- 사용자가 원하는 뷰에 따라서 동작하는 복잡한 viewResolver
- HTTP 헤더의 accpet-header에 원하는 content type을 넣으면 그 요청 헤더 맞는
응답을 선택해서 보여준다 - 스프링 부트는 기본 등록
2. 스프링 부트의 스프링 MVC 설정
스프링MVC-1.동작원리 에서 처음에 살펴보았던 프로젝트가 스프링 부트 프로젝트였었다 여기에서 디버거를 걸고 등록되는 빈들을 살펴보자handlerMapping
- 5개가 등록되어 있음
- favicconHandlerMapping은 파비콘 요청 처리
- resourceHandlerMapping은 정적 리소스 지원
- 캐싱 관련 정보들이 응답 헤더에 추가됨
- resource를 효율적으로 제공
- 예) 변경되지 않으면 NotModified(304응답)을 보내서 브라우저가 캐쉬하고있는
그 리소스를 그대로 사용
- 예) 변경되지 않으면 NotModified(304응답)을 보내서 브라우저가 캐쉬하고있는
- WelcomePageHandlerMapping은 처음 화면 set해주는 기능
HandlerAdapter
- 등록 갯수는 비슷하나 viewResolver가 5개가 들어가 있음
- 위에서 보았던 ContentNegotiatingViewResolver가 기본 등록되어 있음을 확인가능
- 타임리프 의존성을 가지고 있기 떄문에 thymeleafViewReolver도 등록 되어 있음
- ContentNegotiatingViewResolver 최고 우선 순위를 가지기 때문에 항상 이를
거치게 되며 나머지 viewResolver를 사용하는 형태를 가지게 된다
스프링 부트 스터디에서 보았듯이 스프링 부트의 AutoConfigure의 설정에 따라서
자동으로 DS부터 set된다스프링 부트의 “주관”이 적용된 자동설정이 동작
- JSP보다 thymeleaf 선호(JSP는 제약사항이 존재)
- JSON을 기본으로 지원(xml은 기본값 아님)
- 정적 리소스 지원 (welcome page, 파비콘등의 지원)
스프링부트의 스프링 MVC 커스터마이징
- @Configureation + @EnableWebMvc (+ implements WebMvcConfigurer)
- 스프링 부트의 스프링 MVC 자동설정을 사용하지 않는 방법이다
- 스프링 부트의 스프링WebMvc 자동 설정은 WebMvcAutoConfiguration 에 정의됨
- 여기 @ConditionalOnMissingBean({WebMvcConfigurationSupport.class})붙음
- @EnableWebMvc가 임포트 하는 DelegatingWebMvcConfiguration 설정파일이
WebMvcConfigurationSupport를 상속함 - 따라서 @EnableWebMvc를 사용하면 해당 자동 설정이 사용되지 않는다
- 자동 설정이 안되니 직접 설정을 해야하는데 위에서 봤듯이 직접 빈등록보다는
WebMvcConfigurer를 구현해서 편하게 사용하게 된다
- @Configureation (+ implements WebMvcConfigurer)
- 스프링 부트의 스프링MVC 자동설정 + 추가 설정
- 대부분의 경우 이것이 합리적인 선택
- application.properties
- 스프링의 자동 설정은 여러 properties를 사용하며 각 prefix가 선언되어 있다
- 이 prefix를 이용하여 application.properties를 사용하면 커스터마이징 가능
- 가장 형태를 덜 바꾸는 형태로 커스터마이징 가능한 방법
- 가능하면 이 방법을 선택
- @Configureation + @EnableWebMvc (+ implements WebMvcConfigurer)
기타
- converter,formatter 빈들을 등록하게 되어 있으므로 스프링 부트를 사용하는 경우에 한해서는 WebMvcConfigurer의 addformatter등을 사용하지 않아도 그냥 @Bean으로만 formatter를 만들어도 알아서 등록이 된다(된다는 거지 이렇게 하진 말자)
- 타임리프 자동완성을 살펴보면 기본적으로 prefix=classpath:/templates/, suffix=.html로 설정이 되어 있기 때문에 예제에서처럼 자동으로 실행이 된다
3. 스프링 부트에서 JSP 사용해보기
- 거의 사용안하는 일이지만 사용해보자
- 프로젝트 만들때 패키징은 war로 하게 해주어야한다
- 스프링 부트 의존성은 WEB을 추가
- jstl의존성과 jsp 사용을 위한 의존성은 따로 추가하여야 한다데모 프로젝트를 열면 기존 springboot의 SpringBootApplication외에 ServletInitializer 클래스가 있음을 확인 할 수 있다
pom.xml 1
2
3
4
5
6
7
8
9
10
11<!-- jstl 의존성 -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
</dependency>
<!-- JSP를 사용하기 위한 의존성 -->
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<scope>provided</scope>
</dependency> - ServletInitializer
- 스프링 부트는 어플리케이션을 패키징한 다음에 독립적인 jar로 실행 가능
- WAR 패키지로 하는 경우에는 웹 서버에 배포도 가능
- ServletInitializer가 상송하는 SpringBootServletInitializer가 위 예제에서 본
WebApplicationInitializer 인터페이스를 구현하고 있다 - 다시 말하면 스프링 부트에 최적화(커스터마이징)이 된 WebApplicationInitializer
- 더 손댈 필요는 없고 그냥 war 패키징 하면 된다
- 스프링 부트로 만드는 경우 빌드 툴의 래핑 도 같이 넣어준다(메이븐의 경우 mvnw)
따라서 그냥 mvnw package 하면 된다
그래서 그냥 독립적인 파일로 java -jar로 수행할때는 원래의 excutable Jar 형태인
@SpringBootApplication이 실행되는 것이고 , 톰캣에 배포하는 형태로 사용하게 될때는 ServletInitializer가 실행되서 다시 @SpringBootApplication이 붙은 메인 메소드를 가진 클래스를 실행하게 되는 것이다.
- 처음 스프링 예제처럼 소스를 작성한다
- webapp이 없으므로 webapp을 만들어서 webapp/WEB-INF/jsp/list.jsp 위치로 뷰.
- 뷰는 jsp로 작성하며 안에 jstl태그를 사용할 수 있도록 태그 라이브러리를 선언한다
- 반복부분은 core라이브러리의 <c:forEach>를 사용하면 된다
- spring태그 등은 따로 학습이 필요. jstl도 안써 봤을 경우 조금 학습이 필요
- prefix, suffix 설정은 위에서 말했던것처럼 스프링 부트 커스터마이징을 이용하면 되는데 application.properties를 이용하도록 하자스프링 부트에서는 JSP 사용을 권장하지 않는다
application.properties를 이용하면 이렇게 간단하게 커스터마이징 1
2spring.mvc.view.prefix=/WEB-INF/jsp/
spring.mvc.view.suffix=.jsp
If possible, JSPs should be avoided. There are several known limitations when using them with embedded servlet containers
- 제약사항
- JAR 프로젝트로 만들 수 없고 WAR 프로젝트로 만들어야한다
- 큰 제약사항은 아닌듯 하나 번거로움을 준다
- webapp디렉토리도 알고 있어야한다
- excutable war로 만들면 위에서 봤듯이 java -jar로 실행 가능하지만
excutable jar로 만드는 경우에는 JSP는 지원이 되지 않는다 - 톰캣이나 제티는 jsp를 지원하지만 언더토우 서블릿 컨테이너는 jsp를 지원 못한다
- Whitelabel 에러 페이지를 error.jsp로 오버라이딩 할 수 없음
- error.jsp를 만들어도 에러 핸들링의 기본 뷰를 오버라이드 하지 않기 때문에
커스텀 에러 페이지를 resources/error에 만들거나 ErrorViewResolver를
구현해야하는 번거로움이 생긴다
- error.jsp를 만들어도 에러 핸들링의 기본 뷰를 오버라이드 하지 않기 때문에
- JAR 프로젝트로 만들 수 없고 WAR 프로젝트로 만들어야한다
3. WebMvcConfigurer 설정
일단 새로 프로젝트를 만든고 다음과 같은 간단한 컨트롤러와 컨트롤러 테스트를 만든다
1 |
|
(인텔리 J의 경우 컨트롤러에서 ctrl+shift+T를 누르면 바로 테스트를 만들 수 있다)
1 | import org.junit.Test; |
만약 /hello/{이름} 의 요청이 오면 hello, {이름}으로 출력하게 컨트롤러를 만들면서
테스트를 바꾸려먼 어떻게 하면 좋을까?
먼저 테스트를 “/hello/ahn”으로 바꾸고 기대값을 “hello, ahn”으로 바꾼다.
당연히 테스트는 실패한다. 이제 테스트가 성공하도록 컨트롤러를 바꿔보자
1 |
|
- 테스트 성공
- 만약 VO객체나 엔티티인 Person이라는 객체가 있어서 Person 타입으로 받고 필요시
person.getName()등으로 꺼내쓰고 싶다면?
1 | public class Person { |
간단한 경우에는 암묵적 변환, 어노테이션을 사용한 변환에 대해 스프링이 디폴트로 설정한 데이터 바인딩이 사용된다
독자적 데이터 바인딩 설정시 스프링 4.2 이후면 Formatter 인터페이스 구현
Formatter
- 스프링 기초 [Spring 프레임워크 정말 기초 정리] 에서 공부 했음
- 되짚어 보자면
- DataBinding 추상화 쪽에서 PropertyEditor를 먼저 사용했었음
- PropertyEditor 단점을 극복하기 위해 Converter 사용
- 문자열에 치중된 사용자 입력값이나 다국화 메세지 기능등 좀더 웹쪽에 특화된
인터페이스가 필요 –> Formatter 등장
1 | public interface Formatter<T> extends Printer<T>, Parser<T> { |
- Printer & Parser
- Printer: 객체를 (Locale 정보를 참고하여) 문자열로 어떻게 출력할 것인가
- Parser: 문자열을 (Locale 정보를 참고하여) 객체로 어떻게 변환할 것인가
1 |
|
- 테스트 실패
- name으로 들어오는 문자열을 Person에 어떻게 매핑할지 스프링이 판단 불가
–> Formatter가 필요
- name으로 들어오는 문자열을 Person에 어떻게 매핑할지 스프링이 판단 불가
1 | import java.text.ParseException; |
이제 이 Formatter를 등록 해야 한다.
등록방법 : 2가지
1. WebMvcConfigurer의 addformatter(FormatterRegistry) 메소드 정의
1 |
|
설명
- 테스트 성공
- FormatterRegistry 인터페이스가 ConverterRegistry인터페이스를 상속한다
- 따라서registry.addConverter()로 Converter도 추가가 가능하다
- Converter는 좀더 General한 객체로 문자열이 아닌
일반적인 자바 객체 to 또다른 일반적 자바 객체의 변환을 다룬다 - 여기서는 Formatter로 충분
- 이렇게 Formatter를 환경설정에 추가를 해놓으면 이제 스프링은 Person을
문자열로 바꾸는 방법에 대해 알게 된다
Formatter는 URLPathVariable뿐 아니라 RequestParameter로도 동작한다
1
2
3
4
5
6
7
8
9
public class SampleController {
//PathVariable부분을 URL에서 제외
public String hello({ Person person)
return "hello, "+person.getName();
}
}- @GetMapping에서 “/hello”로 매핑한 후
- @PathVariable 대신 @RequestParam(“name”)으로 받는다
1 |
|
- perform안에서 .param으로 파라미터를 Mocking 할 수 있다
- .param(name, value)형태
- 브라우저에서 “/hello?name=ahn”)를 주는것과 같은 요청의 테스트
자 이제 2번째 방법은?
2. Formatter 빈 등록
- springboot 시에는 webConfig 없어도 된다
- Formatter가 빈으로 등록되어있다면 스프링 부트가 알아서 등록이 된다
- 방법
- 자바 환경설정에서 addFormatters 오버라이딩 한 부분 지운다
- 포매터인 PersonFormatter에 @Component를 붙여 빈 등록
- 테스트 실행 –> 에러 발생 !
- 에러발생 이유
- Test클래스에 붙은 @WebMvcTest는 slicing test용으로 웹 관련 bean만 등록
- 따라서 일반적인 @Component는 bean등록을 하지 않음
- 해결 방법
- 명시적으로 추가 빈을 추가하도록 필터설정
이렇게 하면 @WebMvcTest에 추가적인 빈 로드를 설정 할 수 있다 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
```
- 사용하는 통합 테스트로 변형
- 이 경우 모든 빈을 등록
- 이 경우에는 MockMvc가 자동으로 빈 등록이 되지 않는다
이를 해결하기 위해 를 붙여준다
```java + 로 해결하기
public class SampleControllerTest {
}
```
- 위의 2가지 방법 모두 테스트를 성공하게 된다
# 스프링 도메인 컨버터 자동 등록
Person을 도메인으로 보았을떄 실제 프로젝트에선 이런 이름보다는
키값에 해당하는 Id등을 매핑하게 된다
상황을 만들고 테스트를 만들자
- 과정
- Person 객체의 Long 타입의 id를 만들고 getter,setter 설정
- 컨트롤러의 파라미터의 이름도 name에서 id로 수정한다
- 테스트에서 파라미터도 id에 1로 넣도록 하자
- 테스트들 돌리면 HandlerException 에러가 떨어진다
>이 경우 formatter를 직접 만드는 것보다 스프링 데이터 JPA의 도움을 받을 수 있다
(Spring Data JPA를 사용하지 않는다면 이전 formatter와 비슷하게 다시 만들어야)
- JpaRepository를 상속받는 PersonRepository 인터페이스를 만든다
이제 자동으로 컨버터가 등록이 되어서 에러가 바뀌어서 NullException
- Repository에 테스트 데이터가 없어서 그러함
- 테스트를 수정하자
```java 변경한 테스트 클래스
//@WebMvcTest(includeFilters = {@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = PersonFormatter.class)})
public class SampleControllerTest {
MockMvc mockMvc;
PersonRepository personRepository;
public void hello() throws Exception {
//data 생성
Person person = new Person();
person.setName("ahn");
//레포지토리에 저장하고 반환되는 객체는
// JPA가 자동으로 만든 Id를 가지게 된다
Person savedPerson = personRepository.save(person);
this.mockMvc.perform(
get("/hello")
.param("id",savedPerson.getId().toString())) //String만 가능
.andDo(print())
//.andExpect(status().isOk())
.andExpect(content().string("hello, ahn"));
}
} - 이제 테스트 성공을 볼 수 있다
- 명시적으로 추가 빈을 추가하도록 필터설정
이처럼 스프링 데이터 JPA는 스프링 MVC용 도메인 클래스 컨버터를 제공
(기억이 안난다면 이건 JPA쪽 정리한 글 읽을것 .. 잊지좀 말자)
- Domain Class Converter
- Spring Data JPA가 제공하는 기능
- id에 해당하는 도메인 클래스로 변환을 해주는 컨버터가 자동으로 등록이 된다
- Repository를 사용해서 Id에 해당하는 엔티티를 읽어옴
핸들러 인터셉터
HandlerInterceptor
- HandlerMapping에 설정할 수 있는 Interceptor
- HandlerMapping에 Interceptor를 설정해 두면 그 HandlerMapping이 찾아주는
Handler에 이 Interceptor를 적용한다 - 해당 핸들러 실행하기 전,후(렌더링 전), 그리고 완료(렌더링 끝난 이후)시점에
부가작업을 하고 싶은 경우 사용할 수 있다 - 여러 핸들러에서 공통적이거나 반복적으로 사용하는 코드를 줄일 때 사용가능
예) 로깅, 인증 체크, Locale변경 등등
순서 파악 : 다음 순으로 실행이 된다
- preHandle
- 요청 처리(핸들러 실행)
- postHandle
- view rendering
- afterCompletion
- 원래 있던 2,4 전후로 해서 1,3,5 를 끼워넣을 수 있음이 핵심
인터셉터 템플릿 종류와 설명
- boolean preHandle(request, response, handler)
- 핸들러 실행 전 호출 됨
- 일반적인 서블릿 필터로 서블릿 요청 때 처리하는 것과 비슷함
- “핸들러”에 대한 정보를 사용 할 수 있기 때문에 서블릿 필터를 사용하는 것과
비교해서 훨씬 세밀한 로직 구현이 가능하다 - 리턴 값으로 계속 다음 인터셉터 혹은 핸들러로 요청 응답을 전달할지(true),
응답처리가 이곳에서 끝났는지(false) 알린다
- void postHandle(request, response, modelAndView)
- 핸들러 실행이 끝나고 아직 뷰를 렌더링 하기 이전에 호출 됨
- “View”에 전달할 추가적인 정보, 혹은 여러 핸들러에 공통적인 모델정보를 담는데
사용할 수 있다
- void afterCompletion(request, response, handler, ex)
- 요청 처리가 완전히 끝난 뒤(렌더링 끝난 뒤)에 호출 됨
- preHandler에서 true를 리턴한 경우에만 호출된다
- RestController의 경우에는 뷰 렌더링 과정이 없으므로 postHandle들을 종료후
바로 실행 된다
- 중요포인트
- postHandle,afterCompletion 메소드는 인터셉터 역순으로 호출된다
예를들어 postHandle1,postHandle2가 있다 2 1 순으로 호출된다
예제 보고 수정 - 비동기 요청 처리시에는 postHandle, afterCompletion이 호출되지 않는다
대신 AyncHandlerInterceptor가 제공하 다른 메소드가 호출 됨
나중에 비동기 공부할 떄 다시… - 인터셉터의 순서는 Order속성으로 줄 수 있다
서블릿 필터와의 차이
- 서블릿 필터도 마찬가지로 특정 서블릿 호출 이전 이후의 콜백을 제공
- preHandle의 handler 파라미터나 postHandle의 modelAndView 파라미터가 제공
되므로 핸들러에 따라 특정 로직의 변경도 가능하다 - 스프링 정보와 아무런 관련이 없는 일반적인 용도라면 서블릿 필터로 구현하는 것이
좋으며 스프링에 특화되어있다면 혹은 특화된 정보를 참고해야한다면 핸들러 인터셉터로 구현하는 것이 좋다 - 예를 들어 XSS attacks(Cross-Site Scripting attacks:웹 브라우저의 입력값을 받는 form에다가 script를 넣어서 그 게시물을 보는 다른 유저들에 대한 클라이언트에 대한 정보를 빼내가거나 응용할 수 있는 방법)을 차단하고자 한다면 당연히 서블릿 필터로 구현해야 할 것이다 . 스프링 mvc와 아무 관련없고 특정 정보를 볼 필요도 없으므로
핸들러 인터셉터 구현 : 간단하게 인터페이스를 구현해서 만들 수 있다
메세지만 출력하는 인터셉터 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17public class GreetingInterceptor implements HandlerInterceptor {
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("preHandle 1");
return true; //다음으로 계속 진행하도록
}
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("postHandle 1");
}
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("afterCompletion 1");
}
}그리고 이 파일을 그대로 복사 붙이기 해서 class이름과 출력 메세지만 2로 수정한다
위 파일에서 클래스 이름과 출력메세지에 2만 붙였다 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17public class AnotherInterceptor implements HandlerInterceptor {
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("preHandle 2");
return true; //다음으로 계속 진행하도록
}
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("postHandle 2");
}
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("afterCompletion 2");
}
}핸들러 등록: WebConfig에서 addInterceptors를 통해서 가능
1 |
|
테스트를 실행하면 다음과 같이 나오게 된다
1 | preHandle 1 |
만약 순서를 바꾸고 싶거나 명시적으로 순서를 주고 싶다면 order 를 주면 된다
1 | registry.addInterceptor(new GreetingInterceptor()).order(0); |
테스트 결과는 당연히 반대가 된다
1 | preHandle 2 |
인터셉터는 특정패턴에만 적용시킬 수도 있다
1 | registry.addInterceptor(new GreetingInterceptor()).order(0); |
이렇게 하고 테스트를 하면 2번째 인터셉터는 현재 테스트에는 반영되지 않기 때문에
첫번째 인터셉터만 실행된다
1 | preHandle 1 |
리소스 핸들러
- Default Servlet
- 모든 서블릿 컨테이너가 기본으로 제공하는 서블릿
- 이미지, 자바스크립트, CSS, HTML 파일과 같은 정적 리소스를 처리가능
- https://tomcat.apache.org/tomcat-9.0-doc/default-servlet.html
스프링은 정적 리소스 요청시 이렇게 서블릿 컨테이너에 기본 등록된 Default Servlet에
요청을 위임하여 처리한다
스프링 MVC 리소스 핸들러 매핑 등록
- 가장 낮은 우선순위로 등록
- 다른 핸들러 매핑이 요청을 처리하고 최종적으로 리소스 핸들러가 처리하도록
- 가장 낮은 우선순위로 등록
스프링 부트의 리소스 핸들러
- 아무런 설정 없이도 기본 정적 리소스 핸들러와 캐싱을 제공
- resources/static, resources/public등이 있다
- 예를 들어서 resource/static에 index.html에 만들고
/index.html을 요청하면 해당 html이 불러지는 것을 확인할 수 있다 - 캐싱이나 기타 설정은 application.properties에서 제어 할 수 있다
만약 스프링 부트를 사용하지 않는 환경이거나 혹은 스프링 부트를 사용하는데
임의의 리소스 핸들러를 추가로 더 등록 하고 싶은 경우에는 리소스 핸들러 설정이
필요하다리소스 핸들러 설정
- 지금까지 했던것처럼 WebConfig안에서 구현 가능
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class WebConfig implements WebMvcConfigurer {
/*
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addFormatter(new PersonFormmatter()); //Formatter 추가
}
*/
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new GreetingInterceptor()).order(0);
registry.addInterceptor(new AnotherInterceptor())
.addPathPatterns("/hi")
.order(-1);
}
//여기가 새로 추가되는 부분
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/mobile/**")
.addResourceLocations("classpath:/mobile/")
.setCacheControl(CacheControl.maxAge(10,TimeUnit.MINUTES));
}
}
- 지금까지 했던것처럼 WebConfig안에서 구현 가능
설명
- addResourceHandlers() 로 추가 가능
- URL패턴과 실제 매핑되는 Location을 매핑하였다
- 예제에서는 classpath:/mobile/로 매핑하였음
- resources도 클래스패스 잡혀있으므로 resources/mobile/을 만들고 html등을 만들면
/mobile/mobileIndex.html 등으로 매핑이 된다 - 설정을 여러가지가 가능한데 예제에서는 캐쉬만 10분으로 설정하였다
- 이렇게 설정하면 여기서 리턴하는 리소스들은 캐시전략이 응답헤더에 추가됨
- 리소스가 변경되지 않았다면 10분동안 캐싱(리소스가 변경되면 다시 받아온다)
- 이외의 설정
- 스프링 4.1 부터 제공되는 resourceChain을 쓰면
이것도 테스트를 만들 수 있다
- resource/mobile/mobileindex.html을 만들고 body에 “hello mobile index”
1 |
|
설명
- 이전 테스트와 흡사하나 다름
- content().String()이 String타입을 받는 메서드와 Matcher를 받는 메서드
2개가 있어서 정확한 매칭이 아닌 패턴을 찾는 경우에는 위와 같이 org.hamcrest.Matchers.containsString등의 Matcher로 처리 할 수 있다 - 캐시관련된 헤더가 응답헤더에 들어 있는지도 테스트한다;
- 서버를 띄운후 실제 크롬부라우저나 피들러로 요청을 연속으로 2번 하면
첫번째 요청은 200응답이 나오며 헤더에 캐시정보등이 없다
2번째 요청 헤더에는 Cache-Control 정보와 If-Modified-Since정보등이 들어 있고
이에 해당된 응답에는 304가 담긴 응답헤더만 가고 HTML의 바디가 가지 않는다 - 이렇게 리소스 응답 본문을 보내지 않기 때문에 응답시간도 줄고 트래픽도 줄게 된다
리소스 핸들러 설정 : addResourceHandlers안에서 등록하면서 설정
- 어떤 요청 패턴을 지원할 것인가?
- ex)
addResourceHandler("/mobile/**")
- ex)
- 어디서 리소스를 찾을 것인가?
- ex)
addResourceLocations("classpath:/mobile/")
,
ex)addResourceLocations("file:/root/data")
- classpath,file등 접두어가 아무것도 없다 src/main/Webapp에서 찾게됨
- war 프로젝트가 아니라면 대부분 classpath를 쓰게 됨
- ex)
- 캐싱
- ex)
setCacheControl(CacheControl.maxAge(10, TimeUnit.MINUTES))
- ex)
- ResourceResolver
- 요청에 해당하는 리소스를 찾는 전략
- 캐싱, 인코딩(gzip, brotli), WebJAr, …
- ResourceTransformer
- 응답으로 보낼 리소스를 수정하는 전략
- 캐싱, CSS링크, HTML5 AppCache, …
- 깊이 알기에는 난이도가 있음
- 공부하려고 깊게 들어가면 깊게 들어갈 곳이 얾마든지 많음
- 깊이 알기 위해서는 https://www.slideshare.net/rstoya05/resource-handling-spring-framework-41 참조
- 슬라이드 제대로 공부하려면 한시간 넘게 필요
- 어떤 요청 패턴을 지원할 것인가?
HTTP 메세지 컨버터
HTTP 메세지 컨버터
- 요청 본문에서 메세지를 읽어드리거나(@RequestBody),
응답 몬문에 메세지를 작성할 때 (@ResponseBody) 사용한다
- 요청 본문에서 메세지를 읽어드리거나(@RequestBody),
기본 HTTP 메세지 컨버터 :()한것은 pom.xml에 의존성이 있을 경우에만
- 바이트 배열 컨버터
- 문자열 컨버터
- Resource 컨버터
- Form 컨버터 (Form Data to/from MultiValueMap<String, String>)
- (JAXB2 컨버터) : xml용
- (Jackson2 컨버터) :JSON용
- (Jackson 컨버터) :JSON용
- (Gson 컨버터) :JSON용
- (Atom 컨버터) :Atom Feed
- (RSS 컨버터) : RSS Feed
- 그밖에 기본 컨버터들이 존재(스프링 부트도 그대로 사용)
설정 방법 : WebMvcConfigurer를 상속하는 WebConfig에 설정 가능
- 기본으로 등록해주는 컨버터를 다 무시하고 아예 새로 컨버터를 설정
- configureMessageConverters
- 일일히 다 해야하니 특별한 경우가 있지 않고는 거의 보기가 힘든 케이스
- 기본으로 등록해주는 컨버터에 새로운 컨버터 추가
- extendMessageConverters
- 이경우도 밑의 3번방법의로 추가되는 편한 컨버터들이 많기 때문에 직접 만든
컨버터를 등록하는 경우의 외에는 잘 쓰이지 않는다
- 의존성 추가로 컨버터 등록(추천)
- 메이븐/그래들에 의존성을 추가하면 그에 따른 컨버터가 자동으로 등록
- WebMvcConfigurationSupport 클래스에 정의
- 이 클래스는 스프링 부트가 아닌 스프링 프레임워크의 기능
- 이 클래스에서 현재 의존성 패키지를 보고 해당 패키지가 있으면 그에 맞는
컨버터를 추가 시켜 준다
간단한 예제
1 |
|
1 |
|
- 설명
- 핸들러는 GET요청의 BODY의 문자열을 그대로 반환한다
- 현재 컨트롤러인 SampleController는 @RestController이므로
html이 아닌 문자열 그대로 출력된다 - 테스트는 get요청 body에 hello라는 문자열이 있고 그 응답값이 그대로
나오는지 테스트한다 - 테스트는 성공
테스트에서 doPrint()로 출력된 테스트 내용 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37MockHttpServletRequest:
HTTP Method = GET
Request URI = /message
Parameters = {}
Headers = []
Body = hello
Session Attrs = {}
Handler:
Type = me.rkaehdaos.springmvcformatter.SampleController
Method = public java.lang.String me.rkaehdaos.springmvcformatter.SampleController.message(java.lang.String)
Async:
Async started = false
Async result = null
Resolved Exception:
Type = null
ModelAndView:
View name = null
View = null
Model = null
FlashMap:
Attributes = null
MockHttpServletResponse:
Status = 200
Error message = null
Headers = [Content-Type:"text/plain;charset=UTF-8", Content-Length:"5"]
Content type = text/plain;charset=UTF-8
Body = hello
Forwarded URL = null
Redirected URL = null
Cookies = [] - 응답 헤더의 Content-Type도 text/html대신 text/plain이다
- 요청 헤더에는 Content-Type을 지정하지 않았지만 설정 할 수도 있다
HTTP 메세지 컨버터 2 - JSON
스프링 부트 사용 여부에 따른 차이
- 스프링 부트를 사용하지 않을 때
- 사용하고 싶은 JSON라이브러리를 의존성으로 추가
- GSON
- JacksonJSON
- JacksonJSON 2
- 사용하고 싶은 JSON라이브러리를 의존성으로 추가
- 스프링 부트를 사용하는 경우
- 의존성에 spring-boot-starter-web을 추가하면 안에서 starter-json을
추가해주는데 여기서 자동으로 JacksonJSON 2를 가져오게 되어있다 - 위에서 말했듯이 WebMvcConfigurationSupport에 의해 의존성에 의해 컨버터를
가져오게 되면서 해당 JacksonJSON 2를 발견하고
JSON용 HTTP메세지 컨버터가 기본으로 등록된다 - 테스트를 사용해보자
- JSON 이 들어오면 JSON용 컨버터를 사용해서 객체에 매핑하고
- 이를 핸들러에서 출력값으로 리턴하면
- 다시 컨버터로 객체를 JSON으로 변환해서 출력하는 내용
- 의존성에 spring-boot-starter-web을 추가하면 안에서 starter-json을
1 |
|
1 |
|
- 설명
- 컨버터인 ObjectMapper를 주입받는다
- 테스트에 필요한 Person 객체를 만든다
- 해당 객체를 컨버터의 writeValueAsString()으로 JSON으로 변환하였다
- 컨버터 선택은 요청시의 헤더를 보고 판단하게 되므로 테스트시 요청 헤더도 작성
- .contentType으로 헤더에 현재 body 데이터를 보냈다
- .accept로 응답으로 받고자하는(혹은 받을수 있는)타입의 정보도 보냈다
이것은 작성안해도 성공하지만 명시적으로 하는 것이 가독성에 좋을 듯하다
- MockMvc에서 json응답은 json-path로 처리할 수 있다
- JSONPath: JSON 객체의 요소를 쿼리하는 표준화된 방법
- 자세한 내용은 다음 참조
HTTP 메세지 컨버터 3 - XML
OXM(Object-XML Mapper)라이브러리 중에 스프링이 지원하는 의존성 추가
- JacksonXML
- JAXB
1번에서 보았듯이 위의 라이브러리들은 ()처리가 되어있었다
이는 굳이 WebMvcConfigurer등에 메소드를 추가해주지 않아도
의존성만 추가해주면 자동으로 등록된다는 이야기
스프링 부트를 사용하는 경우 기본으로 XML 의존성을 추가해주지 않음
1 | <!--Jaxb Inerface--> |
의존성을 추가했으면 이제 Marshaller를 빈 설정해야 한다
1 |
|
도메인 클래스에도 추가 어노테이션이 필요하다
1 | //새로 추가. Jaxb에서 사용하는 루트엘리먼트를 알려주는 어노테이션 |
테스트 작성
1 |
|
- 설명
- 먀셜러를 빈 주입 받는다
- WebConfig에 등록했던 Jaxb2Marshaller 빈을 Marshaller로 받아온다
(org.springframework.oxm.Marshaller 패키지 확인 필요) - Jaxb2Marshaller가 이 인터페이스를 실제로 구현하고 있으므로 받아온다
- WebConfig에 등록했던 Jaxb2Marshaller 빈을 Marshaller로 받아온다
- 테스트 객체 생성 : 이것은 json테스트와 동일
- 객체를 XML로 변환
- marshaller의 marshal을 이용해서 xml로 변환한다
- 중간에 result 필요로 인해 StringWriter, StreamResult 사용하는 부분 주목
- 자동으로 등록한 Jaxb2Marshaller컨버터가 선택되서 먀살링,언먀샬링을 한다
- 이전 json테스트와 비교
- 헤더에서 정하는 body정보 헤더와 기대하는 응답정보의 타입을 수정
- jsongString대신 xmlString으로 수정
- url은 그대로 “jsonMessage”
- 어차피 핸들러 자체가 객체를 받아서 객체를 리턴하는 핸들러이므로
- jsonPath대신 xml에선 xpath()를 사용해서 xpath문법을 사용한다
- 2019도 String인것이 아쉽?
- .number로 해서 float타입으로 해서 2019.0으로 해도 성공하는 것을 보니
연도가 아닌 숫자인 경우에는 저렇게 해도 될듯 - jsonPath의 경우 value에 Object가 들어오는 반면 xml은 다 String처리인듯
- 번거로움
- JSON 사용할 때보다 훨씬 번거로움
- 스프링 부트에서 기본으로 제공해주지도 않음
- 의존성도 많이 필요 - 스프링부트 스타터 같은게 필요함
- 먀셜러를 빈 주입 받는다
그밖의 WebMvcConfigurer- 나중에 공부할 것들
지금까지 WebMvcConfigurer를 살펴보면서 자주 사용하는 설정을 살펴보았다
- formatter 추가 방법
- Interceptor 추가 방법
- resourceHandler 설정 방법
- MessageConverter 설정
위는 자주 사용하는 기능이며 나중에 공부해야할 내용들 정리
- CORS 설정
- Cross Origin 요청 처리 설정
- 같은 도메인에서 온 요청이 아니더라도 처리를 허용하고 싶다면 설정한다
- 리턴값 핸들러 설정
- 위의 예제들에서 이미 사용됨
- 예를들어 Json 사용시 리턴값 핸들러는 아마 Json 컨버터를 써서 값을 변환했을 것
(아마도) - 스프링 MVC가 제공하는 기본 리턴값 핸들러외의 핸들러를 추가하고자 할때 설정
- 아규먼트 리졸버 설정
- 스프링 MVC가 제공하는 기본 아규먼트 리졸버 외에 커스텀한 아규먼트 리졸버를
추가하고 싶을 떄 설정
- 스프링 MVC가 제공하는 기본 아규먼트 리졸버 외에 커스텀한 아규먼트 리졸버를
- 뷰 컨트롤러
- 단슌 URL에서 서비스 없이 바로 뷰로 연결할 경우 굳이 컨트롤러 생성이나 핸들러를 추가할 필요 없이 WebMvcConfigurer에 뷰컨트롤러를 추가해서 사용 가능
- 비동기 설정
- 비동기 요청 처리에 사용할 Timeout, TaskExecutor를 설정할 수 있다
- 뷰 리졸버 설정
- 핸들러에서 리턴하는 뷰 이름에 해당하는 문자열을 View 인스턴스로 바꿔줄
뷰 리졸버 설정 - 이미 예제에서 실습
- 핸들러에서 리턴하는 뷰 이름에 해당하는 문자열을 View 인스턴스로 바꿔줄
- ContentNegotiation 설정
- 요청 본문 혹은 응답 본문을 어떤(MIME)타입으로 보낼지 결정하는 전략 설정
- 위에서 처럼 헤더에 정보를 넣을 수 없는 경우에 필요
스프링 MVC 설정 요약
- 가장 기본적인 방법은 DispatcherServlet(DS)이 사용하는 모든 빈을 직접 등록
- @Configuration있는 자바설정 클래스에 @Bean사용으로 등록
- 기본 빈의 수도 많기도 해서 쉽지 않은 방법
- 가령 핸들러매핑도 핸들러매핑 뿐 아니라 핸들러매핑이 사용할 핸들러 인터셉터들도
핸들러 매핑안에 등록이 필요 - 핸들러 아답터도 그 자체뿐 아니라 필요에 따라(커스텀한 Http 컨버터를 쓴다던지)
설정이 엄청나게 늘어남 - 그래서 밑의 방법이 나옴
- @EnableWebMvc
- 어노테이션 기반 스프링MVC를 사용할 때 편리한 WebMvc기본설정
- @Configuration에 붙이면 기본 설정이 다 적용. 대부분의 경우 큰 문제가 없음
- 커스터마이징 하고 싶은 경우? 또 다시 다 등록? ㄴㄴ
- 밑의 방법 사용
- WebMvcConfigurer 인터페이스
- @EnableWebMvc가 제공하는 빈을 커스터마이징 할 수 있는 기능을 제공
- @EnableWebMvc이 제공하는 delegation로직에 따라 WebMvcConfigurer를 통해
기본 제공 빈을 커스터마이징 가능
- 스프링 부트
- 위의 설정이 아예 없이 기본적인 자동 설정 적용으로 다양한 기능
- @EnableWebMvc를 사용하면 스프링 부트 자동 설정이 off 되서 2번 방법으로 돌아간다. 스프링 부트 사용시에는 따라서 의도한 상황이 아니라면 사용하지 말자
- 커스텀이 필요하면 WebMvcConfigurer를 사용할 수도 있지만 application.properties를 이용하면 너무 쉽게 설정이 가능하다
- application.properties로 등록이 어려우면 다시 WebMvcConfigurer를 사용하고
그 방법으로도 커스터마이징이 힘든 경우가 있다면 직접 @Bean으로 등록한다