Archive/Spring Web MVC

2.6 HTTP 메시지 컨버터 (JSON, XML)

nimkoes 2021. 7. 9. 01:24
728x90

 

 

HTTP 메시지 컨버터 (이하 메시지 컨버터) 에 대해 정리한다.

메시지 컨버터 역시 WebMvcConfigurer 를 통해 설정할 수 있다.

 

우선 메시지 컨버터는 HTTP request body 또는 response body 에 담긴 데이터를 원하는 형태로 변환하여 사용할 수 있는 기능이다.

MVC 에서 Controller 역할을 하는 handler method 가 요청 정보를 읽을 때는 @RequestBody annotation 을 사용하고, 응답 정보를 보낼 때는 @ResponseBody annotation 을 사용한다.

 

앞서 만들었던 MyHelloController 에 HTTP Get 요청을 처리할 수 있는 handler method 를 하나 추가했다.

 

package me.nimkoes;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class MyHelloController {

    @GetMapping("/hello")
    public String hello(@RequestParam("name") Person person) {
        System.out.println("handler method execute !!");
        return "hello " + person.getName();
    }

    @GetMapping("/message")
    public String message(@RequestBody Person person) {
        return "hello person";
    }
}

 

아래쪽에 HTTP GET 방식의 /message 요청을 처리할 수 있는 handler method 를 하나 추가했는데,

@RequestBody annotation 을 사용해서 HTTP request body 의 데이터를 Person 타입의 객체로 변환하도록 했다.

그리고 @ResponseBody annotation 을 사용해서 "hello person" 을 응답 데이터 본문에 실어야 하는데 사용하지 않았다.

 

그 이유는 MyHelloController 클래스에 @RestController annotation 을 작성했기 때문이다.

이 annotation 을 Controller 역할을 하는 class 에 붙여주면 이 안에 정의한 handler method 는 모두 @ResponseBody annotation 을 작성한 것과 같은 효과를 가진다.

 

실제로 @RestController annotation 이 어떻게 구현되어 있는지 보면 @ResponseBody annotation 이 붙어있는 것을 볼 수 있다.

 

/*
 * Copyright 2002-2017 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.web.bind.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.springframework.core.annotation.AliasFor;
import org.springframework.stereotype.Controller;

/**
 * A convenience annotation that is itself annotated with
 * {@link Controller @Controller} and {@link ResponseBody @ResponseBody}.
 * <p>
 * Types that carry this annotation are treated as controllers where
 * {@link RequestMapping @RequestMapping} methods assume
 * {@link ResponseBody @ResponseBody} semantics by default.
 *
 * <p><b>NOTE:</b> {@code @RestController} is processed if an appropriate
 * {@code HandlerMapping}-{@code HandlerAdapter} pair is configured such as the
 * {@code RequestMappingHandlerMapping}-{@code RequestMappingHandlerAdapter}
 * pair which are the default in the MVC Java config and the MVC namespace.
 *
 * @author Rossen Stoyanchev
 * @author Sam Brannen
 * @since 4.0
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Controller
@ResponseBody
public @interface RestController {

	/**
	 * The value may indicate a suggestion for a logical component name,
	 * to be turned into a Spring bean in case of an autodetected component.
	 * @return the suggested component name, if any (or empty String otherwise)
	 * @since 4.0.1
	 */
	@AliasFor(annotation = Controller.class)
	String value() default "";

}

 

@RestController 는 @ResponseBody 도 있지만 @Controller 타입이기도 한 것을 확인할 수 있다.

 

만약 @RestController 를 사용하지 않고 그냥 @Controller 를 사용했다면, 다음과 같이 응답 결과를 HTTP response body 영역에 실어 보내야 하는 handler method 에 대해 @ResponseBody annotation 을 명시해 주어야 한다.

그렇지 않고 String 타입의 값을 반환하려 한다면 우선 그 문자열에 매칭되는 View 반환을 시도하게 되어 원하는 결과를 받아볼 수 없을 수 있다.

 

