프로젝트

스웨거 @ApiResponses를 커스텀 어노테이션으로 대체해보기

컴맹 개발자 2024. 9. 20. 11:22

 

1.문제점

 

현재는 ErrorCode를 인터페이스로 정의하여, 상황에 맞는 다양한 에러 코드를 enum으로 커스텀하여 구현하고 있습니다.

이를 통해 각 도메인 에러 처리를 객체 지향적으로 작성하고 관리하려고 시도했습니다.

 

public interface ErrorCode {
    HttpStatus getHttpStatus();
    String getErrorCode();
}
package com.linkmoa.source.domain.site.error;

import com.linkmoa.source.global.error.code.ErrorCode;
import lombok.Getter;
import org.springframework.http.HttpStatus;

@Getter
public enum SiteErrorCode implements ErrorCode {

    SITE_NOT_FOUND(HttpStatus.NOT_FOUND,"Site를 찾을 수 없습니다."),
    SITE_ERRORCODE_TEST(HttpStatus.NOT_FOUND,"site error code 테스트");

    private HttpStatus httpStatus;
    private String errorMessage;

    SiteErrorCode(HttpStatus httpStatus, String errorMessage) {
        this.httpStatus = httpStatus;
        this.errorMessage = errorMessage;
    }

    @Override
    public HttpStatus getHttpStatus() {
        return httpStatus;
    }

    @Override
    public String getErrorMessage(){
        return errorMessage;
    }


}

 

 

하지만 이렇게 ErrorCode를 인터페이스로 정의하고 각 도메인 error를 enum을 통해 에러 코드를 관리한 후, Swagger를 사용해 API

문서화를 진행하면서 문제가 발생했습니다.

 

Swagger에서 @ApiResponses @ApiResponse를 사용하는 과정에서, 애노테이션의 속성들은 상수 값만 허용됩니다. 즉, httpStatus errorMessage 같은 값을 동적으로 가져와서 사용할 수 없습니다. 이로 인해 ErrorCode에서 정의한 값들을 자동으로 참조할 수 없고, 개발자가 직접 하나하나 수동으로 옮겨 적어야 하는 불편함이 생겼습니다.

@Tag(name="Post",description = "사이트 관련 API")
    @Operation(summary = "사이트 저장",description = "사이트를 저장합니다.")
    @ApiResponses(
            value = {
                    @ApiResponse(
                            responseCode = "400",
                            description = "실패",
                            content = @Content(
                                    schema = @Schema(implementation = ResponseError.class))),

                    @ApiResponse(
                            responseCode = "200",
                            description = "성공",
                            content = @Content(
                                    schema = @Schema(implementation = ResponseError.class)))
            })
    @PostMapping
    ResponseEntity<ApiSiteResponse<Long>> saveSite(
            @RequestBody @Validated SiteCreateRequestDto siteCreateRequestDto,
            @AuthenticationPrincipal PrincipalDetails principalDetails
    );

 

위와 같이 Swagger에서 에러 관련 응답을 정의할 때, ErrorCode 클래스에 있는 HTTP 상태 코드나 메시지를 자동으로 참조하지 못하고, 매번 일일이 복사해서 사용해야 하는 문제가 발생했습니다.

 

ErrorCode 에서 정의한 값들을 수동으로 적어야 되는 문제점

 

 

2. 해결 방법: 에러 응답 값을 출력할 수 있는 커스텀 어노테이션을 구현

 

현재 문제를 해결하기 위해, ErrorCode를 활용하여 코드와 예시 값을 자동으로 참조할 수 있도록 커스텀 어노테이션을 만들어보겠습니다. 이 어노테이션을 사용하면 Swagger 문서화 시에 에러 코드와 메시지를 일일이 수동으로 입력할 필요 없이, ErrorCode에서 정의한 값들을 간편하게 API 응답에 반영할 수 있습니다.

 

 

1.Swagger 문서화에 사용할 어노테이션 생성 

어노테이션을 생성합니다.

@Target 

  • 어토네이션을 적용할 수 있는 위치(타입)을 지정함. 예를 들어, 메소드 , 클래스 필드 등 특정 요소에만 붙일 수 있게 제약하는 것
  • 아래의 코드의 경우 ElementType.METHOD는  메소드에만 붙을 수 있는 어노테이션이라는 것을 의미합니다. 

