개발일기

[Swagger] 스웨거 커스텀하여 @ApiResponses 커스텀 어노테이션으로 대체하기 본문

photocard backend server 개발일기

[Swagger] 스웨거 커스텀하여 @ApiResponses 커스텀 어노테이션으로 대체하기

한둥둥 2025. 1. 10. 00:04

 @ApiResponses을 커스텀 어노테이션을 사용하기로 결심한 이유

1. responseCode, description 을 수작업으로 반복적으로 모든 클래스에서 적용해야 한다.

2. @ApiResponse의 코드가 길다 보니 중요 코드가 눈에 확 들어오지 않는다. 

 

 

예시 코드) 

@RestController
@RequestMapping("/api/v1/auth")
@Tag(name = "인증", description = "인증 관련 API")
public class AuthMailController {

    @Operation(summary = "이메일 인증 코드 전송", description = "사용자의 이메일로 인증 코드를 전송합니다.")
    @ApiResponses({
        @ApiResponse(responseCode = "201", description = "성공적으로 인증 코드가 전송됨"),
        @ApiResponse(responseCode = "400", description = "잘못된 요청"),
        @ApiResponse(responseCode = "500", description = "서버 오류")
    })
    @PostMapping("/email")
    public ResponseEntity<ApiResponse<String>> sendEmail(
        @Parameter(description = "인증 요청 데이터", required = true) 
        @RequestBody AuthMailRequest request) {
        return ResponseEntity
            .status(HttpStatus.CREATED)
            .body(ApiResponse.success("인증 코드 전송 성공"));
    }
}

예시이다. 이런식으로 많이 입력을 해주어야 한다. 

 

그러다보니, 불편함을 느낄 수 있다. 나는 이런 문제를 해결 해주기 위하여 어노테이션을 통한 Swagger @ApiResponse를 개편해 주려고 한다. 

 

어노테이션 클래스들 

 

SccessResponseAnnotation 클래스 

package com.realworld.common.swagger;


import com.realworld.common.response.code.SuccessResponseCode;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SuccessResponseAnnotation {
    SuccessResponseCode value() default SuccessResponseCode.SUCCESS;

}

 

@Target(ElementType.METHOD) : 해당 어노테이션은 메서드에서만 적용 가능하도록 합니다. 

@Retention(RetentionPolicy.RUNTIME) : 이 어노테이션은 런타입 동안 유지되도록 설정합니다. 

 

package com.realworld.common.swagger;

import com.realworld.common.response.code.ExceptionResponseCode;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExceptionResponseAnnotations {
    ExceptionResponseCode[] value();
}

위에 와 동일합니다. value()메서드를 호출하면 ExceptionResponseCode 배열을 가져옵니다. 

 

package com.realworld.common.response.code;

import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;

@Getter
@RequiredArgsConstructor
public enum ExceptionResponseCode {

    // 비밀번호 변경에 실패한 경우
    FAIL_PASSWORD_CHANGE(HttpStatus.BAD_REQUEST, "패스워드 변경에 실패하였습니다.", 400),

    // 유저 아이디가 존재하지 않은 경우
    NOT_EXISTS_USERID(HttpStatus.BAD_REQUEST, "존재하지 않는 아이디입니다.", 400),

    // 비밀번호가 올바르지 않습니다.
    NOT_EQUAL_PASSWORD(HttpStatus.BAD_REQUEST, "비밀번호가 일치하지 않습니다.", 400),

    // 아이디
    VALIDATION_USERID_ERROR(HttpStatus.BAD_REQUEST, "아이디 형식이 올바르지 않습니다.", 400),

    // 존재하지 않는 이메일
    NOT_EXISTS_EMAIL(HttpStatus.BAD_REQUEST, "존재하지 않는 이메일입니다.", 400),

    // 이메일 인증 중복
    EMAIL_DUPLICATION_ERROR(HttpStatus.BAD_REQUEST, "이메일이 중복되었습니다.", 400),

