티스토리 뷰
@JsonView와 @JsonFilter를 사용하여 Partial response 구현하기
개요
JSON HTTP API를 구현할 때, 페이징만 구현하는 경우를 쉽게 볼 수 있는데, 여기서 한 걸음 더 나아가 조금 더 나은 퍼포먼스를 위하여 클라이언트가 필요한 필드만 골라서 받을 수 있도록 구현하는 것이 필요할 수 있다. 예를 들어 아래와 같은 상황을 예를 들어 볼 수 있다.
- HTTP Reqeust
HTTP/1.1 GET /books/1?fields=isbn,title
- HTTP Response
{ "isbn": "978-3-16-148410-0", "title": "Book Title" }
책 데이터를 조회하는 JSON API가 있다고 했을 때, 헤당 API에서 제공하는 정보 중 일부 정보만을 클라이언트에서 필요한 경우에 fields 쿼리스트링에 필요한 키 값을 콤마 베이스로 나열하여 요청하는 예시이다. 클라이언트가 필요한 응답만을 제공하는 동시에 조금 더 나은 네트워크 레벨에서의 퍼포먼스를 가져갈 수 있다. 이런 부분적인 응답 값을 반환하는 것을 Partial Response
라고 부르며 REST와 관련된 문서에서 심심찮게 발견할 수 있는 내용이다.
구현하는 언어와 환경에 따라 이를 구현하는 방법에는 차이가 존재하는데, 일반적으로 Spring Framework, Boot 환경에서는 JSON 데이터에 대한 처리를 GSON 혹은 Jackson 라이브러리가 담당하게 되는데 그 중에서도 Jackson을 사용하는 환경에서 간단하게 partial response를 구현하는 예제를 작성해보려고 한다.
개발 환경
- Kotlin 1.3.31
- Spring Boot 2.1.5 RELEASE
- WEB
1. @JsonView를 사용한 예제
@JsonView 어노테이션을 활용하면 계층적인 부분 렌더링이 가능하다. @JsonView에 대한 계층 정의 예와 유저에 대한 예제 엔티티는 아래와 같다.
interface Views { interface List interface Get: List } class User( @JsonView(Views.List::class) val id: UUID = UUID.randomUUID(), @JsonView(Views.List::class) val email: String, @JsonView(Views.List::class) val name: String, @JsonView(Views.Get::class) val createdAt: LocalDateTime = LocalDateTime.now(), @JsonView(Views.Get::class) val updatedAt: LocalDateTime = LocalDateTime.now() )
위와 같이 선언할 경우, @JsonView(Dto.Views.List::class)
로 데이터를 처리하게 되면 모든 데이터를 유저 엔티티에서 id
, email
, name
만을 Jackson 라이브러리가 serialize하게 된다. 반대로, @JsonView(Dto.Views.Get::class)
으로 지정해놓게 되면 유저 엔티티의 모든 데이터가 serialize된다.
Spring Mvc에서는 요청 컨트롤러 매핑 메서드에 @JsonView를 명시해주면 해당 뷰로 Serailize 할 수 있도록 지원하고 있다.
@RestController @RequestMapping class BookController { private companion object { val user = Dto.User( name = "Park", email = "park@gmail.com" ) } @GetMapping("/get") @JsonView(Dto.Views.Get::class) fun jsonViewGet() = user @GetMapping("/list") @JsonView(Dto.Views.List::class) fun jsonViewList() = listOf(user) }
@JsonView를 통하여 제대로 결과가 반환되는지 테스트 코드를 간단하게 작성해보면 아래와 같다. 테스트코드는 스프링 부트의 통합테스트 환경을 그대로 사용했으며, jsonPath 라이브러리를 사용하여 해당 키 값이 제대로 존재하는지 그리고 타입이 정확한지 유무까지 테스트를 해봤다.
테스트코드
@RunWith(SpringRunner::class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @AutoConfigureMockMvc class MvcJacksonviewApplicationTests { @Autowired private lateinit var mockMvc: MockMvc @Test fun `JsonView_Get_Test`() { mockMvc.perform( get("/get") ) .andDo(print()) .andExpect(status().isOk) .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8)) .andExpect(jsonPath("id").exists()) .andExpect(jsonPath("id").isString) .andExpect(jsonPath("email").exists()) .andExpect(jsonPath("email").isString) .andExpect(jsonPath("name").exists()) .andExpect(jsonPath("name").isString) .andExpect(jsonPath("createdAt").exists()) .andExpect(jsonPath("createdAt").isString) .andExpect(jsonPath("updatedAt").exists()) .andExpect(jsonPath("updatedAt").isString) } @Test fun `JsonView_List_Test`() { mockMvc.perform( get("/list") ) .andDo(print()) .andExpect(status().isOk) .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8)) .andExpect(jsonPath("[0].id").exists()) .andExpect(jsonPath("[0].id").isString) .andExpect(jsonPath("[0].email").exists()) .andExpect(jsonPath("[0].email").isString) .andExpect(jsonPath("[0].name").exists()) .andExpect(jsonPath("[0].name").isString) .andExpect(jsonPath("[0].createdAt").doesNotExist()) .andExpect(jsonPath("[0].updatedAt").doesNotExist()) } @Test fun `JsonFilter_single`() { mockMvc.perform( get("/json-filter") .param("fields", "isbn") ) .andDo(print()) .andExpect(status().isOk) .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8)) .andExpect(jsonPath("isbn").exists()) .andExpect(jsonPath("isbn").isString) .andExpect(jsonPath("title").doesNotExist()) .andExpect(jsonPath("content").doesNotExist()) .andExpect(jsonPath("createdAt").doesNotExist()) .andExpect(jsonPath("updatedAt").doesNotExist()) } }
2. @JsonFilter를 사용하는 예제
위에서 본 @JsonView의 경우, 딱 지정해놓은 계층 구조 혹은 뷰가 아닐 경우 필드에 대한 선택 자체가 불가능하다. 결국 서버 어플리케이션에서 정해놓은 구조로만 부분 뷰를 응답 받을 수 있는 구조인데, 실제 요청에 부합하는 응답 값만 전달하기 위해서는 Jackson의 @JsonFilter 어노테이션과 Spring MVC의 MappingJacksonValue를 활용하면 해당 구현이 가능하다.
@JsonFilter("bookFilter") class Book ( val isbn: String, val title: String, val content: String, val createdAt: LocalDateTime = LocalDateTime.now(), val updatedAt: LocalDateTime = LocalDateTime.now() ) class GetReq { var fields: List<String> = emptyList() }
@RestController @RequestMapping class BookController { private companion object { val book: Dto.Book = Dto.Book( isbn = UUID.randomUUID().toString(), title = "Title", content = "Content" ) } @GetMapping("/json-filter") fun jsonFilter(reqDto: GetReq) = MappingJacksonValue(book).apply { filters = SimpleFilterProvider().also { it.addFilter("bookFilter", if (reqDto.fields.isNotEmpty()) SimpleBeanPropertyFilter.filterOutAllExcept(reqDto.fields.toSet()) else SimpleBeanPropertyFilter.serializeAll() ) } } }
테스트코드
@RunWith(SpringRunner::class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @AutoConfigureMockMvc class MvcJacksonviewApplicationTests { @Autowired private lateinit var mockMvc: MockMvc @Test fun `JsonFilter_single`() { mockMvc.perform( get("/json-filter") .param("fields", "isbn") ) .andDo(print()) .andExpect(status().isOk) .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8)) .andExpect(jsonPath("isbn").exists()) .andExpect(jsonPath("isbn").isString) .andExpect(jsonPath("title").doesNotExist()) .andExpect(jsonPath("content").doesNotExist()) .andExpect(jsonPath("createdAt").doesNotExist()) .andExpect(jsonPath("updatedAt").doesNotExist()) } @Test fun `JsonFilter_comma_separator`() { mockMvc.perform( get("/json-filter") .param("fields", "isbn,title") ) .andDo(print()) .andExpect(status().isOk) .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8)) .andExpect(jsonPath("isbn").exists()) .andExpect(jsonPath("isbn").isString) .andExpect(jsonPath("title").exists()) .andExpect(jsonPath("title").isString) .andExpect(jsonPath("content").doesNotExist()) .andExpect(jsonPath("createdAt").doesNotExist()) .andExpect(jsonPath("updatedAt").doesNotExist()) } }
간단하게 @JsonView와 @JsonFilter를 사용하여 partial response에 대한 구현을 해볼 수 있었는데, 여기서 조금 더 나아가 복잡한 조건이나 구조에서의 구현이 필요할 경우 Squiggly Filter와 같은 구현체를 사용하는 것도 좋은 선택이 될 수 있을거 같다.
예제는 링크를 통하여 확인 가능합니다.
참고
'Programing > Spring' 카테고리의 다른 글
MVC와 WebFlux에서의 @ReqeustParam (0) | 2021.07.27 |
---|---|
MapStruct 맛보기 (1) | 2019.05.26 |
Spring-Boot Logging And ELK Logstash Format (0) | 2019.05.01 |
스프링 부트 배치 #2 - ItemReader (1) | 2019.01.04 |
스프링 부트 배치 #1 - 개요/주요개념 (0) | 2019.01.01 |
- Total
- Today
- Yesterday
- maven
- tomcat
- Prototype
- Kotlin
- Squelize.js
- Sublime Text 2
- Spring Boot
- pm2
- http method
- WebFlux
- cluster
- Sublime Text 3
- RestTemplate
- implicit prototype chain
- Spring MVC
- jade
- EJS
- Package Control
- Handlebars
- Express.js
- package.js
- SideBarEnhancements
- Til
- ecma
- springboot
- 스프링
- node.js
- HttpClient
- HTTP
- Spring
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |