스프링MVC-2.설정

1. 스프링 MVC 설정

1. 스프링 MVC 구성요소 직접 빈 등록

앞에서 보았듯이 스프링은 아무설정없이도 dispatcherServlet.properties에 있는 설정
에 따라서 기본 빈을 등록하게 된다. 이 경우 그냥 new를 한 객체를 빈 등록 을 하게
된다

밑의 @Bean은 아무설정없이 기본으로 등록되는 것과 같은 역할이다
1
2
3
4
5
6
7
8
9
10
@Configuration
@ComponentScan
public class WebConfig {

@Bean
public ViewResolver viewResolver() {
InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
return viewResolver;
}
}

문제는 이런 기본 빈을 등록할때 그 안에서 쓰는
기본 값도 등록이 된다는 것이다. 앞에서 InternalResourceViewResolver의 커스텀 예제
에서 보았듯이 이 빈은 prefix와 suffix를 사용할 수 있지만 기본 값으로 등록 될때는
사용할 수 없는 상태로 등록이 된다

커스텀한 뷰리졸버도 스프링 MVC 구성요소의 직접 빈등록 예가 된다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Configuration
@ComponentScan
public class WebConfig {

@Bean
public ViewResolver viewResolver() {
//기본 뷰 리졸버에
InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
//prefix, suffix를 set하고
viewResolver.setPrefix("/WEB-INF/");
viewResolver.setSuffix(".jsp");
//그 뷰 리졸버를 등록
return viewResolver;
}
}
  • 위의 뷰리졸버 외의 다른 기본 빈들도 외부에서 커스터마이징 할 수 있는 부분이 많다
1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
@ComponentScan
public class WebConfig {

@Bean
public HandlerMapping handlerMapping() {
RequestMappingHandlerMapping handlerMapping = new RequestMappingHandlerMapping();
handlerMapping.setInterceptors(); //핸들러 인터셉터는 나중에 다시
handlerMapping.setOrder(Ordered.HIGHEST_PRECEDENCE); //핸들러 매핑의 우선순위를 설정할 수 있음
return handlerMapping;
}
}
  • 설명
    • 핸들러 인터셉터는 나중에 다시
    • 이런식으로 필요한 것들을 설정하려면 결국에는 빈 설정을 직접 해야한다
    • 기본 전략에 의존하기는 힘들다
    • 그리고 위의 방법은 아주 로우 레벨 설정이며 설정이 힘들어진다
      • 현재는 스프링 부트가
      • 스프링 부트가 나오기 이전에도 이런식으로 하지는 않았음
      • 이것보다 좀 편하게 설정 할 수 있도록 스프링 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)
    @Target(ElementType.TYPE)
    @Documented
    @Import(DelegatingWebMvcConfiguration.class) //자바 설정 파일을 임포트 한다
    public @interface EnableWebMvc {
    }

    // 임포트 되는 파일은 WebMvcConfigurationSupport를 상속하고 있으며
    @Configuration
    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.
    */
    @Bean
    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;
    }

    // 밑으로 이런식으로 여러 빈 등록이 존재한다

    @Bean
    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를 적용해 보자

@Configuration가 붙은 자바설정 클래스에 적용한다
1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
@ComponentScan //@EnableWebMvc의 필수 요소는 아니다. 지금은 필요에 의해 사용중
@EnableWebMvc // 이런식으로 추가
public class WebConfig {

@Bean
public ViewResolver viewResolver() {
InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
viewResolver.setPrefix("/WEB-INF/");
viewResolver.setSuffix(".jsp");
return viewResolver;
}
}

이렇게만 하면 에러가 난다
에러가 안나도록 수정한다

