Swagger는 프론트엔드와 협업하기에 API 를 문서화 해주는 아주 좋은 도구라고 생각한다.
하지만 요청, 결과값만 적는다면 Controller 에서 작성하는 어노테이션의 양도 그리 많지 않지만,
우리는 에러 처리 내용또한 적어줘야 한다.
하지만 이 에러처리 내용을 모두 적어주게 된다면 우리의 컨트롤러는
@PostMapping("/join")
@PreAuthorize("hasRole('ROLE_GUEST')")
@Operation(
summary = "회원가입",
description = "OAuth2 로그인 후 /register로 리디렉션 후 추가 정보 기입 후 회원가입을 완료합니다.",
requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "회원가입 정보",
required = true,
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = JoinRequest.class)
)
)
)
@ApiResponses(value = {
@io.swagger.v3.oas.annotations.responses.ApiResponse(
responseCode = "200",
description = "회원가입 성공",
useReturnTypeSchema = true
),
@io.swagger.v3.oas.annotations.responses.ApiResponse(
responseCode = "400",
description = "요청 데이터가 유효하지 않은 경우",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = ApiResponse.class),
examples = @ExampleObject(
value = "{\"code\": 400, \"status\": \"BAD_REQUEST\", \"message\": \"성별을 입력해주세요\", \"data\": null}"
)
)
),
@io.swagger.v3.oas.annotations.responses.ApiResponse(
responseCode = "500",
description = "서버 내부에서 처리되지 않은 오류가 발생한 경우",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = ApiResponse.class),
examples = @ExampleObject(
name = "서버 오류 예시",
value = "{ \"code\": 500, \"status\": \"INTERNAL_SERVER_ERROR\", \"message\": \"알 수 없는 오류가 발생했습니다.\", \"data\": null }"
)
)
)
})
public ApiResponse<MemberResponse> join(@RequestBody @Valid JoinRequest joinRequest,
HttpServletResponse response) {
return ApiResponse.created(memberService.join(joinRequest.toServiceRequest(), response));
}
돌이킬 수 없게 길어지기만 할 뿐이다. 이런 식으로 작성하면 Controller 가 너무 더러워진다.
그래서 다른 사람도 코드를 읽기 편하게 이를 바꿔보고자 한다.
아래 어노테이션을 살펴보면
@io.swagger.v3.oas.annotations.responses.ApiResponse(
responseCode = "500",
description = "서버 내부에서 처리되지 않은 오류가 발생한 경우",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = ApiResponse.class),
examples = @ExampleObject(
name = "서버 오류 예시",
value = "{ \"code\": 500, \"status\": \"INTERNAL_SERVER_ERROR\", \"message\": \"알 수 없는 오류가 발생했습니다.\", \"data\": null }"
)
)
)
에러 처리에서 가장 중요한 내용은 responseCode, description, status 이렇게 세가지 이다.
이 외에 내용은 모두 똑같고 쓸데 없이 길기만 하다. 하지만 저 세가지 내용만을 Controller 에 적어준다 하더라도 Controller 의 코드는 읽기도 싫게 길어지기만 할 것이다.
그렇다면 다른 문서에 작성하여 이를 가져오는 방법은 없을까 생각이 들었다.
결론부터 말하자면 Swagger Config 에 커스텀 어노테이션을 통해 처리하도록 진행했다
SwaggerExceptionResponse.java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SwaggerExceptionResponse {
ErrorCode[] value();
}
위와 같이 SwaggerExceptionResponse 파일을 설정하였고 ErrorCode 라는 Enum 파일을 생성해
이곳에 직접 에러코드와 설명을 명세하도록 하였다.
ErrorCode.java
public enum ErrorCode {
// 401 에러
UNAUTHORIZED_ATK_ERROR("E_UAT", UNAUTHORIZED, "AccessToken is invalid"),
UNAUTHORIZED_RTK_ERROR("E_URT", UNAUTHORIZED, "RefreshToken is invalid"),
EXPIRED_TOKEN_ERROR("E_EXT", UNAUTHORIZED, "JWT 토큰이 만료되었습니다."),
UNSUPPORTED_TOKEN_ERROR("E_UST", UNAUTHORIZED, "지원하지 않는 JWT 토큰입니다."),
EMPTY_TOKEN_ERROR("E_EMT", UNAUTHORIZED, "JWT 토큰이 비어 있습니다."),
private final String code;
private String message;
private final HttpStatus status;
ErrorCode(String code, HttpStatus status, String message) {
this.status = status;
this.message = message;
this.code = code;
}
public String getMessage() {
return this.message;
}
public String getCode() {
return code;
}
public HttpStatus getStatus() {
return status;
}
public void updateServerErrorMessage(String message){
this.message = message;
}
}
이렇게 적어놨으면 이제 어노테이션을 Swagger에서 처리하도록 설정만 하면 Controller 에 에러 처리 내용을 직접 명세하지 않더라도 Swagger 에서 볼 수 있도록 할 수 있다.
그렇다면 Swagger 설정은 어떻게 해야할까?
SwaggerConfig.java
@Bean
public OperationCustomizer applyCustomResponse() {
return (Operation operation, HandlerMethod handlerMethod) -> {
SwaggerExceptionResponse swaggerExceptionResponse =
handlerMethod.getMethodAnnotation(SwaggerExceptionResponse.class);
if(swaggerExceptionResponse == null){
return operation;
}
generateErrorCodeResponseExample(operation, swaggerExceptionResponse);
return operation;
};
}
우선 위와 같이 SwaggerConfig 에 어노테이션을 처리할 수 있도록 한다. 이렇게 하면 ErrorCode 에 명세된 에러 명세 값들을 받아와 이를 Response 에 등록하기만 하면 된다.
private void generateErrorCodeResponseExample(
Operation operation,
SwaggerExceptionResponse swaggerExceptionResponse
) {
ApiResponses responses = operation.getResponses();
List<ErrorCode> errorCodes = toExceptionStatusList(swaggerExceptionResponse);
Map<Integer, List<SwaggerExampleHolder>> statusWithExampleHolders =
generateStatusWithExampleHolders(errorCodes);
addExamplesToResponses(responses, statusWithExampleHolders);
}
코드를 읽어보자면 swaggerExceptionResponse 에 있는 값을 읽어와 errorCodes 리스트로 변환한다. 그리고 이를 Swagger 형식에 맞게
private Map<Integer, List<SwaggerExampleHolder>> generateStatusWithExampleHolders(
List<ErrorCode> errorCodes
) {
return
errorCodes.stream()
.map(
errorCode -> {
try {
return SwaggerExampleHolder.builder()
.holder(getSwaggerExample(errorCode))
.code(errorCode.getStatus().value())
.name(errorCode.getCode())
.build();
} catch (Exception e) {
throw new RuntimeException(e);
}
})
.collect(groupingBy(SwaggerExampleHolder::getCode));
}
위와 같이 Error Status code 로 묶어 Map 객체를 생성한 후 이를 Swagger에 아까 Controller 에서 직접 작성했던 형식 그대로 이를 적용해주면 된다.
private void addExamplesToResponses(
ApiResponses responses,
Map<Integer, List<SwaggerExampleHolder>> statusWithExampleHolders) {
statusWithExampleHolders.forEach(
(status, v) -> {
Content content = new Content();
MediaType mediaType = new MediaType();
ApiResponse apiResponse = new ApiResponse();
v.forEach(
exampleHolder -> mediaType.addExamples(
exampleHolder.getName(), exampleHolder.getHolder()));
content.addMediaType(org.springframework.http.MediaType.APPLICATION_JSON_VALUE,
mediaType);
apiResponse.setContent(content);
responses.addApiResponse(status.toString(), apiResponse);
});
}
이렇게 적용해주면 Controller 의 코드가 다음과 같이 깔끔해지는 것을 볼 수 있다.
@PostMapping("/join")
@PreAuthorize("hasRole('ROLE_GUEST')")
@Operation(summary = "회원가입", description = "OAuth2 로그인 후 /register로 리디렉션 후 추가 정보 기입 후 회원가입을 완료합니다.")
@SwaggerExceptionResponse({INVALID_EMAIL, PROVIDER_NOT_NULL, MEMBER_NAME_NOT_NULL, MEMBER_GENDER_NOT_NULL,
MEMBER_BIRTH_DATE_MUST_BE_PAST_OR_PRESENT, MEMBER_ADDRESS_NOT_NULL, MEMBER_FAMILY_ROLE_NOT_NULL, MEMBER_PROFILE_IMG_NOT_NULL, EXIST_EMAIL})
public ApiResponse<MemberResponse> join(@RequestBody @Valid JoinRequest joinRequest,
HttpServletResponse response) {
return ApiResponse.created(memberService.join(joinRequest.toServiceRequest(), response));
}

만약 다음 프로젝트를 진행한다면 위와 같은 방식으로 코드를 작성해나갈 것 같다.