1. 서론
이번 글은 보통의 spring sagger 연결 방법이 아니다.
@ApiOperation(value = "ID로 사용자 가져오기")
@ApiResponses(value = {
@ApiResponse(code = 200, message = "성공적으로 사용자 정보를 가져왔습니다"),
@ApiResponse(code = 401, message = "리소스를 볼 권한이 없습니다"),
@ApiResponse(code = 403, message = "접근이 금지되었습니다"),
@ApiResponse(code = 404, message = "요청한 리소스를 찾을 수 없습니다")
})
@GetMapping("/users/{id}")
public User getUserById(@PathVariable("id") Long id) {
// ...
}
보통 이런방식으로 swagger를 적지 않는가? 아래 2가지의 전역 응답 및 예외 처리로 인해서 이미 http 상태코드 메세지를 어느정도 메세지를 어떤걸 보낼껀지 그리고 응답 형식을 정해논 상황이였다. 그리하며 @ApiResponse를 사용하면 코드를 2번 작성하는 등의 귀찮은 일이 발생하게 된다.
나는 이런 일들을 해결하고 싶었다. 간단하게 어노테이션 하나만 달아도 예외, 응답이 swagger에 응답 형식에 맞춰서 출력되도록 하고 싶었다. 이번 글은 그런 글이다.
2. 만들고 싶은 기능 정의 및 설계 과정
{
"status": "201 CREATED",
"msg": "리소스가 성공적으로 생성되었습니다",
"data": {
"id": 0,
"name": "string",
"email": "string"
},
"code": "CREATED"
}
위 접은글에서 본것과 같이 나는 이런 형식에 응답을 하도록 만들어 놨다.
- 형식을 적느라 swagger 어노테이션에 적는 중복된 코드를 줄이자.
- CommonExceptionCode, CommonResponseCode, CustomResponseCode, CustomExceptionCode를 사용할 수 있도록 하자
- 응답은 swagger에 1개만 나타내도 되지만 예외는 여러가지를 나타낼 수 있어야 한다. 이를 커스텀 어노테이션으로 해결하자
3. 코드 작성 과정
1. 자동 문서화 커스터 마이징 하기
@Bean
public OperationCustomizer operationCustomizer() {
return (operation, handlerMethod) -> {
ResponseCodeAnnotation annotation = handlerMethod.getMethodAnnotation(ResponseCodeAnnotation.class);
CommonResponseCode responseCode = Optional.ofNullable(annotation).map(ResponseCodeAnnotation::value).orElse(CommonResponseCode.SUCCESS);
this.addResponseBodyWrapperSchema(operation, SuccessResponse.class, "data", responseCode);
ExceptionCodeAnnotations exceptionAnnotations = handlerMethod.getMethodAnnotation(ExceptionCodeAnnotations.class);
if (exceptionAnnotations != null) {
for (CommonExceptionCode exceptionCode : exceptionAnnotations.value()) {
this.addExceptionResponseBodyWrapperSchemaExample(operation, exceptionCode);
}
}
return operation;
};
}
Rest 방식을 따르는 모든 controller는 모두 응답이 있다.
일단 이들은 모두 기본적으로 200의 응답을 가진다. 하지만 이들의 응답은 201,202,203 일수도 있다. 이를위해서 어노테이션을 만들었다.
/*
Target : 어노테이션 적용될 수 있는 위치 지금은 메서드로 설정
Retention : 어노테이션의 라이프 사이클 런타임 시에 유지되도록 설정
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ResponseCodeAnnotation {
//응답 어노테이션의 기본값은 SUCCESS가 되도록 설정 어노테이션 아무것도 정하지 않으면 200 응답으로 처리됨
CommonResponseCode value() default CommonResponseCode.SUCCESS;
}
주석과 같다 모든 메서드에 대해서 아무것도 정하지 않으면 200으로 응답하도록 했고
만약 어노테이션에서 설정한 commonResponseCode가 있다면 그것을 따라가도록 했다.
@GetMapping("/user")
@ResponseCodeAnnotation(CommonResponseCode.CREATED)
public ResponseEntity<User> getUser() {
User user = new User();
return new ResponseEntity<>(user, HttpStatus.CREATED);
}
이런 방식으로 CREATED를 응답해야하면 어노테이션을 통해 간단하게 명시할 수 있다.
다시 OperationCustomizer를 보자
예외는 응답이랑 같이 존재할 확률이 높다. 따라서 응답 어노테이션을 사용했다면 그것 또한 적어주도록 했다.
ExceptionCodeAnnotations exceptionAnnotations = handlerMethod.getMethodAnnotation(ExceptionCodeAnnotations.class);
if (exceptionAnnotations != null) {
for (CommonExceptionCode exceptionCode : exceptionAnnotations.value()) {
this.addExceptionResponseBodyWrapperSchemaExample(operation, exceptionCode);
}
}
예외 어노테이션은 여러 가지가 올수 있기 때문에 배열로 commonExceptionCode를 받아주고 각가의 코드에 대해서 따로 swagger 문서를 작성하도록 했다.
@GetMapping("/user")
@ExceptionCodeAnnotations({CommonExceptionCode.BAD_GATEWAY, CommonExceptionCode.BAD_REQUEST})
public ResponseEntity<User> getUser() {
User user = new User();
return new ResponseEntity<>(user, HttpStatus.CREATED);
}
위 코드와 같이 여러가지 예외를 어노테이션 하나를 통해서 처리할 수 있다.
2. 2XX대 응답 wrapping 하기전 전처리
private void addResponseBodyWrapperSchema(Operation operation, Class<?> type, String wrapFieldName, CommonResponseCode responseCode) {
ApiResponses responses = operation.getResponses();
String responseCodeKey = String.valueOf(responseCode.getHttpStatus().value());
if (!"200".equals(responseCodeKey)) {
ApiResponse existingResponse = responses.get("200");
if (existingResponse != null) {
ApiResponse newResponse = new ApiResponse()
.description(existingResponse.getDescription())
.content(existingResponse.getContent());
responses.addApiResponse(responseCodeKey, newResponse);
responses.remove("200");
}
}
ApiResponse response = responses.computeIfAbsent(String.valueOf(responseCode.getHttpStatus().value()), key -> new ApiResponse());
response.setDescription(responseCode.getData());
Content content = response.getContent();
if (content != null) {
content.keySet().forEach(mediaTypeKey -> {
MediaType mediaType = content.get(mediaTypeKey);
if (mediaType != null) {
mediaType.setSchema(wrapSchema(mediaType.getSchema(), type, wrapFieldName, responseCode));
}
});
}
}
위 코드는 전체적인 코드이다.
if (!"200".equals(responseCodeKey)) {
ApiResponse existingResponse = responses.get("200");
if (existingResponse != null) {
ApiResponse newResponse = new ApiResponse()
.description(existingResponse.getDescription())
.content(existingResponse.getContent());
responses.addApiResponse(responseCodeKey, newResponse);
responses.remove("200");
}
}
위 코드가 행하는 일은 200응답이 아닐 때 진행된다.
201,203 등으로 응답을 할때는 200응답이 swagger에 적힐 필요가 없다. 하지만 이 코드를 추가해 주지 않으니까
200응답이 자동으로 적히고 있었다. 이유는 swagger에서 ApiResponses에 자동으로 200 응답을 담아두는데 이걸 삭제하고 swagger문서를 만들지 않으니까 200응답이 자동으로 생성되는 것이였다. 정확히는 모든 응답에 대한 필드들이 있는데 보통은 값이 채워저 있지 않다. 하지만 200에는 값이 항상 채워져 있고 200응답이 아닐경우 필요가 없으니 제거해 주는 것이다.
ApiResponse response = responses.computeIfAbsent(String.valueOf(responseCode.getHttpStatus().value()), key -> new ApiResponse());
response.setDescription(responseCode.getData());
Content content = response.getContent();
if (content != null) {
content.keySet().forEach(mediaTypeKey -> {
MediaType mediaType = content.get(mediaTypeKey);
if (mediaType != null) {
mediaType.setSchema(wrapSchema(mediaType.getSchema(), type, wrapFieldName, responseCode));
}
});
}
위 코드에서는 기존에 있던 response에서 모든 응답 코드를 확인한다.
내가 보내는 응답 http 상태코드에만 content가 null 이 아니다 이 content에서만 swagger를 만들기 위해서 공통 응답 형식을 만드는 wrapSchema를 사용한다.
3. 2XX대 응답 wrapping
@SneakyThrows
private <T> Schema<T> wrapSchema(Schema<?> originalSchema, Class<T> type, String wrapFieldName, CommonResponseCode responseCode) {
Schema<T> wrapperSchema = new Schema<>();
T instance = type.getDeclaredConstructor().newInstance();
for (Field field : type.getDeclaredFields()) {
field.setAccessible(true);
Schema<?> fieldSchema = new Schema<>().example(field.get(instance));
if (field.getName().equals("status")) {
fieldSchema.example(responseCode.getHttpStatus().toString());
} else if (field.getName().equals("msg")) {
fieldSchema.example(responseCode.getData());
} else if (field.getName().equals("data")) {
fieldSchema = originalSchema;
} else if (field.getName().equals("code")) {
fieldSchema.example(responseCode.name());
}
wrapperSchema.addProperty(field.getName(), fieldSchema);
field.setAccessible(false);
}
wrapperSchema.addProperty(wrapFieldName, originalSchema);
return wrapperSchema;
}
type.getDeclaredConstructor() 를 사용하려면 throw로 던져줘야하는데이를 간단하게 처리하기 위해서 @SneakyThrows를 사용한다.
swagger에서는 문서를 작성할 때 아래 사진과 같이 Schema라는 객체를 사용한다. 이를 내가 사용하는 응답에 맞게 커스터마이징 하는 과정이다. 내가 보내는 응답의 key들을 찾에서 schema에 설정해준다. 그럼 자동으로 값들이 적혀있는 Schema를 확인할 수 있을 것이다.
4. 4,5XX대 예외 응답 wrapping 하기전 전처리
private void addExceptionResponseBodyWrapperSchemaExample(Operation operation, CommonExceptionCode exceptionCode) {
ApiResponses responses = operation.getResponses();
ApiResponse response = responses.computeIfAbsent(String.valueOf(exceptionCode.getHttpStatus().value()), key -> new ApiResponse());
response.setDescription(exceptionCode.getData());
Content content = response.getContent();
if (content == null) {
content = new Content();
response.setContent(content);
}
MediaType mediaType = content.getOrDefault("application/json", new MediaType());
mediaType.setSchema(createExceptionSchema(exceptionCode));
content.addMediaType("application/json", mediaType);
response.content(content);
responses.addApiResponse(String.valueOf(exceptionCode.getHttpStatus().value()), response);
}
예외 또한 방식은 같다. content를 확인해서 있는 값이라면 warpping을 시작한다.
이제 보니까 예외에만 mediatype을 설정해 줬다.
5. 4,5XX대 예외 응답 wrapping
private Schema<?> createExceptionSchema(CommonExceptionCode exceptionCode) {
Schema<?> exceptionSchema = new Schema<>();
exceptionSchema.addProperty("status", new Schema<>().example(exceptionCode.getHttpStatus().toString()));
exceptionSchema.addProperty("code", new Schema<>().example(exceptionCode.name()));
exceptionSchema.addProperty("data", new Schema<>().example(exceptionCode.getData()));
return exceptionSchema;
}
예외에는 data에 예외 설명 message만 들어가서 상대적으로 간단하다.
위 과정을 통해 이런식으로 간단하고 빠르게 만들어지는 응답을 만들어 보자.
'Tool > Swagger' 카테고리의 다른 글
[Tool/Swagger] 문서 작성법 (0) | 2024.08.01 |
---|
Coding, Software, Computer Science 내가 공부한 것들 잘 이해했는지, 설명할 수 있는지 적는 공간