Body로 전달된 JSON/XML 데이터를 처리하는 HTTP Message Converter

48504 단어 SpringSpring


이 글의 목적?

바디로 전달된 JSON/XML 데이터를 어떻게 객체로 변환할 수 있을까?

HTTP Message Converter?

요청 본문에서 메시지를 읽거나 응답 본문에 메시지를 작성할 때 사용한다. 즉, @RequestBody, @ResponseBody 어노테이션을 사용할 때 요청/응답을 처리해주는 변환기다.

의존성에 따라 조건적으로 등록이 되는데, 이 설정은 WebMvcConfigurationSupport 클래스의 코드에서 확인할 수 있다.

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

        messageConverters.add(new AllEncompassingFormHttpMessageConverter());
        if (romePresent) {
            messageConverters.add(new AtomFeedHttpMessageConverter());
            messageConverters.add(new RssChannelHttpMessageConverter());
        }

        Jackson2ObjectMapperBuilder builder;
        if (!shouldIgnoreXml) {
            if (jackson2XmlPresent) {
                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) {
            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) {
            builder = Jackson2ObjectMapperBuilder.smile();
            if (this.applicationContext != null) {
                builder.applicationContext(this.applicationContext);
            }

            messageConverters.add(new MappingJackson2SmileHttpMessageConverter(builder.build()));
        }

        if (jackson2CborPresent) {
            builder = Jackson2ObjectMapperBuilder.cbor();
            if (this.applicationContext != null) {
                builder.applicationContext(this.applicationContext);
            }

            messageConverters.add(new MappingJackson2CborHttpMessageConverter(builder.build()));
        }

    }
    
	// (생략...)
    
}

HTTP Message Converter에서 알아야하는 점은, 스프링 부트의 기능이 아닌 스프링 프레임워크 자체의 기능이라는 점이다. 단지 스프링 부트에는 기본적으로 JacksonJSON2가 의존성이 들어가있다는 점 밖에 없다. (스프링 부트를 사용하지 않을 경우에는 직접 JSON 라이브러리를 의존성에 추가해주어야 한다.)

기본적으로 많은 HTTP Message Converter를 제공하고 있어, 추가하는 일은 거의 없을 것 같다. 😆 하지만 새로운 Converter를 추가하고자 할 때는 WebConfigurer 인터페이스를 사용하여 등록하거나 간단하게 의존성 추가만으로도 등록할 수 있다. (강의에서 백기선님은 의존성 추가로 등록하는 방법을 추천하셨다.)

WebConfigurer 인터페이스에 등록할 때는 두 개의 메서드로 추가가 가능하다.

configureMessageConverters() 메서드는 기본적으로 등록되어있는 Converter를 모두 무시하기 때문에 extendMessageConverters() 메서드를 이용하여 기본적으로 등록되어있는 Converter에서 추가하는 형태로 등록하는 편이 안전해보인다.

@Configuration
public class WebConfig implements WebMvcConfigurer {

//    @Override
//    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
//        // 추가할 경우에는 기본적으로 제공하는 메시지 컨버터를 사용할 수 없음
//    }


//    @Override
//    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
//        // 기본적으로 제공하는 메시지 컨버터에 추가
//    }
}
@RestController
public class SimpleController {
    @GetMapping("/message")
    public String messageString(@RequestBody String body) {
        return "message " + body;
    }
}
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class SimpleControllerTest {

    @Autowired
    MockMvc mockMvc;

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

JSON

스프링 부트에서는 기본적으로 JSON 의존성이 존재하기에 별도로 의존성을 추가하지 않고 확인할 수 있다.

@RestController
public class SimpleController {
    @GetMapping("/jsonmessage")
    public Person jsonmessage(@RequestBody Person person) {
        return person;
    }
}
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class SimpleControllerTest {

    @Autowired
    MockMvc mockMvc;

    @Autowired
    ObjectMapper objectMapper;

    @Test
    public void jsonmessage() throws Exception {
        Person person = new Person();
        person.setId(10L);
        person.setName("kevin");
		
        // 객체를 JSON으로 변환
        String jsonString = objectMapper.writeValueAsString(person);

        this.mockMvc.perform(get("/jsonmessage")
                        .contentType(MediaType.APPLICATION_JSON) // JSON 요청을 보냄
                        .accept(MediaType.APPLICATION_JSON) // JSON 응답을 기대
                        .content(jsonString))
                .andDo(print())
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.id").value(10L))
                .andExpect(jsonPath("$.name").value("kevin"));
    }
}

테스트 코드에서 사용한 JSON Path 문법은 하단의 링크를 참고하여 작성해주면 된다.

XML

JSON과 다르게 스프링 부트는 기본적으로 XML 의존성을 추가해주지 않기 때문에 별도로 추가를 해주어야 한다.

OXM(Object-XML Mapper) 라이브러리 중에서 스프링이 지원하는 의존성에는 JacksonXML과 JAXB가 있으니 JAXB를 추가해준다.

<dependency>
	<groupId>javax.xml.bind</groupId>
	<artifactId>jaxb-api</artifactId>
</dependency>
<dependency>
	<groupId>org.glassfish.jaxb</groupId>
	<artifactId>jaxb-runtime</artifactId>
</dependency>
<!-- xml을 객체로 변환(Marshalling) / 객체를 xml로 변환(UnMarshalling) -->
<dependency>
	<groupId>org.springframework</groupId>
	<artifactId>spring-oxm</artifactId>
	<version>${spring-framework.version}</version>
</dependency>

그 다음으로는 빈으로 Marshaller를 등록해주고, XML로 변환시킬 클래스에 @XmlRootElement 어노테이션을 붙여준다.

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Bean
    public Jaxb2Marshaller jaxb2Marshaller() {
        Jaxb2Marshaller jaxb2Marshaller = new Jaxb2Marshaller();
        jaxb2Marshaller.setPackagesToScan(Person.class.getPackageName()); // @XmlRootElement 어노테이션 스캔
        return jaxb2Marshaller;
    }
}
@XmlRootElement
@Getter
@Setter
@Entity
public class Person {

    @Id @GeneratedValue
    private Long id;

    private String name;
}
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class SimpleControllerTest {

    @Autowired
    MockMvc mockMvc;

    @Autowired
    Marshaller marshaller; // WebConfig에 등록한 빈을 주입 받음

    @Test
    public void xmlmessage() throws Exception {
        Person person = new Person();
        person.setId(10L);
        person.setName("kevin");
		
        // 객체를 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) // XML 요청을 보냄
                        .accept(MediaType.APPLICATION_XML) // XML 응답을 기대
                        .content(xmlString))
                .andDo(print())
                .andExpect(status().isOk())
                .andExpect(xpath("person/id").string("10"))
                .andExpect(xpath("person/name").string("kevin"));
    }
}

테스트 코드에서 사용한 XML Path 문법은 하단의 링크를 참고하여 작성해주면 된다.

이 글의 레퍼런스

  • 백기선님의 스프링 웹 MVC

좋은 웹페이지 즐겨찾기