    // 이메일 인증 코드 오류
    EMAIL_AUTH_NUMBER_ERROR(HttpStatus.BAD_REQUEST, "이메일 인증코드를 잘못 입력하였습니다. 다시 시도해 주세요.", 400),

    // 인증 시간 만료
    EMAIL_EXPIRED_ERROR(HttpStatus.UNAUTHORIZED, "이메일 인증 시간이 만료되었습니다.", 401),

    // 잘못된 이메일 요청
    EMAIL_REQUEST_ERROR(HttpStatus.BAD_REQUEST, "잘못된 요청입니다. 이메일 인증을 다시 시도해주세요.", 400),

    // 사용자 잘못된 요청
    LOGIN_REQUEST_ERROR(HttpStatus.BAD_REQUEST, "비밀번호 또는 아이디가 올바르지 않습니다.", 400),

    // 중복되는 아이디 이메일
    LOGIN_DUPLICATION_ERROR(HttpStatus.CONFLICT, "이미 존재하는 아이디 또는 이메일 입니다.", 409),

    // TOKEN 만료
    JWT_TOKEN_EXPIRED_ERROR(HttpStatus.UNAUTHORIZED, "토큰이 만료되었습니다.", 401),

    // 변조된 토큰
    JWT_WRONG_TYPE_TOKEN_ERROR(HttpStatus.UNAUTHORIZED, "변조된 토큰입니다.", 401),

    // Token 오류
    JWT_TOKEN_REQUEST_ERROR(HttpStatus.UNAUTHORIZED, "토큰이 유효하지 않습니다.", 401),

    // 토큰 오류
    JWT_UNKNOWN_ERROR(HttpStatus.UNAUTHORIZED, "토큰이 존재하지 않습니다.", 401),

    // 변조된 토큰
    UNSUPPORTED_TOKEN_ERROR(HttpStatus.UNAUTHORIZED, "변조된 토큰입니다.", 401),

    // 패스워드 오류
    PASSWORD_REQUEST_ERROR(HttpStatus.BAD_REQUEST, "패스워드 형식이 올바르지 않습니다.", 400),

    // 포토카드 게시물 조회 오류
    NOT_EXISTS_PRODUCT(HttpStatus.BAD_REQUEST, "포토카드가 존재하지 않습니다.", 400),

    // 포토카드 사용자 불일치 오류
    NOT_MATCHES_USER_PRODUCT(HttpStatus.BAD_REQUEST, "글을 작성한 유저가 아닙니다.", 400),

    // 임시 저장 포토카드 조회 오류
    NOT_EXISTS_TEMPORARILY_PRODUCT(HttpStatus.BAD_REQUEST, "임시 저장 포토 카드가 존재하지 않습니다.", 400),

    // 잘못된 서버 요청
    BAD_REQUEST_ERROR(HttpStatus.BAD_REQUEST, "Bad Request Exception", 400),

    // @RequestBody 데이터 미 존재
    REQUEST_BODY_MISSING_ERROR(HttpStatus.BAD_REQUEST, "Required request body is missing", 400),

    // 유효하지 않은 타입
    INVALID_TYPE_VALUE(HttpStatus.BAD_REQUEST, "Invalid Type Value", 400),

    // Request Parameter 로 데이터가 전달되지 않을 경우
    MISSING_REQUEST_PARAMETER_ERROR(HttpStatus.BAD_REQUEST, "Missing Servlet RequestParameter Exception", 400),

    // 입력/출력 값이 유효하지 않음
    IO_ERROR(HttpStatus.BAD_REQUEST, "I/O Exception", 400),

    // JSON 파싱 실패
    JSON_PARSE_ERROR(HttpStatus.BAD_REQUEST, "JsonParseException", 400),

    // jackson.core processing error
    JACKSON_PROCESS_ERROR(HttpStatus.BAD_REQUEST, "com.fasterxml.jackson.core Exception", 400),

    // 권한이 없음
    FORBIDDEN_ERROR(HttpStatus.FORBIDDEN, "Forbidden Exception", 403),

