일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- AfterMapping
- mapstruct
- 개방 폐쇄 원칙
- java
- Socket is closed
- Java Rest
- HandlerMethodArgumentResolver
- requestheaderdto
- mTLS
- Unchecked Exception
- tomcat jndi
- Java Graphql
- Java Singleton
- Srping MVC
- 데이터 압축
- Sub Bytes
- Graphql Client
- Tomcat DBCP
- 상호 인증
- 바이트 절삭
- 이중정렬
- Jndi DataSource
- Reading HttpServletRequest Multiple Times
- WildFly
- Checked Exception
- graphql
- NoUniqueBeanDefinitionException
- Open Close Principal
- try - with - resources
- Request Body 여러 번 사용
- Today
- Total
Developer Sang Guy
스프링 WebMvcConfigurer 본문
1. WebMvcConfigurer 1부 Formatter
Formatter 란 어떤 객체를 문자열로 변환하거나 문자열을 객체로 변환 할 때 사용하는 인터페이스라고 합니다.
강의 내 예제에서는 Client 측으로 부터 요청 된 문자열을 객체로 변환하는 것을 볼수 있었습니다.
<사용 방법>
Formatter을 사용하기 위해선 Formatter 인터페이스를 상속받아야 하며 그 안에 있는 추상 메서드 Parser와 Printer를 구현해야 합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
public class PersonFormatter implements Formatter<Person> {
@Override
// 객체를 문자열로 어떻게 보여 줄 것인지
public String print(Person object, Locale locale) {
return object.toString();
}
@Override
// 문자열을 객체로 어떻게 변환 할 것인지
public Person parse(String text, Locale locale) throws ParseException {
Person person = new Person();
person.setName(text);
return person;
}
|
cs |
<Formatter을 상속 받았을 경우 Override 목록>

위 와 같이 목적에 맞는 Formatter를 생성한 뒤 아래와 같이 등록하면 정상적으로 사용이 가능합니다.
- WebMvcConfigurer 인터페이스의 addFormatters를 사용하여 등록
1
2
3
4
5
6
7
8
9
|
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addFormatter(new PersonFormatter());
}
}
|
cs |
- 생성한 Formatter를 스프링 Bean으로 등록
1
2
|
@Component
public class PersonFormatter implements Formatter<Person> {
|
cs |
테스트 소스 :
1
2
3
4
5
6
|
@GetMapping("/hello/{name}")
// name 이라는 이름으로 들어오는 자원을 어떻게 Person 객체로 받는지 스프링 MVC가 모름
// 그거를 알려줄수 있는 것이 Formatter.
public String hello(@PathVariable("name") Person person) {
return "hello " + person.getName();
}
|
cs |
1
2
3
4
5
6
7
|
@Test
public void hello() throws Exception {
this.mockMvc.perform(get("/hello/me"))
.andDo(print()) // Request 테스트 관련 정보 출력
.andExpect(content().string("hello me")); // 응답 값이 hello와 일치하는지 확인
}
|
cs |
결과 :
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
|
MockHttpServletRequest:
HTTP Method = GET
Request URI = /hello/me
Parameters = {}
Headers = []
Body = null
Session Attrs = {}
Handler:
Type = me.sdy01.SampleController
Method = me.sdy01.SampleController#hello(Person)
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:"8"]
Content type = text/plain;charset=UTF-8
Body = hello me
Forwarded URL = null
Redirected URL = null
Cookies = []
|
cs |
그런데.. 그냥 이렇게만 값을 받을거면 Formatter 만들지 않고도 받을수가 있다.
pathVariable 이름을 해당 클래스 타입의 특정 변수 이름으로 받는다면, 굳이 커스텀한 Formatter를 만들지 않아도 기본으로 등록되어 있는 JavaBean 스펙(Getter, Setter)을 따르는 컨버터를 통해서 Person 객체의 name 변수로 받아 올 수있다.
1
2
3
4
|
@GetMapping("/hello/{name}/{id}")
public String hello(Person person) {
return "hello " + person.getName() + person.getId();
}
|
cs |
1
2
3
4
5
6
7
8
9
|
MockHttpServletResponse:
Status = 200
Error message = null
Headers = [Content-Type:"text/plain;charset=UTF-8", Content-Length:"11"]
Content type = text/plain;charset=UTF-8
Body = hello me100
Forwarded URL = null
Redirected URL = null
Cookies = []
|
cs |
그 외 공부 :
- MockMvc 란?
어플리케이션을 서버에 배포하지 않고도 스프링 MVC 테스트를 진행 할 수 있도록 도와주는 클래스이다.
- MockMvc를 사용하기 위해서는
@WebMvcTest 또는 @SpringBootTest, @AutoConfigureMockMvc 를 등록 해줘야 한다.
@WebMvcTest : 슬라이스 테스트 어노테이션의 한 종류, MVC만을 위한 테스트이며 컨트롤러가 내 예상대로 동작하는지 테스트가 가능하다.
아래 내용만 스캔하므로 Service 로직까지 테스트는 불가능하다.
(@Controller, @ControllerAdvice, @JsonComponent, Converter, GenericConverter, Filter, HandlerInterceptor, WebMvcConfigurer, HandlerMethodArgumentResolver)
- 슬라이스 테스트 어노테이션 종류
@WebMvcTest
@WebFluxTest
@DataJpaTest
@JsonTest
@RestClientTest
- 슬라이스 테스트를 하는 이유는
사실 @SpringBootTest 사용하면 모든 테스트 다 가능하다.
하지만 @SpringBootTest의 경우 실제 구동되는 애플리케이션의 설정, 모든 Bean을 로드하기 때문에 시간이 오래 걸리고 무거우며 테스트 단위가 크기 때문에 디버깅이 어려운 편이다.
@SpringBootTest는 왠만하면 어플리케이션 컨텍스트 전체를 사용하는 통합 테스트에서 사용하면 좋을 것으로 보인다.
3. 핸들러 인터셉터
- 핸들러 인신터셉터는
핸들러 맵핑에 설정 할 수 있는 인터셉터이며 핸들러를 실행하기 전, 후 그리고 완료 시점에 부가 작업을 하고 싶은 경우에 사용 할 수 있다.
여러 핸들러에 반복적으로 사용하는 코드를 줄이고 싶을 때 사용하면 적절하다.
- 핸들러 맵핑?
어떠한 요청을 처리 할 Handler를 찾아주는 역할을 담당
- Servlet Fillter랑 차이가 뭐야?
Fillter 또한 기능 처리 전 또는 후 시점에 대한 CallBack을 제공하여 해당 시점에 부가 작업을 할 수가 있다.
Fillter의 경우 스프링과는 무관하여 스프링과 상관없는 일반적인 기능을 구현 시 사용하는게 올바르다.
ex. XSS(Cross-Site Scripting) 공격 필터
HandlerInterceptor의 경우 스프링 MVC에 관련 된 정보를 사용 할 수 있다. (Handler, ModelAndView)

