SuccessResponse 를 @RestControllerAdvice 를 이용해 전역 처리 중이며
Swagger Example 커스터마이징을 하고 싶은 경우
이 글을 읽어주시면 도움이 될 것 입니다.
들어가며
Swagger 를 사용중이며, @RestControllerAdvice 를 사용해 전역 응답을 설정중인 경우
Swagger 응답 예시에서 wrapping 시켜주는 객체가 나타나지 않는다.
최근에는 springfox 공식 업데이트가 멈췄고 springdocs 를 많이 사용중이다.
springfox 같은 경우 기본 설정으로 제네릭 클래스를 응답으로 선택할 수 있는데
springdocs 에서는 이를 지원해주지 않아 다른 방법을 사용해야 한다.
SuccessResponseAdvice 예시
@Getter
@NoArgsConstructor
public class SuccessResponse<T> {
private final boolean success = true;
private final LocalDateTime timeStamp = LocalDateTime.now();
private int status;
private T data;
public SuccessResponse(T data) {
this.status = 200;
this.data = data;
}
public SuccessResponse(T data, int status) {
this.status = status;
this.data = data;
}
}
@RestControllerAdvice(basePackages = "kr.co.swagger")
public class SuccessResponseAdvice implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return true;
}
@Override
public Object beforeBodyWrite(
Object body,
MethodParameter returnType,
MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request,
ServerHttpResponse response) {
final HttpServletResponse servletResponse =
((ServletServerHttpResponse) response).getServletResponse();
int status = servletResponse.getStatus();
final HttpStatus resolve = HttpStatus.resolve(status);
if (resolve == null) {
return body;
}
if (resolve.is2xxSuccessful()) {
return new SuccessResponse<>(body, status);
}
return body;
}
}
예시 응답
실제 응답
이럴 경우 swagger 상에서 설정을 바꿔줘서 기본 예시를 wrapping 할 수 있다.
Swagger 동작 원리
{host}/v3/api-docs 로 접속할 경우 아래와 같이 swagger-ui 로 변환되기 이전의 JSON 데이터가 노출된다.
이를 바탕으로 스웨거는 화면에 API 문서를 띄우게 된다.
즉 이 JSON 데이터를 설정해준다면 스웨거가 그리는 화면을 수정할 수 있다는 것이다.
Swagger Customizer
스웨거에서는 customizer 라는 함수형 인터페이스를 제공하는데
이를 빈으로 등록하게 될 경우 커스터마이징을 할 수 있다
이 중에서 컨트롤러 메소드를 처리하는 OperationCustomizer 를 사용할 것이다.
@FunctionalInterface
public interface OperationCustomizer {
Operation customize(Operation operation, HandlerMethod handlerMethod);
}
공통 응답 랩핑 처리
@Configuration
public class SwaggerConfig {
...
@Bean
public OperationCustomizer operationCustomizer() {
return (operation, handlerMethod) -> {
this.addResponseBodyWrapperSchemaExample(operation);
return operation;
};
}
private void addResponseBodyWrapperSchemaExample(Operation operation) {
final Content content = operation.getResponses().get("200").getContent();
if (content != null) {
content.forEach((mediaTypeKey, mediaType) -> {
Schema<?> originalSchema = mediaType.getSchema();
Schema<?> wrappedSchema = wrapSchema(originalSchema);
mediaType.setSchema(wrappedSchema);
});
}
}
private Schema<?> wrapSchema(Schema<?> originalSchema) {
final Schema<?> wrapperSchema = new Schema<>();
wrapperSchema.addProperty("success", new Schema<>().type("boolean").example(true));
wrapperSchema.addProperty("timeStamp", new Schema<>().type("string").format("date-time").example(
LocalDateTime.now().toString()));
wrapperSchema.addProperty("status", new Schema<>().type("integer").example(200));
wrapperSchema.addProperty("data", originalSchema);
return wrapperSchema;
}
}
operation 의 getResponses() 를 통해 예시 응답 리스트를 꺼내고 그 중에서 200 성공 응답에 대해 가져온다.
JSON 에서 각 컨트롤러 메소드의 예시는 응답 코드 쌍으로 그룹핑된다.
그 후 내부에서는 각 mediaType 별로 그룹핑되어 있는데, 이 중에서 Schema 를 꺼내면 원본 예시가 나오게 된다.
이를 새로운 응답으로 랩핑해주면 원하는 결과가 나올 것이다.
기존 JSON
...
"/user": {
"get": {
"tags": [
"사용자 API"
],
"summary": "사용자 정보 조회",
"description": "사용자 정보를 조회합니다.",
"operationId": "getUser",
"responses": {
"200": {
"description": "OK",
"content": {
"application/json;charset=UTF-8": {
"schema": {
"$ref": "#/components/schemas/UserResponse"
}
}
}
}
}
}
}
...
새로운 JSON
...
"/user": {
"get": {
"tags": [
"사용자 API"
],
"summary": "사용자 정보 조회",
"description": "사용자 정보를 조회합니다.",
"operationId": "getUser",
"responses": {
"200": {
"description": "OK",
"content": {
"application/json;charset=UTF-8": {
"schema": {
"properties": {
"success": {
"type": "boolean",
"example": true
},
"timeStamp": {
"type": "string",
"format": "date-time",
"example": "2024-03-17T18:30:50.334396"
},
"status": {
"type": "integer",
"example": 200
},
"data": {
"$ref": "#/components/schemas/UserResponse"
}
}
}
}
}
}
}
}
}
...
결과
각 스키마에 property 가 지정되며 응답이 성공적으로 wrapping 되었다.
다만 각 필드를 삽입하는 과정에서 텍스트를 하드코딩하게 되는 문제가 있는데 리플렉션으로 해결해보았다.
@Configuration
public class SwaggerConfig {
...
@Bean
public OperationCustomizer operationCustomizer() {
return (operation, handlerMethod) -> {
this.addResponseBodyWrapperSchemaExample(operation, SuccessResponse.class, "data");
return operation;
};
}
private void addResponseBodyWrapperSchemaExample(Operation operation, Class<?> type, String wrapFieldName) {
final Content content = operation.getResponses().get("200").getContent();
if (content != null) {
content.keySet()
.forEach(mediaTypeKey -> {
final MediaType mediaType = content.get(mediaTypeKey);
mediaType.schema(wrapSchema(mediaType.getSchema(), type, wrapFieldName));
});
}
}
@SneakyThrows
private <T> Schema<T> wrapSchema(Schema<?> originalSchema, Class<T> type, String wrapFieldName) {
final Schema<T> wrapperSchema = new Schema<>();
final T instance = type.getDeclaredConstructor().newInstance();
for (Field field : type.getDeclaredFields()) {
field.setAccessible(true);
wrapperSchema.addProperty(field.getName(), new Schema<>().example(field.get(instance)));
field.setAccessible(false);
}
wrapperSchema.addProperty(wrapFieldName, originalSchema);
return wrapperSchema;
}
}
이렇게 되면 SuccessResponse 필드 코드가 바뀌었을 때에도 조치가 가능하다.
이 부분은 최초 Swagger 로딩시에만 리플렉션이 동작하고 이후에는 캐싱되기 때문에 어느정도 감수할만 하다고 생각해서 적용해보았다.
스웨거 커스터마이징에 익숙해진다면 더 좋은 방법이 있을 수도 있을 것 같다.
마치며
스웨거 응답을 커스터마이징하여 API 문서의 공통 응답을 변경하는 방법을 알아보았다.
이외에도 예외 처리에 대한 문서 자동화, 복잡한 처리 등이 가능하니 잘 찾아본다면 더 깔끔하고 정확한 스웨거 문서를 작성할 수 있겠다.
'📡 백엔드 > 🌱 Spring Boot' 카테고리의 다른 글
[Spring] x-www-form-urlencoded 요청 JSON 으로 변환하여 받기 (0) | 2024.04.01 |
---|---|
[Spring] Jackson Module 을 이용한 Jackson 확장 (1) | 2024.03.25 |
[Spring] spring-data-envers 를 이용한 엔티티 변경 이력 관리 (0) | 2024.03.10 |
[Spring] Redisson 분산락 AOP로 동시성 문제 해결하기 (트랜잭션 전파속성 NEVER 사용) (0) | 2023.07.06 |
[Spring] 스프링 소셜 로그인 OIDC 방식으로 구현하기 (OAuth with OpenID Connect) (1) | 2023.03.25 |