    // 서버로 요청한 리소스가 존재하지 않음
    NOT_FOUND_ERROR(HttpStatus.FORBIDDEN, "Not Found Exception", 403),

    // NULL Point Exception 발생
    NULL_POINT_ERROR(HttpStatus.FORBIDDEN, "Null Point Exception", 403),

    // @RequestBody 및 @RequestParam, @PathVariable 값이 유효하지 않음
    NOT_VALID_ERROR(HttpStatus.FORBIDDEN, "handle Validation Exception", 403),

    // @RequestBody 및 @RequestParam, @PathVariable 값이 유효하지 않음
    NOT_VALID_HEADER_ERROR(HttpStatus.FORBIDDEN, "Header에 데이터가 존재하지 않는 경우", 403),


    // 서버가 처리 할 방법을 모르는 경우 발생
    INTERVAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "Internal Server Error Exception", 500),

    // Transaction Insert Error
    INSERT_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "Insert Transaction Error Exception", 500),

    // Transaction Update Error
    UPDATE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "Update Transaction Error Exception", 500),

    // Transaction Delete Error
    DELETE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "Delete Transaction Error Exception", 500);

    private final HttpStatus httpStatus;
    private final String message;
    private final int resultCode;

}

 

필드)

  • HttpStatus httpStatus: HTTP 응답 상태를 나타내며, 예외 상황에 적절한 HTTP 상태 코드 지정 
  • String message : 예외 상황에 대한 메시지를 담고 있어 클라이언트가 에러의 원인을 이해할 수 있도록 설명 
  • int resultCode:  API의 응답 결과를 표현하는 코드 일반적으로 비즈니스 로직에서 에러를 구분하거나 클라이언트에게 더 상세한 정보를 제공하기 위해 사용한다. 

 

SwaggerConfigV3 클래스 

package com.realworld.common.swagger;

import com.realworld.common.response.SuccessResponse;
import com.realworld.common.response.code.ExceptionResponseCode;
import com.realworld.common.response.code.SuccessResponseCode;
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
import io.swagger.v3.oas.annotations.info.Info;
import io.swagger.v3.oas.annotations.security.SecurityScheme;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.media.Content;
import io.swagger.v3.oas.models.media.MediaType;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.parameters.RequestBody;
import io.swagger.v3.oas.models.responses.ApiResponse;
import io.swagger.v3.oas.models.responses.ApiResponses;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import lombok.SneakyThrows;
import org.springdoc.core.customizers.OperationCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.MethodParameter;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Optional;

@OpenAPIDefinition(
        info = @Info(
                title = "capo documentation",
                version = "1.0",
                description = "연예인 포토카드 api 스웨거 명세서"
        )
)
@SecurityScheme(
        name = "Bearer Authentication",
        type = SecuritySchemeType.HTTP,
        scheme = "bearer",
        bearerFormat = "JWT"
)
@Configuration
public class SwaggerConfigV3 {
    @Bean
    public OperationCustomizer operationCustomizer() {
        return (operation, handlerMethod) -> {
            // @SuccessResponseAnnotation 추가
            SuccessResponseAnnotation successAnnotation = handlerMethod.getMethodAnnotation(SuccessResponseAnnotation.class);
            // 응답 코드 설정
            SuccessResponseCode successResponseCode = Optional.ofNullable(successAnnotation).map(SuccessResponseAnnotation::value).orElse(SuccessResponseCode.SUCCESS);
            this.addResponseBodyWrapperSchema(operation, SuccessResponse.class, "result", successResponseCode);

            ExceptionResponseAnnotations exceptionAnnotation = handlerMethod.getMethodAnnotation(ExceptionResponseAnnotations.class);
            if (exceptionAnnotation != null) {
                this.addExceptionResponses(operation, exceptionAnnotation);
            }

            for (MethodParameter parameter : handlerMethod.getMethodParameters()) {
                SwaggerRequestBody swaggerRequestBody = parameter.getMethodAnnotation(SwaggerRequestBody.class);
                if (swaggerRequestBody != null) {
                    RequestBody requestBody = new RequestBody()
                            .description(swaggerRequestBody.description())
                            .required(swaggerRequestBody.required());

                    Content content = getContent(swaggerRequestBody);
                    requestBody.setContent(content);

                    operation.setRequestBody(requestBody);
                }
            }

            operation.addSecurityItem(new SecurityRequirement().addList("Bearer Authentication"));
            return operation;
        };
    }