@Retention

  • 어노테이션은 메타 정보가 유지되는 기간을 정의함. 즉, 어노테이션의 라이플 사이클 (어노테이션이 언제까지 살아있느냐)를 결정
  • 아래의 코드의 경우 RetentionPolicy.RUNTIME는 런타임 동안에도 유지되며, 프로그램 실행 중에 해당 어노테이션의 정보를 읽을 수 있음을 의미함. 즉, 코드가 실행되는 도중에도 어노텡션의 내용을 참조하거나 조작할 수 있음.
  • 또한, RUNTIME은 Reflection API로 접근도 가능
    Reflection API란? 
    기본적으로 Class의 객체를 생성해서 본 클래스를 인스턴스화,메소드 호출 등을 진행할 수 있습니다. 하지만, 구체적인 클래스 타입을 알지 못해도 그 클래스의 정보(메소드, 타입 ,변수 등)에 접근할 수 있게 해주는 자바 기법을 Reflection API라고 합니다.

Class<? extends ErrorCode> value();

저는 ErrorCode 인터페이스를 구현한 특정 enum의 타입들을 사용하여 에러 코드를 관리하고 있습니다. 그렇기 때문에, 해당 API 메소드에서 사용할 에러 코드(enum 타입)을 전달받을 수 있게 작성했습니다. 

package com.linkmoa.source.global.swagger;

import com.linkmoa.source.global.error.code.ErrorCode;

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 ApiErrorCodeExamples {

    Class<? extends ErrorCode> value(); // ErrorCode를 구현한 enum 클래스 타입을 배열로 받음

}

 

 

2.SwaggerConfig 작성

한번에 보기 힘드니 메소드 별로 나눠서 알아보겠습니다.

//SwaggerConfig.java

@Component
@RequiredArgsConstructor
public class SwaggerConfig {

@Bean
    public OperationCustomizer customize() {
        return (Operation operation, HandlerMethod handlerMethod) -> {
            ApiErrorCodeExamples apiErrorCodeExample =
                    handlerMethod.getMethodAnnotation(ApiErrorCodeExamples.class);
            // ApiErrorCodeExample 어노테이션 단 메소드 적용
            if (apiErrorCodeExample != null) {
                generateErrorCodeResponseExample(operation, apiErrorCodeExample.value());
            }
            return operation;
        };

}


    private void generateErrorCodeResponseExample(Operation operation, Class<? extends ErrorCode> value) {
        ApiResponses responses =operation.getResponses();

        ErrorCode[] errorCodes= value.getEnumConstants();

        Map<Integer, List<ExampleHolder>> statusWithExampleHolders = Arrays.stream(errorCodes)
                .map(
                        errorCode -> ExampleHolder.builder()
                                .holder(getSwaggerExample((Enum<?>) errorCode))
                                .name(((Enum<?>) errorCode).name()) // 직접 name() 메서드 사용
                                .code(errorCode.getHttpStatus().value())
                                .build()
                        )
                .collect(Collectors.groupingBy(ExampleHolder::getCode));

        // ExampleHolders를 ApiResponses에 추가
        addExamplesToResponses(responses, statusWithExampleHolders);
    }

    // exampleHolder를 ApiResponses에 추가
    private void addExamplesToResponses(ApiResponses responses,
                                        Map<Integer, List<ExampleHolder>> 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("application/json", mediaType);
                    apiResponse.setContent(content);
                    responses.addApiResponse(String.valueOf(status), apiResponse);
                }
        );
    }

    private Example getSwaggerExample(Enum<?> errorCodeEnum) {
        // Enum을 ErrorCode로 캐스팅
        ErrorCode errorCode = (ErrorCode) errorCodeEnum;

        // ResponseError 객체 생성
        ResponseError responseError = ResponseError.builder()
                .httpStatusCode(errorCode.getHttpStatus())
                .errorMessage(errorCode.getErrorMessage())
                .build();

       Example example =new Example();
       example.setValue(responseError);

       return example;

    }

 

 

 customize() 메서드 

    @Bean
    public OperationCustomizer customize() {
        return (Operation operation, HandlerMethod handlerMethod) -> {
            ApiErrorCodeExamples apiErrorCodeExample =
                    handlerMethod.getMethodAnnotation(ApiErrorCodeExamples.class);
            // ApiErrorCodeExample 어노테이션 단 메소드 적용
            if (apiErrorCodeExample != null) {
                generateErrorCodeResponseExample(operation, apiErrorCodeExample.value());
            }
            return operation;
        };

}


