Swagger 문서 작성 깔끔하게 하기

MoonJaeGyeong·2025년 5월 19일

개발 공부

목록 보기
3/3

개요


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 설정


그렇다면 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));
    }

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

profile
내 맘대로 끄적이는 개발 블로그

0개의 댓글