    private Content getContent(SwaggerRequestBody swaggerRequestBody) {
        Content content = new Content();
        MediaType mediaType = new MediaType();

        if (swaggerRequestBody.content().length > 0 && swaggerRequestBody.content()[0].schema() != null) {
            // 커스텀 어노테이션의 첫 번째 content에서 schema 설정 가져오기
            Schema<?> schema = new Schema<>();

            Class<?> schemaImplementation = swaggerRequestBody.content()[0].schema().implementation();
            schema.set$ref(schemaImplementation.getSimpleName());
            mediaType.setSchema(schema);
        }

        content.addMediaType("application/json", mediaType);
        return content;
    }

    @SneakyThrows
    private <T> void addResponseBodyWrapperSchema(Operation operation, Class<T> type, String wrapFieldName, SuccessResponseCode successResponseCode) {
        ApiResponses responses = operation.getResponses();

        String responseCode = String.valueOf(successResponseCode.getHttpStatus().value());

        if (!"200".equals(responseCode)) {
            ApiResponse existingResponse = responses.get("200");
            this.changeResponseCode(existingResponse, responses, responseCode);
        }

        ApiResponse response = responses.computeIfAbsent(String.valueOf(successResponseCode.getHttpStatus()), key -> new ApiResponse());
        response.setDescription(successResponseCode.getMessage());
        Content content = response.getContent();

        if (content != null) {
            content.keySet().forEach(key -> {
                MediaType mediaType = content.get(key);
                mediaType.setSchema(wrapSchema(mediaType.getSchema(), type, wrapFieldName, successResponseCode));
            });
        }
    }

    private void changeResponseCode(ApiResponse existingResponse, ApiResponses responses, String responseCode) {
        if (existingResponse != null) {
            ApiResponse newResponse = new ApiResponse()
                    .description(existingResponse.getDescription())
                    .content(existingResponse.getContent());

            responses.addApiResponse(responseCode, newResponse);
            responses.remove("200");
        }
    }

    @SneakyThrows
    private <T> Schema<T> wrapSchema(Schema<?> originalSchema, Class<T> type, String wrapFieldName, SuccessResponseCode successResponseCode) {
        Schema<T> wrapperSchema = new Schema<>();
        T instance = type.getDeclaredConstructor().newInstance();

        for (Field field : type.getDeclaredFields()) {
            String fieldName = field.getName();
            String getterName = "get" + Character.toUpperCase(fieldName.charAt(0)) + fieldName.substring(1);
            Method getterMethod = type.getMethod(getterName);

            Object value = getterMethod.invoke(instance);

            Schema<?> fieldSchema = new Schema<>().example(value);
            if (fieldName.equals("result")) {
                fieldSchema = originalSchema;
            } else if (fieldName.equals("resultCode")) {
                fieldSchema.example(successResponseCode.getResultCode());
            } else if (fieldName.equals("resultMsg")) {
                fieldSchema.example(successResponseCode.getMessage());
            } else if (fieldName.equals("httpStatus")) {
                fieldSchema.example(successResponseCode.getHttpStatus());
            }

            wrapperSchema.addProperty(fieldName, fieldSchema);
        }

        wrapperSchema.addProperty(wrapFieldName, originalSchema);

        return wrapperSchema;
    }

    private void addExceptionResponses(Operation operation, ExceptionResponseAnnotations exceptionResponseAnnotations) {
        for (ExceptionResponseCode exceptionCode : exceptionResponseAnnotations.value()) {
            this.addExceptionResponse(operation, exceptionCode);
        }

    }

    private void addExceptionResponse(Operation operation, ExceptionResponseCode exceptionResponseCode) {
        ApiResponses responses = operation.getResponses();
        String responseCodeKey = String.valueOf(exceptionResponseCode.getHttpStatus().value());
        ApiResponse response = new ApiResponse()
                .description(exceptionResponseCode.getMessage());

        Content content = new Content();
        MediaType mediaType = new MediaType();
        mediaType.setSchema(createExceptionSchema(exceptionResponseCode));
        content.addMediaType("application/json", mediaType);
        response.setContent(content);

        responses.addApiResponse(responseCodeKey + "_" + exceptionResponseCode.name(), response);
    }

    private <T> Schema<T> createExceptionSchema(ExceptionResponseCode exceptionResponseCode) {
        Schema<T> exceptionSchema = new Schema<>();
        exceptionSchema.addProperty("httpStatus", new Schema<>().example(exceptionResponseCode.getHttpStatus().toString()));
        exceptionSchema.addProperty("resultCode", new Schema<>().example(exceptionResponseCode.getResultCode()));
        exceptionSchema.addProperty("resultMsg", new Schema<>().example(exceptionResponseCode.getMessage()));

        return exceptionSchema;
    }

}

해당 코드는 최대한 함수로 분리하여 코드를 구현하였다. 하지만 "200" 등 문자열 자체를 그냥 비교 해주는 부분이 조금 찝찝하다고 해야하나 그래서 고민을 하다가 해당 코드를 그대로 두는 방향으로 하기로 했다. 맨위에서 private static final 로 선언하기에는 너무 많은 정보가 있다고 판단하였다. 그렇다고 따로 Enum 타입을 만드는 것도 애매하다 판단하여 해당 방식으로 진행하였다. 맞는건지.. 아직 내 수준에서 고민하는 것에서는 결론이 안나오는거 같다. 

 

함수를 만들면서 최대한 기능 별로 동작 별로 나눌려고 노력하였다. 

 

클래스 레벨 애노테이션 @OpenApiDefinition 

  • openAPI 명세를 정의한다. 
  • title : API 문서 제목 
  • version API 버전. 
  • description: API에 대한 설명을 제공합니다. 

@SecurityScheme

  • 보안 스키마를 정의합니다. 
  • name: 스키마 이름 (Bearer Authentication)
  • type: HTTP 스키마를 사용
  • schme: bearer 인증 방식 
  • bearerFormat: JWT 토큰 형식 

@Configuration

  • Spring의 설정 클래스임을 나타냅니다. 

 

operationCustomizer 메서드 

@Bean
    public OperationCustomizer operationCustomizer() {
        return (operation, handlerMethod) -> {
            // @SuccessResponseAnnotation 추가
            SuccessResponseAnnotation successAnnotation = handlerMethod.getMethodAnnotation(SuccessResponseAnnotation.class);
            // 응답 코드 설정
            SuccessResponseCode successResponseCode = Optional.ofNullable(successAnnotation).map(SuccessResponseAnnotation::value).orElse(SuccessResponseCode.SUCCESS);
            this.addResponseBodyWrapperSchema(operation, SuccessResponse.class, "result", successResponseCode);

            ExceptionResponseAnnotations exceptionAnnotation = handlerMethod.getMethodAnnotation(ExceptionResponseAnnotations.class);
            if (exceptionAnnotation != null) {
                this.addExceptionResponses(operation, exceptionAnnotation);
            }

            for (MethodParameter parameter : handlerMethod.getMethodParameters()) {
                SwaggerRequestBody swaggerRequestBody = parameter.getMethodAnnotation(SwaggerRequestBody.class);
                if (swaggerRequestBody != null) {
                    RequestBody requestBody = new RequestBody()
                            .description(swaggerRequestBody.description())
                            .required(swaggerRequestBody.required());

                    Content content = getContent(swaggerRequestBody);
                    requestBody.setContent(content);

                    operation.setRequestBody(requestBody);
                }
            }

            operation.addSecurityItem(new SecurityRequirement().addList("Bearer Authentication"));
            return operation;
        };
    }

 