context에 setServletContext로 SC를 설정해주어야 한다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class WebApplication implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext servletContext) throws ServletException {

AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();

//@EnableWebMvc 사용을 위해 다음을 추가
context.setServletContext(servletContext);

context.register(WebConfig.class);
context.refresh();

DispatcherServlet dispatcherServlet = new DispatcherServlet(context);
ServletRegistration.Dynamic app = servletContext.addServlet("app", dispatcherServlet);
app.addMapping("/app/*");
}
}
  • 설명

    • onStartup에서 만들어지는 AC에 SC를 set해준다
    • @EnableWebMvc가 import하는 환경설정에서 SC를 참조하기 때문
    • 따라서 DS가 사용하는 context에 SC가 set되어있어야 한다
  • 디버거시 달라진 점

    • HandlerMapping
      • RequestMappingHandlerMapping이 BeanNameUrlHandlerMapping보다 먼저 등록
        -> order 설정이 명시적으로 set 되어 있음
      • RequestMappingHandlerMapping에 interceptors에 2개 인터셉터가 등록되어 있음
    • HandlerAdapter
      • RequestMappingHandlerAdapter가 먼저 등록 되어 있음
        –> 이 우선 순위로 성능적으로도 조금 더 이득이 있음
      • RequestMappingHandlerAdapter에 MessageConverters에는 컨버터가 6개로
        기존 4개보다 2개 더 등록 되어 있음
      • 만약 의존성에 json이나 jackson등을 추가하면 추가한 부분을 처리할 컨버터가
        추가로 등록된다
    • ViewResolver
      • 기존 1개보다 1개 더 많은 2개가 등록이 되어 있음
      • 기존 1개는 예제에서 커스텀한 InternalResourceViewResolver가 등록
      • 새로 등록된 ViewResolverComposite는 가지고 있는 viewResolvers가 0이여서
        사실 이 예제에서는 큰 영향이 없다

@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
    @Configuration
    @ComponentScan
    @EnableWebMvc
    public class WebConfig implements WebMvcConfigurer {
    /*
    //기존 직접 빈 등록 부분
    @Bean
    public ViewResolver viewResolver() {
    InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
    viewResolver.setPrefix("/WEB-INF/");
    viewResolver.setSuffix(".jsp");
    return viewResolver;
    }
    */
    @Override
    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응답)을 보내서 브라우저가 캐쉬하고있는
          그 리소스를 그대로 사용
    • 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를 사용하면 커스터마이징 가능
      • 가장 형태를 덜 바꾸는 형태로 커스터마이징 가능한 방법
      • 가능하면 이 방법을 선택
  • 기타

    • converter,formatter 빈들을 등록하게 되어 있으므로 스프링 부트를 사용하는 경우에 한해서는 WebMvcConfigurer의 addformatter등을 사용하지 않아도 그냥 @Bean으로만 formatter를 만들어도 알아서 등록이 된다(된다는 거지 이렇게 하진 말자)
    • 타임리프 자동완성을 살펴보면 기본적으로 prefix=classpath:/templates/, suffix=.html로 설정이 되어 있기 때문에 예제에서처럼 자동으로 실행이 된다

3. 스프링 부트에서 JSP 사용해보기

  • 거의 사용안하는 일이지만 사용해보자
  • 프로젝트 만들때 패키징은 war로 하게 해주어야한다
  • 스프링 부트 의존성은 WEB을 추가
  • jstl의존성과 jsp 사용을 위한 의존성은 따로 추가하여야 한다
    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>
    데모 프로젝트를 열면 기존 springboot의 SpringBootApplication외에 ServletInitializer 클래스가 있음을 확인 할 수 있다
  • ServletInitializer
    • 스프링 부트는 어플리케이션을 패키징한 다음에 독립적인 jar로 실행 가능
    • WAR 패키지로 하는 경우에는 웹 서버에 배포도 가능
    • ServletInitializer가 상송하는 SpringBootServletInitializer가 위 예제에서 본
      WebApplicationInitializer 인터페이스를 구현하고 있다
    • 다시 말하면 스프링 부트에 최적화(커스터마이징)이 된 WebApplicationInitializer
    • 더 손댈 필요는 없고 그냥 war 패키징 하면 된다
    • 스프링 부트로 만드는 경우 빌드 툴의 래핑 도 같이 넣어준다(메이븐의 경우 mvnw)
      따라서 그냥 mvnw package 하면 된다

