-
[Spring Boot] REST API 만들기(10) - XML 요청/응답 구현 및 테스트Spring Boot/2.4.x - REST API 만들기 2020. 5. 19. 17:50반응형
스프링에서 제공하는 MarshallingHttpMessageConverter를 사용하여 XML 형식으로 데이터를 교환할 수 있도록 구현하겠습니다. MarshallingHttpMessageConverter는 스프링 OXM을 이용해서 객체와 XML 간의 상호 변환 작업을 합니다.
Spring OXM
스프링 OXM(Object Xml Mapping)은 XML 문서를 객체로 변환하거나, 객체를 XML 문서로 변환하는 기능을 제공합니다. 객체를 XML로 변환하는 과정은 XML Marshalling이라고 하며, XML을 객체로 변환하는 과정을 XML UnMarshalling이라고 합니다.
Marshaller와 UnMarshaller 인터페이스는 구분되어 있지만, 실제 스프링에서 제공하는 구현체들은 하나의 클래스에서 두 개의 인터페이스를 모두 구현하고 있습니다. 그래서 구현 클래스 하나만 등록하면 Marshaller와 UnMarshaller 모두 사용할 수 있습니다.
1. pom.xml 의존성 추가
pom.xml에 spring-oxm에 대한 의존성을 추가하세요.
pom.xml
더보기12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.4.8</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.api</groupId><artifactId>board</artifactId><version>0.0.1-SNAPSHOT</version><name>board</name><description>Demo project for Spring Boot</description><properties><java.version>1.8</java.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-configuration-processor --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-configuration-processor</artifactId><optional>true</optional></dependency><!-- https://mvnrepository.com/artifact/org.mybatis.spring.boot/mybatis-spring-boot-starter --><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.1.4</version></dependency><!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java --><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId></dependency><!-- https://mvnrepository.com/artifact/org.bgee.log4jdbc-log4j2/log4jdbc-log4j2-jdbc4.1 --><dependency><groupId>org.bgee.log4jdbc-log4j2</groupId><artifactId>log4jdbc-log4j2-jdbc4.1</artifactId><version>1.16</version></dependency><!-- JUnit4 사용하기 위해서 Vintage Engine 모듈을 제외 --><dependency><groupId>org.junit.vintage</groupId><artifactId>junit-vintage-engine</artifactId><scope>test</scope><exclusions><exclusion><groupId>org.hamcrest</groupId><artifactId>hamcrest-core</artifactId></exclusion></exclusions></dependency><!-- https://mvnrepository.com/artifact/org.springframework/spring-oxm --><dependency><groupId>org.springframework</groupId><artifactId>spring-oxm</artifactId></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>cs 2. WebMvcConfig.java 수정
클라이언트의 요청이 오면 등록된 MessageConverter를 통해서 application/json, application/xml 또는 text/xml 형태 등 요청한 형태로 응답할 수 있습니다. 이때 MessageConverter에 대표적인 것들로 MappingJackson2HttpMessageConverter(application/json 형태로 응답), MarshallingHttpMessageConverter(application/xml 또는 text/xml 형태로 응답)가 있습니다. 이 외에도 다양한 종류의 응답 형태로 변환해 주는 MessageConverter가 있으며, 원하는 형태의 응답을 할 때 필요한 MessageConverter 설정을 추가하면 됩니다.
application/json, application/xml 또는 text/xml 형태의 응답을 위해 MappingJackson2HttpMessageConverter와 MarshallingHttpMessageConverter를 추가하세요.
Jaxb2Marshaller는 XML을 객체로 변환하거나 객체를 XML로 변환해주는 라이브러리로 변환이 필요한 객체를 찾는 경우, classesToBeBound 또는 packagesToScan를 사용합니다. classToBeBound는 class를 주입해야 하고 packageToScan는 package를 선언해야 하는데, class를 주입하는 경우 객체가 생성될 때마다 설정을 변경하는 불편함이 있어서 package를 선언하여 변환이 필요한 객체를 찾도록 선언하세요.
WebMvcConfig.java
더보기123456789101112131415161718192021222324252627282930313233343536373839404142434445464748package com.api.board.config;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.ComponentScan;import org.springframework.context.annotation.ComponentScan.Filter;import org.springframework.context.annotation.Configuration;import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;import org.springframework.http.converter.xml.MarshallingHttpMessageConverter;import org.springframework.oxm.jaxb.Jaxb2Marshaller;import org.springframework.stereotype.Controller;import org.springframework.web.servlet.config.annotation.InterceptorRegistry;import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;import com.api.board.interceptor.BoardInterceptor;@ComponentScan(basePackages = {"com.api.board.controller"}, useDefaultFilters = false, includeFilters = {@Filter(Controller.class)})@Configurationpublic class WebMvcConfig implements WebMvcConfigurer {@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new BoardInterceptor()).addPathPatterns("/**").excludePathPatterns("/sample/**");}@Beanpublic MappingJackson2HttpMessageConverter mappingJacksonHttpMessageConverter() {MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();converter.setPrettyPrint(true);return converter;}@Beanpublic MarshallingHttpMessageConverter marshallingHttpMessageConverter() {MarshallingHttpMessageConverter converter = new MarshallingHttpMessageConverter();converter.setMarshaller(jaxb2Marshaller());converter.setUnmarshaller(jaxb2Marshaller());return converter;}@Beanpublic Jaxb2Marshaller jaxb2Marshaller() {Jaxb2Marshaller marshaller = new Jaxb2Marshaller();marshaller.setPackagesToScan(new String[] { "com.api.board.domain" });return marshaller;}}cs 3. Bords.java 추가
@XmlRootElement 어노테이션과 @XmlElement 어노테이션은 자바 객체에서 XML 또는 XML에서 자바 객체로 변환시킬 때 필요한 정보를 설정할 수 있도록 합니다.
Boards.java
더보기12345678910111213141516171819202122232425262728package com.api.board.domain;import java.util.List;import javax.xml.bind.annotation.XmlElement;import javax.xml.bind.annotation.XmlRootElement;@XmlRootElement(name = "boards")public class Boards {private List<Board> boards;public Boards() {}public Boards(List<Board> boards) {setBoards(boards);}@XmlElement(name = "board")public List<Board> getBoards() {return boards;}public void setBoards(List<Board> boards) {this.boards = boards;}}cs 4. Board.java 수정
Board.java
더보기123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127package com.api.board.domain;import javax.xml.bind.annotation.XmlRootElement;import javax.xml.bind.annotation.XmlType;@XmlRootElement(name = "board")@XmlType(propOrder = {"board_seq", "board_re_ref", "board_re_lev", "board_re_seq", "board_writer", "board_subject", "board_content", "board_hits", "del_yn", "ins_user_id", "ins_date", "upd_user_id", "upd_date"})public class Board {int board_seq;int board_re_ref;int board_re_lev;int board_re_seq;String board_writer;String board_subject;String board_content;int board_hits;String del_yn;String ins_user_id;String ins_date;String upd_user_id;String upd_date;public int getBoard_seq() {return board_seq;}public void setBoard_seq(int board_seq) {this.board_seq = board_seq;}public int getBoard_re_ref() {return board_re_ref;}public void setBoard_re_ref(int board_re_ref) {this.board_re_ref = board_re_ref;}public int getBoard_re_lev() {return board_re_lev;}public void setBoard_re_lev(int board_re_lev) {this.board_re_lev = board_re_lev;}public int getBoard_re_seq() {return board_re_seq;}public void setBoard_re_seq(int board_re_seq) {this.board_re_seq = board_re_seq;}public String getBoard_writer() {return board_writer;}public void setBoard_writer(String board_writer) {this.board_writer = board_writer;}public String getBoard_subject() {return board_subject;}public void setBoard_subject(String board_subject) {this.board_subject = board_subject;}public String getBoard_content() {return board_content;}public void setBoard_content(String board_content) {this.board_content = board_content;}public int getBoard_hits() {return board_hits;}public void setBoard_hits(int board_hits) {this.board_hits = board_hits;}public String getDel_yn() {return del_yn;}public void setDel_yn(String del_yn) {this.del_yn = del_yn;}public String getIns_user_id() {return ins_user_id;}public void setIns_user_id(String ins_user_id) {this.ins_user_id = ins_user_id;}public String getIns_date() {return ins_date;}public void setIns_date(String ins_date) {this.ins_date = ins_date;}public String getUpd_user_id() {return upd_user_id;}public void setUpd_user_id(String upd_user_id) {this.upd_user_id = upd_user_id;}public String getUpd_date() {return upd_date;}public void setUpd_date(String upd_date) {this.upd_date = upd_date;}}cs 5. BoardController.java 수정
Spring OXM에서 사용할 XML Marshaller와 UnMarshaller는 JAXB를 사용하겠습니다. JAXB에서는 List<E> 형태의 객체를 XML로 변환할 수 없고, JSON과는 다르게 XML은 Root Element가 필요합니다. 그래서 Boards라는 클래스를 생성하여 게시글 목록 조회 시 리턴 타입으로 사용해야 합니다.
BoardController.java
더보기1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980package com.api.board.controller;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.http.HttpStatus;import org.springframework.web.bind.annotation.DeleteMapping;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.PathVariable;import org.springframework.web.bind.annotation.PostMapping;import org.springframework.web.bind.annotation.PutMapping;import org.springframework.web.bind.annotation.RequestBody;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.ResponseStatus;import org.springframework.web.bind.annotation.RestController;import com.api.board.domain.Board;import com.api.board.domain.Boards;import com.api.board.service.BoardService;@RequestMapping("/board")@RestControllerpublic class BoardController {@Autowiredprivate BoardService boardService;/** 게시글 목록 조회 */@GetMappingpublic Boards getBoardList() throws Exception {Boards boards = new Boards();boards.setBoards(boardService.getBoardList());return boards;}/** 게시글 상세 조회 */@GetMapping("/{board_seq}")public Board getBoardDetail(@PathVariable("board_seq") int board_seq) throws Exception {return boardService.getBoardDetail(board_seq);}/** 게시글 등록 */@ResponseStatus(value = HttpStatus.CREATED)@PostMappingpublic Board insertBoard(@RequestBody Board board) throws Exception {boardService.insertBoard(board);int boardSeq = board.getBoard_seq();Board boardDetail = boardService.getBoardDetail(boardSeq);return boardDetail;}/** 게시글 수정 */@ResponseStatus(value = HttpStatus.OK)@PutMapping("/{board_seq}")public Board updateBoard(@PathVariable("board_seq") int board_seq, @RequestBody Board board) throws Exception {boardService.updateBoard(board);Board boardDetail = boardService.getBoardDetail(board_seq);return boardDetail;}/** 게시글 삭제 */@ResponseStatus(value = HttpStatus.OK)@DeleteMapping("/{board_seq}")public Board deleteBoard(@PathVariable("board_seq") int board_seq) throws Exception {boardService.deleteBoard(board_seq);Board deleteBoard = new Board();deleteBoard.setBoard_seq(board_seq);return deleteBoard;}}cs 6. Controller 테스트
JSON 형식으로 요청/응답 값을 테스트하는 BoardControllerTest 클래스를 BoardControllerAsJsonTest 클래스로 명칭만 변환하고, XML 형식으로 요청/응답 값을 테스트하는 BoardControllerAsXmlTest. 클래스를 추가하세요.
BoardControllerAsJsonTest.java
더보기123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168package com.api.board.controller;import static org.junit.Assert.assertEquals;import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;import java.io.IOException;import org.junit.Before;import org.junit.Test;import org.junit.runner.RunWith;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.json.JsonParseException;import org.springframework.boot.test.context.SpringBootTest;import org.springframework.http.MediaType;import org.springframework.test.annotation.Rollback;import org.springframework.test.context.junit4.SpringRunner;import org.springframework.test.context.web.WebAppConfiguration;import org.springframework.test.web.servlet.MockMvc;import org.springframework.test.web.servlet.MvcResult;import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;import org.springframework.test.web.servlet.setup.MockMvcBuilders;import org.springframework.transaction.annotation.Transactional;import org.springframework.web.context.WebApplicationContext;import org.springframework.web.filter.CharacterEncodingFilter;import com.api.board.domain.Board;import com.api.board.service.BoardServiceTest;import com.fasterxml.jackson.core.JsonProcessingException;import com.fasterxml.jackson.databind.JsonMappingException;import com.fasterxml.jackson.databind.ObjectMapper;@WebAppConfiguration@Transactional@RunWith(SpringRunner.class)@SpringBootTestpublic class BoardControllerAsJsonTest {Logger logger = LoggerFactory.getLogger(BoardServiceTest.class);private MockMvc mockMvc;@AutowiredWebApplicationContext webApplicationContext;public String mapToJson(Object obj) throws JsonProcessingException {ObjectMapper objectMapper = new ObjectMapper();return objectMapper.writeValueAsString(obj);}public <T> T mapFromJson(String json, Class<T> clazz) throws JsonParseException, JsonMappingException, IOException {ObjectMapper objectMapper = new ObjectMapper();return objectMapper.readValue(json, clazz);}@Beforepublic void setUp() {mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).addFilters(new CharacterEncodingFilter("UTF-8", true)).alwaysDo(print()).build();}int boardSeq = 0;/** 게시글 목록 조회 시 응답 값이 200이면 테스트 통과 */@Testpublic void testGetBoardList() {try {MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.get("/board").accept(MediaType.APPLICATION_JSON_VALUE)).andReturn();assertEquals(200, mvcResult.getResponse().getStatus());} catch (Exception e) {e.printStackTrace();}}/** 게시글 상세 조회 시 응답 값이 200이면 테스트 통과 */@Testpublic void testGetBoardDetail() {try {if (boardSeq != 0) {MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.get("/board/" + boardSeq).accept(MediaType.APPLICATION_JSON_VALUE)).andReturn();assertEquals(200, mvcResult.getResponse().getStatus());}} catch (Exception e) {e.printStackTrace();}}/** 게시글 등록 시 응답 값이 201이면 테스트 통과 */@Rollback(true)@Testpublic void testInsertBoard() throws Exception {Board insertBoard = new Board();insertBoard.setBoard_writer("게시글 작성자 등록");insertBoard.setBoard_subject("게시글 제목 등록");insertBoard.setBoard_content("게시글 내용 등록");MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.post("/board").contentType(MediaType.APPLICATION_JSON_VALUE).content(this.mapToJson(insertBoard))).andReturn();assertEquals(201, mvcResult.getResponse().getStatus());}/** 게시글 수정 시 응답 값이 200이면 테스트 통과 */@Rollback(true)@Testpublic void testUpdateBoard() {try {if (boardSeq != 0) {Board updateBoard = new Board();updateBoard.setBoard_seq(boardSeq);updateBoard.setBoard_subject("게시글 제목 수정");updateBoard.setBoard_content("게시글 내용 수정");MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.put("/board/" + boardSeq).contentType(MediaType.APPLICATION_JSON_VALUE).content(this.mapToJson(updateBoard))).andReturn();assertEquals(200, mvcResult.getResponse().getStatus());}} catch (Exception e) {e.printStackTrace();}}/** 게시글 삭제 시 응답 값이 200이면 테스트 통과 */@Rollback(true)@Testpublic void testDeleteBoard() {try {if (boardSeq != 0) {MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.delete("/board/" + boardSeq)).andReturn();assertEquals(200, mvcResult.getResponse().getStatus());}} catch (Exception e) {e.printStackTrace();}}}cs BoardControllerAsXmlTest.java
더보기123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165package com.api.board.controller;import static org.junit.Assert.assertEquals;import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;import java.io.StringWriter;import javax.xml.transform.stream.StreamResult;import org.junit.Before;import org.junit.Test;import org.junit.runner.RunWith;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.context.SpringBootTest;import org.springframework.http.MediaType;import org.springframework.oxm.jaxb.Jaxb2Marshaller;import org.springframework.test.annotation.Rollback;import org.springframework.test.context.junit4.SpringRunner;import org.springframework.test.context.web.WebAppConfiguration;import org.springframework.test.web.servlet.MockMvc;import org.springframework.test.web.servlet.MvcResult;import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;import org.springframework.test.web.servlet.setup.MockMvcBuilders;import org.springframework.web.context.WebApplicationContext;import org.springframework.web.filter.CharacterEncodingFilter;import com.api.board.domain.Board;import com.api.board.service.BoardServiceTest;@WebAppConfiguration@RunWith(SpringRunner.class)@SpringBootTestpublic class BoardControllerAsXmlTest {Logger logger = LoggerFactory.getLogger(BoardServiceTest.class);private MockMvc mockMvc;@AutowiredWebApplicationContext webApplicationContext;@AutowiredJaxb2Marshaller jaxb2Marshaller;public String mapToXml(Object obj) throws Exception {StringWriter writer = new StringWriter();jaxb2Marshaller.marshal(obj, new StreamResult(writer));String content = writer.toString();return content;}@Beforepublic void setUp() {mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).addFilters(new CharacterEncodingFilter("UTF-8", true)).alwaysDo(print()).build();}int boardSeq = 0;/** 게시글 목록 조회 시 응답 값이 200이면 테스트 통과 */@Testpublic void testGetBoardList() {try {MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.get("/board").accept(MediaType.APPLICATION_XML)).andReturn();assertEquals(200, mvcResult.getResponse().getStatus());} catch (Exception e) {e.printStackTrace();}}/** 게시글 상세 조회 시 응답 값이 200이면 테스트 통과 */@Testpublic void testGetBoardDetail() {try {if (boardSeq != 0) {MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.get("/board/" + boardSeq).accept(MediaType.APPLICATION_XML)).andReturn();assertEquals(200, mvcResult.getResponse().getStatus());}} catch (Exception e) {e.printStackTrace();}}/** 게시글 등록 시 응답 값이 201이면 테스트 통과 */@Rollback(true)@Testpublic void testInsertBoard() throws Exception {Board insertBoard = new Board();insertBoard.setBoard_writer("게시글 작성자 등록");insertBoard.setBoard_subject("게시글 제목 등록");insertBoard.setBoard_content("게시글 내용 등록");MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.post("/board").contentType(MediaType.APPLICATION_XML).content(this.mapToXml(insertBoard))).andReturn();assertEquals(201, mvcResult.getResponse().getStatus());}/** 게시글 수정 시 응답 값이 200이면 테스트 통과 */@Rollback(true)@Testpublic void testUpdateBoard() {try {if (boardSeq != 0) {Board updateBoard = new Board();updateBoard.setBoard_seq(boardSeq);updateBoard.setBoard_subject("게시글 제목 수정");updateBoard.setBoard_content("게시글 내용 수정");MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.put("/board/" + boardSeq).contentType(MediaType.APPLICATION_XML).content(this.mapToXml(updateBoard))).andReturn();assertEquals(200, mvcResult.getResponse().getStatus());}} catch (Exception e) {e.printStackTrace();}}/** 게시글 삭제 시 응답 값이 200이면 테스트 통과 */@Rollback(true)@Testpublic void testDeleteBoard() {try {if (boardSeq != 0) {MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.delete("/board/" + boardSeq)).andReturn();assertEquals(200, mvcResult.getResponse().getStatus());}} catch (Exception e) {e.printStackTrace();}}}cs 소스 코드는 Github Repository - https://github.com/tychejin1218/api-board_v1 (branch : section10) 를 참조하세요.
JAXB - Marshalling, UnMarshalling 사용법 - https://tychejin.tistory.com/135
Github에서 프로젝트 가져오기 - https://tychejin.tistory.com/33
반응형'Spring Boot > 2.4.x - REST API 만들기' 카테고리의 다른 글
[Spring Boot] REST API 만들기(12) - Content Negotiation 설정 (0) 2020.05.20 [Spring Boot] REST API 만들기(11) - JSON Root Element 추가 (0) 2020.05.20 [Spring Boot] REST API 만들기(9) - Controller 구현 및 테스트(Junit4) (0) 2020.05.10 [Spring Boot] REST API 만들기(8) - Service 구현 및 테스트(Junit4) (0) 2020.05.09 [Spring Boot] REST API 만들기(7) - Transaction 적용 (0) 2020.05.09