OperationCustomizer 빈은 리플렉션을 사용하여 특정 어노테이션이 달린 메소드 정보를 동적으로 탐색하고, 스웨거(Swagger) 문서에 자동으로 에러 응답을 처리하고 있습니다. 자세한 동작 과정은 아래와 같습니다. 

 

1.리플렉션을 통한 어노테이션 탐색 

  • handlerMethod.getMethodAnnotation(ApiErrorCodeExamples.class)를 호출하여, 현재 처리 중인 메소드에 @ApiErrorCodeExamples 어노테이션이 붙어 있는지 확인
  • getMethodAnnotation 메소드는 HandlerMethod 객체를 사용하여, 이 메소드에 특정 어노테이션이 붙어 있는지 확인합니다. 이를 통해 메소드에 @ApiErrorCodeExamples와 같은 어노테이션이 달려 있는지 런타임 동안에 동적으로 확인함. 이는 리플렉션을 활용한 방식으로 컴파일 타임이 아닌 런타임 중에 메타데이터를 검사할 수 있음.

2.어노테이션이 존재하는 경우 처리

  • @ApiErrorCodeExamples 어노테이션이 메소드에 존재하면, 해당 어노테이션의 value() 메소드를 호출여 에러 코드(enum 타입) 정보를 가져옴.
  • value() 메소드로 얻은 정보는 해당 메소드가 반환할 수 있는 에러 응답의 세부 정보를 포함

3.스웨거 예시 응답 생성

  • 가져온 에러 코드(enum 타입) 정보를 바탕으로 generateErrorCodeResponseExample 메소드를 호출하여 스웨거 문서에 자동으로 에러 응답 예시 값을 추가함.
  • generateErrorCodeResponseExample는 에러 코드의 HTTP 상태 코드와 에러 메시지를 스웨거 문서에 반영

4.API 문서에 반영 

  • 최종적으로 수정된 Operation 객체가 Swagger 문서에 반영됨. 

 

1.Operation이란

Operation은 Swagger의 각 API 엔드포인트에 대한 정보를 담고 있는 객체입니다.

OperationCustomizer를 사용해 Operation 객체를 수정하고 있으므로, 해당 API의 커스텀 정보나 에러 응답 예시를 추가하는 데 사용됩니다.

  • Swagger에서 API의 설명, 응답 코드, 요청/응답 스키마 등을 정의할 때 사용하는 클래스입니다.
  • 이 객체는 각 API가 어떻게 동작하는지에 대한 정보를 스웨거 문서에 표시하는 역할을 합니다.

2.HandlerMethod이란 

HandlerMethod는 스프링에서 사용되는 객체로, 특정 HTTP 요청에 매핑된 컨트롤러의 메소드 정보를 담고 있습니다.

  • 예를 들어, @PostMapping, @GetMapping 같은 어노테이션이 달린 메소드가 실제로 어떤 메소드인지, 그 메소드의 파라미터와
    리턴 타입은 무엇인지 등의 정보를 포함

 

generateErrorCodeResponseExample  메서드 

private void generateErrorCodeResponseExample(Operation operation, Class<? extends ErrorCode> value) {
    ApiResponses responses =operation.getResponses();

    ErrorCode[] errorCodes= value.getEnumConstants();

    Map<Integer, List<ExampleHolder>> statusWithExampleHolders = Arrays.stream(errorCodes)
            .map(
                    errorCode -> ExampleHolder.builder()
                            .holder(getSwaggerExample((Enum<?>) errorCode))
                            .name(((Enum<?>) errorCode).name()) // 직접 name() 메서드 사용
                            .code(errorCode.getHttpStatus().value())
                            .build()
                    )
            .collect(Collectors.groupingBy(ExampleHolder::getCode));

    // ExampleHolders를 ApiResponses에 추가
    addExamplesToResponses(responses, statusWithExampleHolders);
}

// ExampleHolder
@Getter
@Builder
public class ExampleHolder {


    private Example holder; // 스웨거 Example 객체
    private String name; // 에러 코드 이름
    private int code; // http 상태코드

}