그래서 그냥 독립적인 파일로 java -jar로 수행할때는 원래의 excutable Jar 형태인
@SpringBootApplication이 실행되는 것이고 , 톰캣에 배포하는 형태로 사용하게 될때는 ServletInitializer가 실행되서 다시 @SpringBootApplication이 붙은 메인 메소드를 가진 클래스를 실행하게 되는 것이다.
스프링 부트 WAR 패키지가 실행된 모습

  • 처음 스프링 예제처럼 소스를 작성한다
  • webapp이 없으므로 webapp을 만들어서 webapp/WEB-INF/jsp/list.jsp 위치로 뷰.
  • 뷰는 jsp로 작성하며 안에 jstl태그를 사용할 수 있도록 태그 라이브러리를 선언한다
  • 반복부분은 core라이브러리의 <c:forEach>를 사용하면 된다
  • spring태그 등은 따로 학습이 필요. jstl도 안써 봤을 경우 조금 학습이 필요
  • prefix, suffix 설정은 위에서 말했던것처럼 스프링 부트 커스터마이징을 이용하면 되는데 application.properties를 이용하도록 하자
    application.properties를 이용하면 이렇게 간단하게 커스터마이징
    1
    2
    spring.mvc.view.prefix=/WEB-INF/jsp/
    spring.mvc.view.suffix=.jsp
    스프링 부트에서는 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를
        구현해야하는 번거로움이 생긴다

3. WebMvcConfigurer 설정

일단 새로 프로젝트를 만든고 다음과 같은 간단한 컨트롤러와 컨트롤러 테스트를 만든다

/hello의 url에 hello라고 응답하는 간단한 컨트롤러
1
2
3
4
5
6
@RestController
public class SampleController {

@GetMapping("/hello")
public String hello(){ return "hello";}
}

(인텔리 J의 경우 컨트롤러에서 ctrl+shift+T를 누르면 바로 테스트를 만들 수 있다)

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
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;

@RunWith(SpringRunner.class) //Runner 설정
@WebMvcTest //스프링 mvc의 웹관련 slicing test
public class SampleControllerTest {

@Autowired
MockMvc mockMvc; // @WebMvcTest 덕에 주입 가능

@Test
public void hello() throws Exception {
this.mockMvc.perform(get("/hello")) // "/hello"요청이 들어오면
.andDo(print()) // 과정은 출력하면서
.andExpect(content().string("hello"));// 기대값은 다음과 같음
}
}

만약 /hello/{이름} 의 요청이 오면 hello, {이름}으로 출력하게 컨트롤러를 만들면서
테스트를 바꾸려먼 어떻게 하면 좋을까?
먼저 테스트를 “/hello/ahn”으로 바꾸고 기대값을 “hello, ahn”으로 바꾼다.
당연히 테스트는 실패한다. 이제 테스트가 성공하도록 컨트롤러를 바꿔보자

매개변수에 따른 url로 매핑 하도록 컨트롤러 수정
1
2
3
4
5
6
7
8
9
@RestController
public class SampleController {

@GetMapping("/hello/{name}")
//URLPath로 받는다고들 한다
public String hello(@PathVariable String name){
return "hello, "+name;
}
}
  • 테스트 성공
  • 만약 VO객체나 엔티티인 Person이라는 객체가 있어서 Person 타입으로 받고 필요시
    person.getName()등으로 꺼내쓰고 싶다면?