1. @SuccessResponseAnnotation 을 사용하여 성공 응답의 기본 구조를 정의

2. @ExceptionResponseAnnotation 을 사용하여 예외 응답 스키마를 문서화. 

3. @SwaggerRequestBody 처리하여 요청 본문(Request Body)의 문서화를 지원 

4. Bearer 인증(JWT) 사용한 보안 요구 사항 추가 

 

addResponseBodyWrapperSchema 

@SneakyThrows
    private <T> void addResponseBodyWrapperSchema(Operation operation, Class<T> type, String wrapFieldName, SuccessResponseCode successResponseCode) {
        ApiResponses responses = operation.getResponses();

        String responseCode = String.valueOf(successResponseCode.getHttpStatus().value());

        if (!"200".equals(responseCode)) {
            ApiResponse existingResponse = responses.get("200");
            this.changeResponseCode(existingResponse, responses, responseCode);
        }

        ApiResponse response = responses.computeIfAbsent(String.valueOf(successResponseCode.getHttpStatus()), key -> new ApiResponse());
        response.setDescription(successResponseCode.getMessage());
        Content content = response.getContent();

        if (content != null) {
            content.keySet().forEach(key -> {
                MediaType mediaType = content.get(key);
                mediaType.setSchema(wrapSchema(mediaType.getSchema(), type, wrapFieldName, successResponseCode));
            });
        }
    }

API 응답 본문을 래핑하여 통일된 스키마로 반환하도록 처리 

응답 코드가 200이 아닌 경우, 기존에 200으로 처리되어있는 디폴트를 새응답 코드로 변경하여 감싸준다. 

 

changeResponseCode

private void changeResponseCode(ApiResponse existingResponse, ApiResponses responses, String responseCode) {
        if (existingResponse != null) {
            ApiResponse newResponse = new ApiResponse()
                    .description(existingResponse.getDescription())
                    .content(existingResponse.getContent());

            responses.addApiResponse(responseCode, newResponse);
            responses.remove("200");
        }
    }

 

기존 응답 코드 200을 다른 응답 코드로 변경 

 

wrapSchema 

@SneakyThrows
    private <T> Schema<T> wrapSchema(Schema<?> originalSchema, Class<T> type, String wrapFieldName, SuccessResponseCode successResponseCode) {
        Schema<T> wrapperSchema = new Schema<>();
        T instance = type.getDeclaredConstructor().newInstance();

        for (Field field : type.getDeclaredFields()) {
            String fieldName = field.getName();
            String getterName = "get" + Character.toUpperCase(fieldName.charAt(0)) + fieldName.substring(1);
            Method getterMethod = type.getMethod(getterName);

            Object value = getterMethod.invoke(instance);

            Schema<?> fieldSchema = new Schema<>().example(value);
            if (fieldName.equals("result")) {
                fieldSchema = originalSchema;
            } else if (fieldName.equals("resultCode")) {
                fieldSchema.example(successResponseCode.getResultCode());
            } else if (fieldName.equals("resultMsg")) {
                fieldSchema.example(successResponseCode.getMessage());
            } else if (fieldName.equals("httpStatus")) {
                fieldSchema.example(successResponseCode.getHttpStatus());
            }

            wrapperSchema.addProperty(fieldName, fieldSchema);
        }

        wrapperSchema.addProperty(wrapFieldName, originalSchema);

        return wrapperSchema;
    }

 

API 응답을 래핑하는 스키마를 생성

successResponse  같은 구조로 데이터를 감쌈. 

동적으로 필드 이름과 값을 기반으로 스키마 생성 

 

addExceptionResponses 및 addExceptionResponse

