인프런 커뮤니티 질문&답변

cnw529님의 프로필 이미지
cnw529

작성한 질문수

[개정판 2023-11-27] Spring Boot 3.x 를 이용한 RESTful Web Services 개발

Swagger Documentation 구현 - Spring Boot 2.7 사용 ②

Swagger와 Jackson Filter 사용 시 Swagger-ui의 example value

작성

·

1.4K

0

Jackson Filter를 적용한 코드에서는 Swagger-UI에서 example value 값이 "filters"와 "value"로만 보입니다. 필터가 적용된 example value 값을 온전히 표시하기 위해서는 어떻게 해야하는지 궁금합니다.

답변 2

0

cnw529님의 프로필 이미지
cnw529
질문자

저 또한 Postman을 이용하여 'GET /users/1'에 대한 Request로 다음과 같은 Response를 정상적으로 받고 있습니다.

User controller의 'GET /users/{id}' 요청은 JsonFilter를 적용하여 MappingJacksonValue를 반환하도록 코드를 작성하였으며, 이로인해 Swagger-ui에서 User controller의 'GET /users/{id}'에 대한 Example value에서 다음과 같이 User Domain 클래스의 필드 구조를 제대로 보여주지 않았고 이를 문제라고 생각하였습니다.

필터를 적용하지 않고, EntityModel<User>를 반환하는 경우는 다음과 같이 example value가 올바르게 표시됩니다. 저는 MappingJacksonValue와 EntitiyModel<User>, 즉, 반환값의 차이에서 이러한 문제가 발생했다고 생각하고 있습니다.

swagger-ui Models의 User에서는 User Domain Class에 대한 정보를 올바르게 표시하고 있습니다.

다음은 작성한 User.java, UserController.java, pom.xml의 코드 입니다.

User.java

package com.example.restfulwebservice.user;

import com.fasterxml.jackson.annotation.JsonFilter;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.validation.constraints.Past;
import javax.validation.constraints.Size;
import java.util.Date;

@Data
@AllArgsConstructor
@NoArgsConstructor
//@JsonIgnoreProperties(value = {"password", "ssn"})
@JsonFilter("UserInfo")
@ApiModel(description = "사용자 상세 정보를 위한 도메인 객체")
public class User {
private Integer id;

@Size(min=2, message = "Name 2글자 이상 입력해 주세요.")
@ApiModelProperty(notes = "사용자 이름을 입력해 주세요.")
private String name;
// 미래 데이터 사용 X
@Past
@ApiModelProperty(notes = "사용자 등록일을 입력해 주세요.")
private Date joinDate;

@ApiModelProperty(notes = "사용자 비빌번호를 입력해 주세요.")
private String password;
@ApiModelProperty(notes = "사용자 주민등록번호를 입력해 주세요.")
private String ssn;
}

UserController.java

package com.example.restfulwebservice.user;

import com.fasterxml.jackson.databind.ser.FilterProvider;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.hateoas.CollectionModel;
import org.springframework.hateoas.EntityModel;
import org.springframework.hateoas.server.mvc.WebMvcLinkBuilder;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.json.MappingJacksonValue;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;

import javax.validation.Valid;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;

import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn;