HandlerInterceptor의 등록 구현 방법 및 등록은 아래 예제와 같다.
<구현 방법>
1 | public class GreetingInterceptor implements HandlerInterceptor { | cs |
<HandlerInterceptor에서 제공하는 추상 메서드>

- preHandle, postHandle, afterCompletion이 뭔데?

preHandle는 핸들러를 실행하기 전에 호출 되며 handler에 대한 정보를 사용 할 수 있기 때문에 Fillter에 비해 좀 더 다양한 기능을 구현 할 수 있다.
리턴 타입을 Boolean이며 해당 값에 따라 다음 인터셉터와 핸들러로 요청, 응답을 전달 할 지 결정 할 수 있다.
postHandle는 핸들러 실행이 끝나고 아직 뷰 페이지를 렌더링 하기 이전에 호출되며 ModelAndView를 통해 뷰 페이지에 모델 정보를 담거나 View 페이지 자체를 변경 할 수 있다.
이 메소드는 인터셉터 등록 순서와 역순으로 호출된다.
afterCompletion는 뷰 페이지를 렌더링이 끝난 뒤에 호출되며 postHandle와 동일하게 인터셉터 등록 순서와 역순으로 호출된다.
preHandle의 반환 값이 true 일 경우 postHandle의 실행 호출 여부와 상관없이 호출된다.
<GreetingInterceptor Handler 구현>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
public class GreetingInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("preHandle 1");
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("postHandle 1");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("afterCompletion 1");
}
}
|
cs |
<등록>
1
2
3
4
5
|
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new GreetingInterceptor());
}
|
cs |
<실행 및 결과>
1
2
3
4
5
6
7
8
9
|
@GetMapping("/helloView1")
public ModelAndView viewTest1(ModelAndView modelAndView) {
System.out.println("helloView1 Handler");
modelAndView.setViewName("helloView1");
return modelAndView;
}
|
cs |
1
2
3
4
|
preHandle 1
helloView1 Handler
postHandle 1
afterCompletion 1
|
cs |
<인터셉터의 preHandle Return false일 경우 결과>
1
|
preHandle 1
|
cs |
<AnotherInterceptor Handler 추가 구현>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("preHandle 2");
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("postHandle 2");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("afterCompletion 2");
}
|
cs |
<Handler 여러 개 등록>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
@Override
public void addInterceptors(InterceptorRegistry registry) {
/**
* 따로 Order 순서 지정안하면 위에서 부터 지정한 순서대로 실행
* order를 지정 할 경우 낮은 순서부터 실행
* addPathPatterns("path")를 통해 특정 페이지를 호출했을 경우에만 동작하게 할 수 있음
*/
registry.addInterceptor(new GreetingInterceptor())
.order(1);
registry.addInterceptor(new AnotherInterceptor())
.order(0);
// .addPathPatterns("path");
}
|
cs |
<결과>
1
2
3
4
5
6
7
|
preHandle 2
preHandle 1
view1 페이지로 이동
postHandle 1
postHandle 2
afterCompletion 1
afterCompletion 2
|
cs |
<처음으로 호출되는 인터셉터의 preHandle Return false일 경우 결과>
1
|
preHandle 2
|
cs |
<다음으로 호출되는 인터셉터의 preHandle Return false일 경우 결과>
1
2
3
|
preHandle 2
preHandle 1
afterCompletion 2
|
cs |
4. 리소스 핸들러
- 리소스 핸들러는
image, js, css, html 과 같은 정적 자원 처리를 하는 Handler 이다.
Serlvet Container에는 정적 자원 처리를 할 수 있는 Default Servlet가 이미 등록이 되어 있으며 내용은 아래와 같다.