Person.java
1
2
3
4
5
6
7
8
9
10
11
public class Person {
private String name;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
}
  • 간단한 경우에는 암묵적 변환, 어노테이션을 사용한 변환에 대해 스프링이 디폴트로 설정한 데이터 바인딩이 사용된다

  • 독자적 데이터 바인딩 설정시 스프링 4.2 이후면 Formatter 인터페이스 구현

  • Formatter

    • 스프링 기초 [Spring 프레임워크 정말 기초 정리] 에서 공부 했음
    • 되짚어 보자면
      • DataBinding 추상화 쪽에서 PropertyEditor를 먼저 사용했었음
      • PropertyEditor 단점을 극복하기 위해 Converter 사용
      • 문자열에 치중된 사용자 입력값이나 다국화 메세지 기능등 좀더 웹쪽에 특화된
        인터페이스가 필요 –> Formatter 등장
Formatter인터페이스는 Printer,Parser 2가지 인터페이스를 합친 것
1
2
public interface Formatter<T> extends Printer<T>, Parser<T> {
}
  • Printer & Parser
    • Printer: 객체를 (Locale 정보를 참고하여) 문자열로 어떻게 출력할 것인가
    • Parser: 문자열을 (Locale 정보를 참고하여) 객체로 어떻게 변환할 것인가
수정된 컨트롤러. name으로 들어온 패스 파라미터를 Person 객체에 매핑하고 싶다
1
2
3
4
5
6
7
8
9
@RestController
public class SampleController {

@GetMapping("/hello/{name}")
//URLPath로 받는다고들 한다
public String hello(@PathVariable("name") Person person){
return "hello, "+person.getName();
}
}
  • 테스트 실패
    • name으로 들어오는 문자열을 Person에 어떻게 매핑할지 스프링이 판단 불가
      –> Formatter가 필요
PersonFormatter.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.text.ParseException;
import java.util.Locale;

public class PersonFormatter implements Formatter<Person> {
@Override
public Person parse(String s, Locale locale) throws ParseException {
Person person = new Person();
person.setName(s);
return person;
}

@Override
public String print(Person person, Locale locale) {
return person.getName();
}
}

이제 이 Formatter를 등록 해야 한다.
등록방법 : 2가지

1. WebMvcConfigurer의 addformatter(FormatterRegistry) 메소드 정의

WebMvcConfigurer를 구현하는 WebConfig 자바 환경설정 클래스 추가
1
2
3
4
5
6
7
8
@Configuration
public class WebConfig implements WebMvcConfigurer {

@Override
public void addFormatters(FormatterRegistry registry) {
registry.addFormatter(new PersonFormatter()); //Formatter 추가
}
}
  • 설명

    • 테스트 성공
    • FormatterRegistry 인터페이스가 ConverterRegistry인터페이스를 상속한다
    • 따라서registry.addConverter()로 Converter도 추가가 가능하다
    • Converter는 좀더 General한 객체로 문자열이 아닌
      일반적인 자바 객체 to 또다른 일반적 자바 객체의 변환을 다룬다
    • 여기서는 Formatter로 충분
    • 이렇게 Formatter를 환경설정에 추가를 해놓으면 이제 스프링은 Person을
      문자열로 바꾸는 방법에 대해 알게 된다
  • Formatter는 URLPathVariable뿐 아니라 RequestParameter로도 동작한다

    1
    2
    3
    4
    5
    6
    7
    8
    9

    @RestController
    public class SampleController {

    @GetMapping("/hello") //PathVariable부분을 URL에서 제외
    public String hello(@RequestParam("name") Person person){
    return "hello, "+person.getName();
    }
    }
    • @GetMapping에서 “/hello”로 매핑한 후
    • @PathVariable 대신 @RequestParam(“name”)으로 받는다
1
2
3
4
5
6
7
8
@Test
public void hello() throws Exception {
this.mockMvc.perform(
get("/hello")
.param("name","ahn")) //파라미터 : name, value
.andDo(print())
.andExpect(content().string("hello, ahn"));
}
  • 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
      @WebMvcTest(includeFilters = {@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = PersonFormatter.class)})
      ```
      - @SpringBootTest 사용하는 통합 테스트로 변형
      - 이 경우 모든 빈을 등록
      - 이 경우에는 MockMvc가 자동으로 빈 등록이 되지 않는다
      이를 해결하기 위해 @AutoConfigureMockMvc를 붙여준다
      ```java @SpringBootTest + @AutoConfigureMockMvc로 해결하기
      @RunWith(SpringRunner.class)
      @SpringBootTest
      @AutoConfigureMockMvc
      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 변경한 테스트 클래스
      @RunWith(SpringRunner.class)
      //@WebMvcTest(includeFilters = {@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = PersonFormatter.class)})
      @SpringBootTest
      @AutoConfigureMockMvc
      public class SampleControllerTest {

      @Autowired
      MockMvc mockMvc;

      @Autowired
      PersonRepository personRepository;

      @Test
      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변경 등등
  • 순서 파악 : 다음 순으로 실행이 된다

    1. preHandle
    2. 요청 처리(핸들러 실행)
    3. postHandle
    4. view rendering
    5. afterCompletion
    • 원래 있던 2,4 전후로 해서 1,3,5 를 끼워넣을 수 있음이 핵심
  • 인터셉터 템플릿 종류와 설명

    1. boolean preHandle(request, response, handler)
    • 핸들러 실행 전 호출 됨
    • 일반적인 서블릿 필터로 서블릿 요청 때 처리하는 것과 비슷함
    • “핸들러”에 대한 정보를 사용 할 수 있기 때문에 서블릿 필터를 사용하는 것과
      비교해서 훨씬 세밀한 로직 구현이 가능하다
    • 리턴 값으로 계속 다음 인터셉터 혹은 핸들러로 요청 응답을 전달할지(true),
      응답처리가 이곳에서 끝났는지(false) 알린다
    1. void postHandle(request, response, modelAndView)
    • 핸들러 실행이 끝나고 아직 뷰를 렌더링 하기 이전에 호출 됨
    • “View”에 전달할 추가적인 정보, 혹은 여러 핸들러에 공통적인 모델정보를 담는데
      사용할 수 있다
    1. void afterCompletion(request, response, handler, ex)
    • 요청 처리가 완전히 끝난 뒤(렌더링 끝난 뒤)에 호출 됨
    • preHandler에서 true를 리턴한 경우에만 호출된다
    • RestController의 경우에는 뷰 렌더링 과정이 없으므로 postHandle들을 종료후
      바로 실행 된다
    1. 중요포인트
    • 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
    17
    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");
    }
    }

    그리고 이 파일을 그대로 복사 붙이기 해서 class이름과 출력 메세지만 2로 수정한다

    위 파일에서 클래스 이름과 출력메세지에 2만 붙였다
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public class AnotherInterceptor implements HandlerInterceptor {
    @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");
    }
    }
  • 핸들러 등록: WebConfig에서 addInterceptors를 통해서 가능

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Configuration
public class WebConfig implements WebMvcConfigurer {

//위의 예제 그대로 Spring Data JPA 사용 때문에 주석처리한 상태
/*
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addFormatter(new PersonFormmatter()); //Formatter 추가
}
*/

@Override
public void addInterceptors(InterceptorRegistry registry) {
//특별한 order가 없다면 add한 순서대로 등록 된다
registry.addInterceptor(new GreetingInterceptor());
registry.addInterceptor(new AnotherInterceptor());
}
}

테스트를 실행하면 다음과 같이 나오게 된다

pre는 1,2 순으로, 나머지는 위에서 설명한대로 역순인 2,1의 순서로 호출된다
1
2
3
4
5
6
preHandle 1
preHandle 2
postHandle 2
postHandle 1
afterCompletion 2
afterCompletion 1

만약 순서를 바꾸고 싶거나 명시적으로 순서를 주고 싶다면 order 를 주면 된다