@RestController
public class UserController {
@Autowired
private UserDaoService service;

// public UserController(UserDaoService service) {
// this.service = service;
// }

@GetMapping("/users")
public List<EntityModel<User>> retrieveAllUsers() {
List<EntityModel<User>> models = new ArrayList<>();
List<User> users = service.findAll();

// HATEOAS
for (User user : users) {
EntityModel model = EntityModel.of(user);
model.add(linkTo(methodOn(this.getClass()).retrieveAllUsers()).withSelfRel());
models.add(model);
}

// JacksonFilter
// SimpleBeanPropertyFilter filter = SimpleBeanPropertyFilter
// .filterOutAllExcept("id", "name", "joinDate");
// FilterProvider filters = new SimpleFilterProvider().addFilter("UserInfo", filter);
//
// MappingJacksonValue mapping = new MappingJacksonValue(models);
// mapping.setFilters(filters);

return models;
}

@GetMapping("/users/{id}")
public MappingJacksonValue retrieveUser(@PathVariable int id) {
User user = service.findOne(id);
if (user == null) {
throw new UserNotFoundException(id);
}

// HATEOAS
EntityModel<User> model = EntityModel.of(user);
WebMvcLinkBuilder linkTo = linkTo(methodOn(this.getClass()).retrieveAllUsers());
model.add(linkTo.withRel("all-users"));
linkTo = linkTo(methodOn(this.getClass()).updateUser(user, id));
model.add(linkTo.withRel("update-user"));

// JacksonFilter
SimpleBeanPropertyFilter filter = SimpleBeanPropertyFilter
.filterOutAllExcept("id", "name", "joinDate");
FilterProvider filters = new SimpleFilterProvider().addFilter("UserInfo", filter);

MappingJacksonValue mapping = new MappingJacksonValue(model);
mapping.setFilters(filters);
return mapping;
}

@PostMapping("/users")
public ResponseEntity<User> createUser(@Valid @RequestBody User user) {
User savedUser = service.save(user);

URI location = ServletUriComponentsBuilder.fromCurrentRequest()
.path("/{id}")
.buildAndExpand(savedUser.getId())
.toUri();
return ResponseEntity.created(location).build();
}

@DeleteMapping("/users/{id}")
public void deleteUser(@PathVariable int id) {
User user = service.deleteById(id);

if (user == null) {
throw new UserNotFoundException(id);
}
}

@PutMapping("/users/{id}")
public ResponseEntity<User> updateUser(@RequestBody User user, @PathVariable int id) {
User updatedUser = service.update(user, id);

if (updatedUser != null) {
return ResponseEntity.noContent().build();
}
else {
throw new UserNotFoundException(id);

}
}
}

pom.xml

<?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.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>restful-web-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>restful-web-service</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>16</java.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-hateoas</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
</dependency>

<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
<version>2.10.2</version>
</dependency>
<dependency>
<groupId>org.springframework.hateoas</groupId>
<artifactId>spring-hateoas</artifactId>
</dependency>

<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
<version>3.0.0</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-rest-hal-explorer</artifactId>
<version>3.5.1</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>

</project>

해결 하셧나요 똑같네요 현상이

저도 같은 상황이라 해결해 보려고 꽤 찾아봤는데 모르겠네여 😂

Swagger 가 인식하는 것은 Endpoint 의 `반환타입`입니다.

고로 Wrapper 로 지정한 클래스가 보이게 됩니다.

근데 사실 Wrapper 는 편의상의 이유로 사용하게 되는데

여기서 MappingJacksonValue 는 `Serialization` 에서 `동적 Filter` 을 위해서 쓰였네요.

그렇다고 Swagger 보자고 소스코드 바꾸고 그러진 마시고

DTO class 에 지정하신 것처럼 `Endpoint` method 에

아래 `Annotation` 을 사용해서 Document UI 에 보여줄 실제 클래스를 지정해주실 수 있어요.

@ApiResponses
({
@ApiResponse(responseCode = "200", content = { @Content(mediaType = "application/json", schema = @Schema(implementation = 보여주고싶은클래스.class)) })
})

 

 

0

Dowon Lee님의 프로필 이미지
Dowon Lee
지식공유자

안녕하세요, 이도원입니다. 

위에 올려주신 코드를 그대로 적용하여 확인해 보았을 때, 오류 발생 없이 다음과 같이 메시지가 출력되는 것을 확인하였습니다. 작성하신 코드에는 문제가 없어 보입니다. 괜찮으시면 작업하신 User.java, pom.xml 코드 등을 공유해 주실 수 있을까요? 오류를 같이 찾아 보도록 하겠습니다. 

edowon0623@gmail.com

참고로 아래 github에서 작업된 코드를 확인해 보실 수 있습니다.

https://github.com/joneconsulting/my-restful-services/settings

감사합니다. 

cnw529님의 프로필 이미지
cnw529

작성한 질문수

질문하기