private void addExceptionResponses(Operation operation, ExceptionResponseAnnotations exceptionResponseAnnotations) {
        for (ExceptionResponseCode exceptionCode : exceptionResponseAnnotations.value()) {
            this.addExceptionResponse(operation, exceptionCode);
        }

    }

    private void addExceptionResponse(Operation operation, ExceptionResponseCode exceptionResponseCode) {
        ApiResponses responses = operation.getResponses();
        String responseCodeKey = String.valueOf(exceptionResponseCode.getHttpStatus().value());
        ApiResponse response = new ApiResponse()
                .description(exceptionResponseCode.getMessage());

        Content content = new Content();
        MediaType mediaType = new MediaType();
        mediaType.setSchema(createExceptionSchema(exceptionResponseCode));
        content.addMediaType("application/json", mediaType);
        response.setContent(content);

        responses.addApiResponse(responseCodeKey + "_" + exceptionResponseCode.name(), response);
    }

@ExceptionResponseAnnotations에 정의된 예외 응답 코드를 문서화 

각 예외 응답은 HTTP 상태 코드와 메시지를 포함 

 

createExceptionSchema

private <T> Schema<T> createExceptionSchema(ExceptionResponseCode exceptionResponseCode) {
        Schema<T> exceptionSchema = new Schema<>();
        exceptionSchema.addProperty("httpStatus", new Schema<>().example(exceptionResponseCode.getHttpStatus().toString()));
        exceptionSchema.addProperty("resultCode", new Schema<>().example(exceptionResponseCode.getResultCode()));
        exceptionSchema.addProperty("resultMsg", new Schema<>().example(exceptionResponseCode.getMessage()));

        return exceptionSchema;
    }

예외 응답의 스키마를 생성해준다. 

각 필드(httpStatus, resultCode, resultMsg) 이 필드는 SuccesResponse<T>의 필드 값이다. 

 

 

ExceptionResponse<T> 클래스 

package com.realworld.common.response;

import lombok.Builder;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;

@Getter
@Builder
@RequiredArgsConstructor
public class ExceptionResponse<T> {
    private final HttpStatus httpStatus;
    private final int resultCode;
    private final T result;
    private final String resultMsg;

}

 

예외처리시, 들어갈 필드 값

 

SuccessResponse<T> 클래스 

package com.realworld.common.response;

import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;

@Getter
@RequiredArgsConstructor
@NoArgsConstructor(force = true)
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public class SuccessResponse<T> {

    private final T result;

    private final int resultCode;

    private final HttpStatus httpStatus;

    private final String resultMsg;

}

 

각각의 필드 값 존재 

 

package com.realworld.web.auth.mail.controller;

import com.realworld.application.auth.mail.service.AuthMailService;
import com.realworld.common.response.SuccessResponse;
import com.realworld.common.response.code.ExceptionResponseCode;
import com.realworld.common.response.code.SuccessResponseCode;
import com.realworld.common.swagger.ExceptionResponseAnnotations;
import com.realworld.common.swagger.SuccessResponseAnnotation;
import com.realworld.common.swagger.SwaggerRequestBody;
import com.realworld.web.auth.mail.payload.request.AuthMailRequest;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@Tag(name= "auth", description="인증")
@RestController
@RequiredArgsConstructor
@RequestMapping("/v2/auth")
public class AuthController {

    private final AuthMailService authMailService;

    @Operation(
        summary = "이메일 인증 메일 보내기",
        description = "이메일 인증 메일 보내기 API"
    )
    @SuccessResponseAnnotation(SuccessResponseCode.CREATED)
    @ExceptionResponseAnnotations({ExceptionResponseCode.EMAIL_REQUEST_ERROR})
    @PostMapping(value="/email")
    public ResponseEntity<SuccessResponse<Object>> authMail(@SwaggerRequestBody(description = "이메일 인증 보내기 요청 정보", required = true, content = @Content(schema = @Schema(implementation = AuthMailRequest.class))) @RequestBody AuthMailRequest request) {
        authMailService.send(request);

        return ResponseEntity.ok(new SuccessResponse<>(null, 201, HttpStatus.CREATED,"메일 전송 성공"));
    }

}

이런식으로 진짜 간단하게 어노테이션으로 모든걸 해결 할 수 있게 되었다. 

여담으로 @SwaggerRequestBody는  @RequestBody 스웨거를 사용하지 않기 위해 만든 어노테이션이다.