스프링은 위와 같이 등록되어 있는 Default Servlet에 요청을 위임하여 정적 리소스 처리를 한다.
<동작 순서>

<등록 방법 servlet-context.xml>

<mvc:default-servlet-handler>을 선언하면 Default Servlet Handler이 Bean으로 등록되며 동작하게 된다.
스프링 부트에서는 아무 설정 없이 정적 자원 처리에 대한 기능을 지원받으며 관련 디렉토리는 아래와 같다.
<org.springframework.boot.autoconfigure.web.WebProperties.$Resources>

기본적으로 지원하는 기능 외에 자체적으로 리소스 핸들러를 등록하는 방법은 아래 예제와 같다.
<리소스 핸들러 설정>
1
2
3
4
5
6
7
8
9
10
11
|
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/mobile/**") // 적용 할 정적 자원 URL Path
.addResourceLocations("classpath:/mobile/") // 리소스가 들어있는 디렉토리 경로
.setCacheControl(CacheControl.maxAge(10, TimeUnit.MINUTES));
}
}
|
cs |
- 리소스 핸들러 설정은
1. 어떤 요청 패턴을 지원 할 것인지
2. 어디서 리소르를 찾을 것인지
3. 캐싱 전략
4. ResourceResolver : 요청에 해당하는 리소스를 찾는 전략
5. ResourceTransformer : 응답으로 보낼 리소스를 수정하는 전략
5. HTTP 메세지 컨버터
요청 본문에서 메세지를 읽어 들이거나(@RequestBody), 응답 본문에 메세지를 작성할 때(@ResponseBody) 사용하며
메세지 컨버터는 내가 읽을 요청 본문 또는 응답 할 응답 본문의 형식을 내가 원하는 것(JSON, XML 등)으로 쉽게 변환해준다.
HTTP 메시지 컨버터 종류

바이트 배열, 문자열, Resource, Form 컨버터는 기본으로 등록이 되어있으며 그 아래 괄호로 되어있는 컨버터는 pom.xml에 dependency가 있는 경우에만 등록이 된다.
자동 등록 관련 소스는 org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport 클래스에서 확인 할 수 있다.
<패키지 경로에 클래스가 있는지 없는지 확인>
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
|
public class WebMvcConfigurationSupport implements ApplicationContextAware, ServletContextAware {
/**
* Boolean flag controlled by a {@code spring.xml.ignore} system property that instructs Spring to
* ignore XML, i.e. to not initialize the XML-related infrastructure.
* <p>The default is "false".
*/
private static final boolean shouldIgnoreXml = SpringProperties.getFlag("spring.xml.ignore");
private static final boolean romePresent;
private static final boolean jaxb2Present;
private static final boolean jackson2Present;
private static final boolean jackson2XmlPresent;
private static final boolean jackson2SmilePresent;
private static final boolean jackson2CborPresent;
private static final boolean gsonPresent;
private static final boolean jsonbPresent;
private static final boolean kotlinSerializationJsonPresent;
static {
ClassLoader classLoader = WebMvcConfigurationSupport.class.getClassLoader();
romePresent = ClassUtils.isPresent("com.rometools.rome.feed.WireFeed", classLoader);
jaxb2Present = ClassUtils.isPresent("javax.xml.bind.Binder", classLoader);
jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader) &&
ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", classLoader);
jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader);
jackson2SmilePresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.smile.SmileFactory", classLoader);
jackson2CborPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.cbor.CBORFactory", classLoader);
gsonPresent = ClassUtils.isPresent("com.google.gson.Gson", classLoader);
jsonbPresent = ClassUtils.isPresent("javax.json.bind.Jsonb", classLoader);
kotlinSerializationJsonPresent = ClassUtils.isPresent("kotlinx.serialization.json.Json", classLoader);
}
|
cs |
<클래스가 존재 할 경우 컨버터 등록>
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
66
67
68
69
70
|
/**
* Adds a set of default HttpMessageConverter instances to the given list.
* Subclasses can call this method from {@link #configureMessageConverters}.
* @param messageConverters the list to add the default message converters to
*/
protected final void addDefaultHttpMessageConverters(List<HttpMessageConverter<?>> messageConverters) {
messageConverters.add(new ByteArrayHttpMessageConverter());
messageConverters.add(new StringHttpMessageConverter());
messageConverters.add(new ResourceHttpMessageConverter());
messageConverters.add(new ResourceRegionHttpMessageConverter());
if (!shouldIgnoreXml) {
try {
messageConverters.add(new SourceHttpMessageConverter<>());
}
catch (Throwable ex) {
// Ignore when no TransformerFactory implementation is available...
}
}
messageConverters.add(new AllEncompassingFormHttpMessageConverter());
if (romePresent) {
messageConverters.add(new AtomFeedHttpMessageConverter());
messageConverters.add(new RssChannelHttpMessageConverter());
}
if (!shouldIgnoreXml) {
if (jackson2XmlPresent) {
Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.xml();
if (this.applicationContext != null) {
builder.applicationContext(this.applicationContext);
}
messageConverters.add(new MappingJackson2XmlHttpMessageConverter(builder.build()));
}
else if (jaxb2Present) {
messageConverters.add(new Jaxb2RootElementHttpMessageConverter());
}
}
if (kotlinSerializationJsonPresent) {
messageConverters.add(new KotlinSerializationJsonHttpMessageConverter());
}
if (jackson2Present) {
Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.json();
if (this.applicationContext != null) {
builder.applicationContext(this.applicationContext);
}
messageConverters.add(new MappingJackson2HttpMessageConverter(builder.build()));
}
else if (gsonPresent) {
messageConverters.add(new GsonHttpMessageConverter());
}
else if (jsonbPresent) {
messageConverters.add(new JsonbHttpMessageConverter());
}
if (jackson2SmilePresent) {
Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.smile();
if (this.applicationContext != null) {
builder.applicationContext(this.applicationContext);
}
messageConverters.add(new MappingJackson2SmileHttpMessageConverter(builder.build()));
}
if (jackson2CborPresent) {
Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.cbor();
if (this.applicationContext != null) {
builder.applicationContext(this.applicationContext);
}
messageConverters.add(new MappingJackson2CborHttpMessageConverter(builder.build()));
}
}
|
cs |
추가로 스프링 부트에서는 기본적으로 jackson 라이브러리를 제공하므로 jackson 컨버터는 자동으로 등록이 되어 objectMapper 클래스를 사용 할 수있다.
jackson 라이브러리를 사용한 메시지 컨버터 예제
<Controller>
1
2
3
4
5
|
@GetMapping("/jsonMessage")
public Person message(@RequestBody Person person) {
return person;
}
|
cs |
<Test>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
@Test
void jsonMessageTest() throws Exception {
Person person = new Person();
person.setId(100L);
person.setName("Ahn");
ObjectMapper objectMapper = new ObjectMapper();
String jsonStr = objectMapper.writeValueAsString(person);
this.mockMvc.perform(get("/jsonMessage")
.contentType(MediaType.APPLICATION_JSON_VALUE) // 요청 컨텐트 타입
.accept(MediaType.APPLICATION_JSON_VALUE) // 응답받을 컨텐트 타입
.content(jsonStr))
.andDo(print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(100)) // jsonPath 사용하여 응답 값 비교
.andExpect(jsonPath("$.name").value("Ahn")); // jsonPath 사용하여 응답 값 비교
}
|
cs |
<결과>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
MockHttpServletRequest:
HTTP Method = GET
Request URI = /jsonMessage
Parameters = {}
Headers = [Content-Type:"application/json;charset=UTF-8", Content-Length:"23"]
Body = {"id":100,"name":"Ahn"}
MockHttpServletResponse:
Status = 200
Error message = null
Headers = [Content-Type:"application/json"]
Content type = application/json
Body = {"id":100,"name":"Ahn"}
Forwarded URL = null
Redirected URL = null
Cookies = []
|
cs |
기본적으로 제공하는 컨버터 외에 추가 적으로 등록 할 일은 굉장히 드물지만 어쩔수 없이 커스텀한 컨버터를 등록해야 할 경우엔 아래와 같은 방법으로 등록 할 수 있다.
1
2
3
4
5
6
7
8
9
|
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
}
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
}
|
cs |
위와 같이 2가지 방법이 있는데 차이는 아래와 같다.
- configureMessageConverters
기본적으로 제공하는 Message Converter를 대체한다. 기본적인 Message Converter를 대체하기 때문에 사용하는데 주의 필요
- extendMessageConverters
기본적으로 제공하는 Message Converter에 등록하고자 하는 Message Converter를 추가
'Spring' 카테고리의 다른 글
HttpServletRequest Body 여러 번 읽기 (0) | 2022.12.28 |
---|---|
[Spring] RequestMapping 기능 (produces, consumes ) (0) | 2022.10.07 |
[Spring Boot] DB 연결 (0) | 2022.09.28 |
[Spring] Aop, Logback 활용 (0) | 2022.04.28 |
Spring @Autowired를 사용한 스프링 빈 주입 (0) | 2021.05.31 |