낮을수록 높은 우선 순위. 따라서 이렇게 하면 우선순위가 바뀌게 된다
1
2
registry.addInterceptor(new GreetingInterceptor()).order(0);
registry.addInterceptor(new AnotherInterceptor()).order(-1);

테스트 결과는 당연히 반대가 된다

위의 테스트 결과와 정 반대의 결과가 나온다
1
2
3
4
5
6
preHandle 2
preHandle 1
postHandle 1
postHandle 2
afterCompletion 1
afterCompletion 2

인터셉터는 특정패턴에만 적용시킬 수도 있다

2번째 인터셉터를 특정 패턴시에만 적용되도록 변경하였다
1
2
3
4
registry.addInterceptor(new GreetingInterceptor()).order(0);
registry.addInterceptor(new AnotherInterceptor())
.addPathPatterns("/hi")
.order(-1);

이렇게 하고 테스트를 하면 2번째 인터셉터는 현재 테스트에는 반영되지 않기 때문에
첫번째 인터셉터만 실행된다

테스트 한 결과는 첫번쨰 인터셉터에서 출력한 메세지만 보인다
1
2
3
preHandle 1
postHandle 1
afterCompletion 1

리소스 핸들러

스프링은 정적 리소스 요청시 이렇게 서블릿 컨테이너에 기본 등록된 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
      @Configuration
      public class WebConfig implements WebMvcConfigurer {
      /*
      @Override
      public void addFormatters(FormatterRegistry registry) {
      registry.addFormatter(new PersonFormmatter()); //Formatter 추가
      }
      */

      @Override
      public void addInterceptors(InterceptorRegistry registry) {
      registry.addInterceptor(new GreetingInterceptor()).order(0);
      registry.addInterceptor(new AnotherInterceptor())
      .addPathPatterns("/hi")
      .order(-1);
      }

      //여기가 새로 추가되는 부분
      @Override
      public void addResourceHandlers(ResourceHandlerRegistry registry) {
      registry.addResourceHandler("/mobile/**")
      .addResourceLocations("classpath:/mobile/")
      .setCacheControl(CacheControl.maxAge(10,TimeUnit.MINUTES));
      }
      }

  • 설명

    • 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
2
3
4
5
6
7
8
9
@Test
public void helloStatic() throws Exception {
mockMvc.perform(get("/mobile/mobileindex.html"))
.andDo(print())
.andExpect(status().isOk())
.andExpect(content().string(containsString("hello mobile index")))
.andExpect(header().exists(HttpHeaders.CACHE_CONTROL))
;
}
  • 설명

    • 이전 테스트와 흡사하나 다름
    • 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) addResourceLocations("classpath:/mobile/"),
        ex) addResourceLocations("file:/root/data")
      • classpath,file등 접두어가 아무것도 없다 src/main/Webapp에서 찾게됨
      • war 프로젝트가 아니라면 대부분 classpath를 쓰게 됨
    • 캐싱
      • ex) setCacheControl(CacheControl.maxAge(10, TimeUnit.MINUTES))
    • ResourceResolver
      • 요청에 해당하는 리소스를 찾는 전략
      • 캐싱, 인코딩(gzip, brotli), WebJAr, …
    • ResourceTransformer
      • 응답으로 보낼 리소스를 수정하는 전략
      • 캐싱, CSS링크, HTML5 AppCache, …
    • 깊이 알기에는 난이도가 있음