package me.nimkoes;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class MyHelloController {

    @GetMapping("/hello")
    @ResponseBody
    public String hello(@RequestParam("name") Person person) {
        System.out.println("handler method execute !!");
        return "hello " + person.getName();
    }

    @GetMapping("/message")
    @ResponseBody
    public String message(@RequestBody Person person) {
        return "hello person";
    }
}

 

테스트 해보기 위해 @ResponseBody 를 제거 하자마자 똑똑한 IDEA 가 일치하는 View 가 없다고 경고를 보여주고 있다.

 

 

실제로 요청을 보낸 결과 다음과 같이 500 에러를 받았다.

 

{
    "timestamp": "2021-07-08T12:45:00.765+0000",
    "status": 500,
    "error": "Internal Server Error",
    "message": "Error resolving template [hello person], template might not exist or might not be accessible by any of the configured Template Resolvers",
    "path": "/message"
}

 

서버에서도 TemplateEngine 쪽에서 오류가 발생한걸 볼 수 있었다.

 

2021-07-08 21:45:00.763 ERROR 16036 --- [io-8080-exec-10] org.thymeleaf.TemplateEngine             : [THYMELEAF][http-nio-8080-exec-10] Exception processing template "hello person": Error resolving template [hello person], template might not exist or might not be accessible by any of the configured Template Resolvers

 

Spring 에는 따로 등록하지 않아도 기본적으로 제공해주는 메시지 컨버터가 있다.

- 바이트 배열 컨버터

- 문자열 컨버터

- Resource 컨버터

- Form 컨버터 (html form data 를 map 형태로 변환)

 

 

메시지 컨버터를 등록해서 사용하는 방법은 WebMvcConfigurer 인터페이스를 구현한다는 것은 동일하지만

1. configureMessageConverters 메소드를 재정의 하거나

2. extendMessageConverters 메소드를 재정의 하는

두 가지 방법이 있다.

 

이 두 방법은 차이가 있는데 configurerMessageConverters 를 재정의하면 자동 등록 된 메시지 컨버터를 무시하고 이곳에 등록한 메시지 컨버터만 사용 한다는 것을 의미하고, extendMessageConverters 를 재정의하면 자동 등록 된 메시지 컨버터에 더해서 메시지 컨버터가 추가 된다는 점이다.

 

    /**
     * Configure the {@link HttpMessageConverter HttpMessageConverters} to use for reading or writing
     * to the body of the request or response. If no converters are added, a
     * default list of converters is registered.
     * <p><strong>Note</strong> that adding converters to the list, turns off
     * default converter registration. To simply add a converter without impacting
     * default registration, consider using the method
     * {@link #extendMessageConverters(java.util.List)} instead.
     * @param converters initially an empty list of converters
     */
    default void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
    }
 
     /**
     * A hook for extending or modifying the list of converters after it has been
     * configured. This may be useful for example to allow default converters to
     * be registered and then insert a custom converter through this method.
     * @param converters the list of configured converters to extend.
     * @since 4.1.3
     */
    default void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
    }

 

하지만 사실 이렇게 직접 메시지 컨버터를 추가하는 일도 흔한 경우는 아니다.

왜냐하면 그 외 일반적으로 많이 사용하는 JAXB2, Jackson2, Jackson, Gson, Atom, RSS 등 메시지 컨버터는 pom.xml 에 depondency 로 해당 의존성이 추가되어 있는 경우 자동으로 등록 되기 때문이다.

다시 말해서 classpath 에 이 라이브러리가 존재 한다면 자동으로 등록 되어 사용할 수 있다는 것을 뜻한다.

 

classpath 에 라이브러리가 있으면, pom.xml 에 dependency 로 의존성만 추가가 되어있으면 자동으로 메시지 컨버터를 등록해주는 곳은 WebMvcConfigurationSupport 에 구현되어 있다.

WebMvcConfigurationSupport 클래스는 Spring Boot 가 아닌 Spring Web MVC 에서 제공하는 기능이다.

 

이 클래스 파일에서 관련된 주요한 내용만 추려 보았다.

 

// 생략...

public class WebMvcConfigurationSupport implements ApplicationContextAware, ServletContextAware {

    // 생략..

    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);
    }

    // 생략..

    /**
     * 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()));
        }
    }

    // 생략..

}

 

위에 언급한 기본 제공하는 메시지 컨버터들은 Spring 뿐만 아니라 Spring Boot 에서도 동일하게 적용 된다.

Spring Boot 관련해서 내용을 추가 하자면 Spring Boot 는 spring-boot-starter-web 을 통해 spring-boot-starter-json 이 의존성으로 추가 되고, 이 starter json 이 Jackson2 를 가져오도록 되어 있다.

 

 

그래서 Spring Boot 를 쓰면 별도의 메시지 컨버터를 등록하거나 dependency 에 의존성 추가 없이 json 형태의 데이터를 위한 메시지 컨버터를 바로 사용할 수 있다.

 

 


 

 

실제로 메시지 컨버터를 사용하는 예제를 만들어 본다.

 

앞서 만들었던 /message handler method 를 다음과 같이 수정하여 문자열 데이터를 잘 처리하는지 확인해보자.

 

package me.nimkoes;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class MyHelloController {

    @GetMapping("/hello")
    public String hello(@RequestParam("name") Person person) {
        System.out.println("handler method execute !!");
        return "hello " + person.getName();
    }

    @GetMapping("/message")
    public String message(@RequestBody String body) {
        return "Hello, " + body;
    }
}

 

HTTP request body 에 문자열을 전달 받은 내용을 사용해서 response body 에 담아 보내는 handler mothod 로 수정했다.

그리고 다음과 같이 테스트 코드를 작성하고 실행해 보았다.

 

    @Test
    public void stringMessage() throws Exception {
        this.mockMvc.perform(get("/message").content("nimkoes"))
            .andDo(print())
            .andExpect(status().isOk())
            .andExpect(content().string("Hello, nimkoes"));
    }

 

MockHttpServletRequest:
      HTTP Method = GET
      Request URI = /message
       Parameters = {}
          Headers = {}
             Body = nimkoes
    Session Attrs = {}

Handler:
             Type = me.nimkoes.MyHelloController
           Method = public java.lang.String me.nimkoes.MyHelloController.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=[14]}
     Content type = text/plain;charset=UTF-8
             Body = Hello, nimkoes
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

 

의도한 대로 잘 동작하는 것을 확인할 수 있었다.

 

 

그럼 한가지 의문이 생기는데, request 와 response 에 대해 어떤 형태의 데이터를 원하는지 어떻게 알 수 있을까.

결론부터 얘기하면 HTTP 의 header 정보를 참고하여 어떤 형태의 데이터가 요청 본문에 들어오고, 이 요청은 어떤 형태의 데이터를 응답 본문에 담아야 할지 결정한다.

 

HTTP header 정보를 사용하여 json 형태의 데이터를 객체로 변환하고, 변환 된 객체 정보를 json 형태로 응답 하는 handler method 를 만들어 보았다.

 

    @GetMapping("/jsonMessage")
    public Person jsonMessage(@RequestBody Person person) {
        System.out.println(person.getName());
        person.setName("My name is " + person.getName());
        return person;
    }

 

이 handler 는 json 형식으로 된 데이터를 HTTP request body 에 담아 보내면,

Person 타입으로 변환해서 person 객체에 담고, 이 객체를 다시 json 형식으로 변환하여 HTTP response body 에 담아 보내도록 할 것이다.

 

정상적으로 동작하는지 확인하기 위한 테스트 코드를 작성했다.

 

package me.nimkoes;

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.*;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.hamcrest.Matchers;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;

@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class MyHelloControllerTest {

    // @WebMvcTest annotation 을 정의하면 MockMvc 를 주입받을 수 있다.
    @Autowired
    MockMvc mockMvc;

    // Spring Boot 를 사용하여 기본적으로 등록 된 bean 중 Jackson 이 제공하는 ObjectMapper 를 사용
    @Autowired
    ObjectMapper objectMapper;

    @Test
    public void jsonMessage() throws Exception {

        Person person = new Person();
        person.setName("nimkoes");

        // 객체를 json 문자열로 변환
        String jsonString = objectMapper.writeValueAsString(person);

        this.mockMvc.perform(get("/jsonMessage")
                .contentType(MediaType.APPLICATION_JSON_UTF8)  // context type 을 사용하여 request 데이터가 json 형식임을 알려줌
                .accept(MediaType.APPLICATION_JSON_UTF8)       // accept 를 사용하여 response 데이터가 json 형식이기를 바란다고 알려줌
                .content(jsonString))                          // request body 에 jsonString 입력
            .andDo(print())
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.name").value("My name is nimkoes"));
    }
}

 

MockHttpServletRequest:
      HTTP Method = GET
      Request URI = /jsonMessage
       Parameters = {}
          Headers = {Content-Type=[application/json;charset=UTF-8]Accept=[application/json;charset=UTF-8]}
             Body = {"name":"nimkoes"}
    Session Attrs = {}

Handler:
             Type = me.nimkoes.MyHelloController
           Method = public me.nimkoes.Person me.nimkoes.MyHelloController.jsonMessage(me.nimkoes.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=[application/json;charset=UTF-8]}
     Content type = application/json;charset=UTF-8
             Body = {"name":"My name is nimkoes"}
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

 

의도한 대로 동작하는 것을 확인할 수 있었다.

테스트 코드에 사용한 jsonPath 에 대한 내용은 다음 두 링크를 통해 확인할 수 있다.

1. github 설명 저장소 링크

2. JSONPath Online Evaluator 링크

 

 

다음으로 xml 형식의 데이터로 실습을 해보려 한다.

json 이전에는 xml 형식의 데이터도 많이 사용했던 것으로 기억 한다. 그렇다고 요즘 xml 형식을 사용하지 않는 것은 아니지만 json 이 나온 이후로 꽤 많이 줄어든 것으로 알고 있다.

 

아무튼,

Spring Boot 를 사용하고 있기 때문에 json 의 경우 dependency 를 추가 할 필요가 없었다.

하지만 xml 형식의 데이터를 사용하려면 pom.xml 에 dependency 를 추가해 줘야 한다.

그나마 다행인건 의존성만 추가하면 알아서 메시지 컨버터로 등록 해주기 때문에 직접 등록해주는 일은 하지 않아도 된다는 것이다.

 

        <!-- jaxb interface -->
        <dependency>
            <groupId>javax.xml.bind</groupId>
            <artifactId>jaxb-api</artifactId>
        </dependency>

        <!-- jaxb 구현체체 -->
       <dependency>
            <groupId>org.glassfish.jaxb</groupId>
            <artifactId>jaxb-runtime</artifactId>
        </dependency>

        <!--  Object Xml Mapping / marshalling, unmarshalling -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-oxm</artifactId>
            <version>${spring-framework.version}</version>
        </dependency>

 

의존성을 추가 했으니 Spring oxm 이 제공하는 marshaller 를 bean 으로 등록 한다.

현재 사용중인 WebMvcConfigurer 를 구현하고 있는 설정파일인 WebConfig 파일에 등록 했다.

 

 

package me.nimkoes;

import java.util.concurrent.TimeUnit;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.CacheControl;
import org.springframework.oxm.jaxb.Jaxb2Marshaller;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Bean
    public Jaxb2Marshaller jaxb2Marshaller() {
        Jaxb2Marshaller jaxb2Marshaller = new Jaxb2Marshaller();

        // jaxb 에서 사용하는 @XmlRootElement annotation 의 위치를 알려줘야 변환이 가능하다.
        jaxb2Marshaller.setPackagesToScan(Person.class.getPackage().getName());
        return jaxb2Marshaller;
    }

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

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/m/**")            // 어떤 패턴의 요청을 처리할지 정의
            .addResourceLocations("classpath:/m/", "file:/Users/nimkoes/files/", "/myStatic/")  // 리소스를 찾을 위치
            .setCacheControl(CacheControl.maxAge(10, TimeUnit.MINUTES));  // 리소스가 변경되지 않은 동안 캐싱을 유지할 시간
    }
}

 

Jaxb2Marshaller 를 등록한 내용 중에 @XmlRootElement annotation 에 대해서는 다음에 내용을 추가하겠다.

그리고 Person 클래스에 @XmlRootElement annotation 을 작성해 주었다.

 

 

package me.nimkoes;

import javax.xml.bind.annotation.XmlRootElement;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
@XmlRootElement
public class Person {

    String name;

}

 

이번에 xml 데이터를 주고 받는 테스트를 해보기 위해 별도의 handler method 를 사용하지 않고, 앞서 json 테스트를 해보기 위해 만들었던 /jsonMessage handler method 를 재사용 하려 한다.

똑같은 handler method 이지만 header 정보와 body 영역의 데이터 형식에 따라 똑같은 요청에 대해 어떻게 결과가 달라지는지 확인해 볼 수 있다.

 

package me.nimkoes;

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.*;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.StringWriter;
import javax.xml.transform.Result;
import javax.xml.transform.stream.StreamResult;
import org.hamcrest.Matchers;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.oxm.Marshaller;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;

@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class MyHelloControllerTest {

    // @WebMvcTest annotation 을 정의하면 MockMvc 를 주입받을 수 있다.
    @Autowired
    MockMvc mockMvc;

    @Autowired
    Marshaller marshaller;

    @Test
    public void xmlMessage() throws Exception {

        Person person = new Person();
        person.setName("nimkoes");

        // 객체를 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)  // context type 을 사용하여 request 데이터가 xml 형식임을 알려줌
                .accept(MediaType.APPLICATION_XML)       // accept 를 사용하여 response 데이터가 xml 형식이기를 바란다고 알려줌
                .content(xmlString))                     // request body 에 xmlString 입력
            .andDo(print())
            .andExpect(status().isOk())
            .andExpect(xpath("person/name").string("My name is nimkoes"));
    }
}

 

다른 테스트 코드는 삭제하고 xml 메시지 컨버터를 테스트 할 테스트 코드만 남겨 두었다.

Marshaller 타입의 객체를 주입 받으면, dependency 에 등록한 spring oxm 의 실제 구현체로 등록한 bean 인 Jaxb2Marshaller 객체가 들어온다.

그리고 이 구현체를 가지고 Person 타입 객체를 문자열로 만드는데 사용했다.

 

MockHttpServletRequest:
      HTTP Method = GET
      Request URI = /jsonMessage
       Parameters = {}
          Headers = {Content-Type=[application/xml;charset=UTF-8]Accept=[application/xml]}
             Body = <?xml version="1.0" encoding="UTF-8" standalone="yes"?><person><name>nimkoes</name></person>
    Session Attrs = {}

Handler:
             Type = me.nimkoes.MyHelloController
           Method = public me.nimkoes.Person me.nimkoes.MyHelloController.jsonMessage(me.nimkoes.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=[application/xml]}
     Content type = application/xml
             Body = <?xml version="1.0" encoding="UTF-8" standalone="yes"?><person><name>My name is nimkoes</name></person>
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

 

똑같은 handler method 를 사용 했지만 header 의 context type 과 accept 정보에 따라 사용 된 메시지 컨버터가 달라지고, 요청 응답 데이터도 원하는 형식으로 쉽게 바꿔 사용할 수 있었다.

 

테스트 코드에 사용한 xpath 에 대한 내용은 다음 두 링크를 통해 확인할 수 있다.

1. w3school 설명 링크

2. XPath Tester / Evaluator

 

 

xml 을 사용했을 때 json 때보다 조금 불편하긴 하지만 공통으로 사용 할 수 있도록 만들어두면 그리 불편하지 않을 것 같다.

 

 

 

 

728x90