//ErrorCode
public interface ErrorCode {
    HttpStatus getHttpStatus();
    //String getErrorCode();
    String getErrorMessage();
}

//SiteErrorCode  예시 
@Getter
public enum SiteErrorCode implements ErrorCode {

    SITE_NOT_FOUND(HttpStatus.NOT_FOUND,"Site를 찾을 수 없습니다."),
    SITE_ERRORCODE_TEST(HttpStatus.NOT_FOUND,"site error code 테스트");

    private HttpStatus httpStatus;
    private String errorMessage;

    SiteErrorCode(HttpStatus httpStatus, String errorMessage) {
        this.httpStatus = httpStatus;
        this.errorMessage = errorMessage;
    }

}

 

 

generateErrorCodeResponseExample 메서드는 ErrorCode enum의 HTTP 상태 코드와 에러 코드의 이름을 가져와서 에러 응답 예시인 ExampleHolder 객체를 생성합니다. 그리고  ExampleHolder 객체를 ApiResponse에 추가합니다.

 

간단히 말해, 이 메서드는:

  1. ErrorCode enum에서 모든 오류 코드를 가져옴
  2. 각 오류 코드에 대해 예시(ExampleHolder 객체)를 생성함
  3. 오류 코드에 해당하는 HTTP 상태 코드별로 그룹화
  4. 이 정보를 ApiResponse 객체에 추가하여, Swagger 문서에 해당 오류 응답 예시가 포함시킴
     - API 문서에서 API가 반화할 수 있는 응답( ex) 200 OK, 404 Not Found)인 ApiResponse 객체를 정의하여 사용한다는 의미

 

addExamplesToResponse 메서드

private void addExamplesToResponses(ApiResponses responses,
                                    Map<Integer, List<ExampleHolder>> 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("application/json", mediaType);
                apiResponse.setContent(content);
                responses.addApiResponse(String.valueOf(status), apiResponse);
            }
    );
}

 

 

addExamplesToResponses 메서드는 ExampleHolder 객체를 사용하여 추가적으로 커스텀된 ApiResponses를 생성하고, 이를 Swagger 문서에서 사용될 ApiResponse에 추가합니다. 이때, ApiResponse는 실제 HTTP 응답이 아닌, Swagger API 문서에 각 API가 반환할 수 있는 응답 예시를 정의하는 객체입니다.

 

 

 

getSwaggerExample 메서드

private Example getSwaggerExample(Enum<?> errorCodeEnum) {
    // Enum을 ErrorCode로 캐스팅
    ErrorCode errorCode = (ErrorCode) errorCodeEnum;

    // ResponseError 객체 생성
    ResponseError responseError = ResponseError.builder()
            .httpStatusCode(errorCode.getHttpStatus())
            .errorMessage(errorCode.getErrorMessage())
            .build();

   Example example =new Example();
   example.setValue(responseError);

   return example;

}

getSwaggerExample 메서드는 ErrorCode enum 값을 받아서, 그에 대한 예시 응답 객체를 만듭니다.

그러면 아래와 같이 Swagger 문서에 오류 응답 예시를 생성할 수 있습니다.

 

 

결과적으로 아래와 같이 에러 코드 문서화를 할 수 있습니다

 

 

학습을 위해 작성된 글입니다. 잘못된 내용이 발견되면 즉각 수정하겠습니다

감사합니다 

 

출처 :

https://inpa.tistory.com/entry/JAVA-☕-누구나-쉽게-배우는-Reflection-API-사용법#reflection_api_기법

https://devnm.tistory.com/29

 

[스프링] spring swagger 같은 코드 여러 에러 응답 예시 만들기

[스프링] error code 도메인 별 분리하기 두둥 프로젝트에서는 처리중에 에러가 발생할경우 RuntimeException 을 상속받은 DuDoongException 에서 다시 상속받아서 코드별 에러클래스를 만들고 있다. @Getter @A

devnm.tistory.com

https://leeeeeyeon-dev.tistory.com/92

 

[Packy] Swagger @ApiResponses를 커스텀 어노테이션으로 대체하기

1. @ApiResponses의 한계 Swagger에서 정상 응답값과 함께 에러 응답값도 명시해주어야 클라이언트 단에서도 에러 응답값에 대한 예외 처리를 할 수 있다. Swagger에서는 기본적으로 아래와 같이 @ApiRespon

leeeeeyeon-dev.tistory.com