HTTP 메세지 컨버터

  • HTTP 메세지 컨버터

    • 요청 본문에서 메세지를 읽어드리거나(@RequestBody),
      응답 몬문에 메세지를 작성할 때 (@ResponseBody) 사용한다
  • 기본 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에 설정 가능

    1. 기본으로 등록해주는 컨버터를 다 무시하고 아예 새로 컨버터를 설정
    • configureMessageConverters
    • 일일히 다 해야하니 특별한 경우가 있지 않고는 거의 보기가 힘든 케이스
    1. 기본으로 등록해주는 컨버터에 새로운 컨버터 추가
    • extendMessageConverters
    • 이경우도 밑의 3번방법의로 추가되는 편한 컨버터들이 많기 때문에 직접 만든
      컨버터를 등록하는 경우의 외에는 잘 쓰이지 않는다
    1. 의존성 추가로 컨버터 등록(추천)
    • 메이븐/그래들에 의존성을 추가하면 그에 따른 컨버터가 자동으로 등록
    • WebMvcConfigurationSupport 클래스에 정의
    • 이 클래스는 스프링 부트가 아닌 스프링 프레임워크의 기능
    • 이 클래스에서 현재 의존성 패키지를 보고 해당 패키지가 있으면 그에 맞는
      컨버터를 추가 시켜 준다

간단한 예제

SampleController에 요청Body에 들어오는 문자열을 출력하는 핸들러를 추가
1
2
3
4
@GetMapping("/message")
public String message(@RequestBody String body) {
return body;
}
만든 핸들러에 대한 테스트를 작성한다
1
2
3
4
5
6
7
8
9
10
@Test
public void message() throws Exception {
this.mockMvc.perform(
get("/message") //GET URL
.content("hello")) //GET BODY
.andDo(print()) //과정 출력
.andExpect(status().isOk()) //응답 기대값은 200
.andExpect(content().string("hello")) //응답 바디 기대값
;
}
  • 설명
    • 핸들러는 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
      37
      MockHttpServletRequest:
      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
  • 스프링 부트를 사용하는 경우
    • 의존성에 spring-boot-starter-web을 추가하면 안에서 starter-json을
      추가해주는데 여기서 자동으로 JacksonJSON 2를 가져오게 되어있다
    • 위에서 말했듯이 WebMvcConfigurationSupport에 의해 의존성에 의해 컨버터를
      가져오게 되면서 해당 JacksonJSON 2를 발견하고
      JSON용 HTTP메세지 컨버터가 기본으로 등록된다
    • 테스트를 사용해보자
      • JSON 이 들어오면 JSON용 컨버터를 사용해서 객체에 매핑하고
      • 이를 핸들러에서 출력값으로 리턴하면
      • 다시 컨버터로 객체를 JSON으로 변환해서 출력하는 내용
핸들러 작성
1
2
3
4
@GetMapping("/jsonMessage")
public Person jsonMessage(@RequestBody Person person){
return person;
}
핸들러에 대한 테스트
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Autowired
ObjectMapper objectMapper;

@Test
public void jsonMessage() throws Exception {

Person person = new Person();
person.setId(2019l);
person.setName("GeunChang");

//객체를 json 문자열로 변환

String jsonString=objectMapper.writeValueAsString(person);
this.mockMvc.perform(get("/jsonMessage")
.contentType(MediaType.APPLICATION_JSON_UTF8) //body안의 정보의 타입
.accept(MediaType.APPLICATION_JSON_UTF8) //기대하는 응답 정보의 타입
.content(jsonString))
.andDo(print())
.andExpect(status().isOk())
//.andExpect(content().string(jsonString))
.andExpect(jsonPath("$.id").value(2019))
.andExpect(jsonPath("$.name").value("GeunChang"))
;
}
  • 설명
    • 컨버터인 ObjectMapper를 주입받는다
    • 테스트에 필요한 Person 객체를 만든다
    • 해당 객체를 컨버터의 writeValueAsString()으로 JSON으로 변환하였다
    • 컨버터 선택은 요청시의 헤더를 보고 판단하게 되므로 테스트시 요청 헤더도 작성
      • .contentType으로 헤더에 현재 body 데이터를 보냈다
      • .accept로 응답으로 받고자하는(혹은 받을수 있는)타입의 정보도 보냈다
        이것은 작성안해도 성공하지만 명시적으로 하는 것이 가독성에 좋을 듯하다
    • MockMvc에서 json응답은 json-path로 처리할 수 있다

HTTP 메세지 컨버터 3 - XML

OXM(Object-XML Mapper)라이브러리 중에 스프링이 지원하는 의존성 추가

  • JacksonXML
  • JAXB

1번에서 보았듯이 위의 라이브러리들은 ()처리가 되어있었다
이는 굳이 WebMvcConfigurer등에 메소드를 추가해주지 않아도
의존성만 추가해주면 자동으로 등록된다는 이야기

스프링 부트를 사용하는 경우 기본으로 XML 의존성을 추가해주지 않음

JAXB 의존성 추가
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!--Jaxb  Inerface-->
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
</dependency>
<!-- glassfish에서 구현한 Jaxb 구현체 -->
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
</dependency>
<!-- Marshaller를 등록
xml과 객체를 서로 변환하는것을 마샬링 언먀샬링이라고 한다
그것을 추상해놓은 api를 스프링이 제공해준다
spring-oxm에서 먀살러도 제공하는데 이를 빈으로 등록해서 사용할 수 있다
-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-oxm</artifactId>
<version>${spring-framework.version}</version>
</dependency>

의존성을 추가했으면 이제 Marshaller를 빈 설정해야 한다

Marshaller 빈 등록
1
2
3
4
5
6
7
8
9
10
11
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Bean
public Jaxb2Marshaller jaxb2Marshaller(){
Jaxb2Marshaller jaxb2Marshaller = new Jaxb2Marshaller();
//그냥 등록이 아니라 Person의 패키지 이름부터 스캔
jaxb2Marshaller.setPackagesToScan(Person.class.getPackageName());
return jaxb2Marshaller;
}
//나머지 부분 생략
}

도메인 클래스에도 추가 어노테이션이 필요하다

person.java 도메인클래스 Person에 @XmlRootElement 어노테이션 추가
1
2
3
@XmlRootElement //새로 추가. Jaxb에서 사용하는 루트엘리먼트를 알려주는 어노테이션
@Entity
public class Person { /*내용생략*/ }

테스트 작성

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Test
public void xmlnMessage() throws Exception {

Person person = new Person();
person.setId(2019l);
person.setName("GeunChang");

//객체를 xml문자열로 변환
StringWriter stringWriter = new StringWriter();
Result result = new StreamResult(stringWriter);
marshaller.marshal(person,result);
String xmlString = stringWriter.toString();

this.mockMvc.perform(get("/jsonMessage")
.contentType(MediaType.APPLICATION_XML) //body안의 정보의 타입
.accept(MediaType.APPLICATION_XML) //기대하는 응답 정보의 타입
.content(xmlString))
.andDo(print())
.andExpect(status().isOk())
.andExpect(xpath("person/name").string("GeunChang"))
.andExpect(xpath("person/id").string("2019"))
;
}
  • 설명
    • 먀셜러를 빈 주입 받는다
      • WebConfig에 등록했던 Jaxb2Marshaller 빈을 Marshaller로 받아온다
        (org.springframework.oxm.Marshaller 패키지 확인 필요)
      • Jaxb2Marshaller가 이 인터페이스를 실제로 구현하고 있으므로 받아온다
    • 테스트 객체 생성 : 이것은 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가 제공하는 기본 아규먼트 리졸버 외에 커스텀한 아규먼트 리졸버를
      추가하고 싶을 떄 설정
  • 뷰 컨트롤러
    • 단슌 URL에서 서비스 없이 바로 뷰로 연결할 경우 굳이 컨트롤러 생성이나 핸들러를 추가할 필요 없이 WebMvcConfigurer에 뷰컨트롤러를 추가해서 사용 가능
  • 비동기 설정
    • 비동기 요청 처리에 사용할 Timeout, TaskExecutor를 설정할 수 있다
  • 뷰 리졸버 설정
    • 핸들러에서 리턴하는 뷰 이름에 해당하는 문자열을 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으로 등록한다

Related POST

공유하기