본문 바로가기
Backend

[spring swagger로 에러문서화하기] 에러를 문서로 만들기 (2/2)

by 뜨거운 개발자 2024. 9. 28.

일단 이전에 에러를 어떻게 만들었고 global handler를 어떻게 만들었는지를 이전 게시물을 통해 확인하고 이 글을 읽도록 하자.

https://haward.tistory.com/250

 

[spring swagger로 에러문서화하기] 에러 형식 정의 및 handler 정의 (1/2)

시작하며공용 에러를 정의하고 스웨거를 통해 에러에 대해서 문서화를 하는 방법에 대해서 이야기 하도록 하겠다.이 게시물은 공통으로 사용할 에러의 형식을 만들어서 설정하는 부분을 먼저

haward.tistory.com

 

이전 두 게시물에서 API의 에러를 처리하는 방법과 공통 에러 형식을 정의하는 방법을 다루었다. 이제는 Swagger를 이용해서 정의된 에러를 문서화하는 방법에 대해 알아보겠다.

 

0. 스웨거를 커스텀 하려면

스웨거를 커스텀할 수있는부분은 OperationCustomizer 이다. 
handlerMethod 로 api의 메소드를 받아올 수 있고,
operation 은 문서로 적힐 정보에 대한 객체이다.

https://github.com/OAI/OpenAPI-Specification/blob/3.1.0/versions/3.1.0.md#operation-object

 

OpenAPI-Specification/versions/3.1.0.md at 3.1.0 · OAI/OpenAPI-Specification

The OpenAPI Specification Repository. Contribute to OAI/OpenAPI-Specification development by creating an account on GitHub.

github.com

 

 

{
  "tags": [
    "pet"
  ],
  "summary": "Updates a pet in the store with form data",
  "operationId": "updatePetWithForm",
  "requestBody": {
    //생략
  },
  "responses": {
    "200": {
      "description": "Pet updated.",
      "content": {
        "application/json": {},
        "application/xml": {}
      }
    },
    "405": {
      "description": "Method Not Allowed",
      "content": {
        "application/json": {},
        "application/xml": {}
      }
    }
  },
  // 지워줄 부분임
  "security": [
    {
      "petstore_auth": [
        "write:pets",
        "read:pets"
      ]
    }
  ]
}

operation 객체 예시 ( 기본 pet store 예시에서 가져왔다. https://petstore.swagger.io/ )

스웨거의 문서에 대한 정보는 index html을 정볼받은후에 서버로 api대한 정보들을 콜해서 받아오는 형식이다.

스웨거에서 네트워크 탭을 킨 후 새로고침을 하면 저런 형태의 오퍼레이션들을 json으로 받아온다.

위 json 형식의 api가 그려진 예시

 

이제 우리는 어노테이션과 리플렉션을 사용해서 별도의 example/{domain} 형식의 api 를 만든뒤에 커스텀 어노테이션을 에러코드 정보와 함께 기술해서, OperationCustomizer 에서 해당 커스텀 어노테이션이 붙은 api라면 Operation 정보에 응답 코드와 그 예시들을 보여줄 것이다.

 

0.1 스웨거 타입 분석

https://github.com/OAI/OpenAPI-Specification/blob/3.1.0/versions/3.1.0.md#response-object

 

OpenAPI-Specification/versions/3.1.0.md at 3.1.0 · OAI/OpenAPI-Specification

The OpenAPI Specification Repository. Contribute to OAI/OpenAPI-Specification development by creating an account on GitHub.

github.com

 

 

우리가 커스텀 하게될 부분은

Responses 안에 있는 Response 객체이다.

공식 문서에 의하면 Responses 클래스 안에는 Response 클래스 형식을 가지는 필드들이있다.

// Operation 중 responses 필드
"responses": {
    "200": { // Response
      "description": "Pet updated.",
      "content": {
        "application/json": {},
        "application/xml": {}
      }
    },
    "405": { // Response
      "description": "Method Not Allowed",
      "content": {
        "application/json": {},
        "application/xml": {}
      }
    }
  },
"200" : <Response>
"400" : <Response>

위와 같은 형식이다.

content 안에 application/json 형식안에 있는 객체는 Media Type Object 이다

 

 

https://github.com/OAI/OpenAPI-Specification/blob/3.1.0/versions/3.1.0.md#media-type-object

 

OpenAPI-Specification/versions/3.1.0.md at 3.1.0 · OAI/OpenAPI-Specification

The OpenAPI Specification Repository. Contribute to OAI/OpenAPI-Specification development by creating an account on GitHub.

github.com

 

 

// Media Type Object
{
  "application/json": {
    "schema": {
         "$ref": "#/components/schemas/Pet"
    },
    "examples": {
      "cat" : {
        "summary": "An example of a cat",
        "value": 
          {
            "name": "Fluffy",
            "petType": "Cat",
            "color": "White",
            "gender": "male",
            "breed": "Persian"
          }
      },
      "dog": {
        "summary": "An example of a dog with a cat's name",
        "value" :  { 
          "name": "Puma",
          "petType": "Dog",
          "color": "Black",
          "gender": "Female",
          "breed": "Mixed"
        }
      }
    }
  }
}

Media Type Object 안에

examplse 안에는 Example Object 가 올수 있는데,

https://github.com/OAI/OpenAPI-Specification/blob/3.1.0/versions/3.1.0.md#example-object

 

OpenAPI-Specification/versions/3.1.0.md at 3.1.0 · OAI/OpenAPI-Specification

The OpenAPI Specification Repository. Contribute to OAI/OpenAPI-Specification development by creating an account on GitHub.

github.com

 

우린 summary 와, value 값을 도메인별 ErrorCode enum 에서 가져와서 예시로 적어줄 것이다

사진을 보면 좀더 이해하기에 쉬울것같다.

 

1. API 경로별로 에러 정의하기

우리는 각 API 경로에서 발생할 수 있는 에러를 명확히 문서화하기 위해 커스텀 어노테이션을 사용하여 각 컨트롤러 메소드에 적용할 수 있도록 한다. 이를 통해 Swagger 문서에 자동으로 에러 코드와 에러 응답 예시가 추가된다.

 

1.1. 커스텀 어노테이션 생성

먼저, 각 메소드에서 발생할 수 있는 에러 코드를 정의할 수 있도록 커스텀 어노테이션을 생성한다.

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiErrorCodeExample {
    Class<? extends BaseErrorCode> value();  // BaseErrorCode 타입을 확장한 에러 코드만 받음
}

ApiErrorCodeExample 어노테이션을 만들고, 안에 value는 BassErrorCode 를 확장시킨 타입의 Class만 받도록 하였다.

1.2. 컨트롤러에 어노테이션 적용

컨트롤러 메소드에 이 어노테이션을 적용하면, Swagger는 해당 API에서 발생할 수 있는 에러 코드와 그 예시를 문서화한다.

@GetMapping("/info")
@Operation(summary = "워크스페이스 정보 조회")
@ApiErrorCodeExample(WorkspaceErrorCodes.class)  // 여기서 에러 코드 정의를 적용
public WorkspaceInfo getInfo() {
    return workspaceService.getWorkspaceInfo();
}

 

위 예시에서 /info 경로는 WorkspaceErrorCodes에 정의된 에러 코드들을 사용할 수 있다. Swagger 문서에서는 이 API에 대한 에러 응답을 문서화할 것이다.

 

2. Swagger 설정과 연동

Swagger에서 에러 응답 예시를 문서화하려면, OperationCustomizer를 사용하여 메소드에 적용된 ApiErrorCodeExample 어노테이션을 기반으로 응답 예시를 추가하는 커스텀 설정을 구성해야 한다.

 

2.1. OperationCustomizer 설정

Swagger에서 OperationCustomizer를 이용해 API 문서를 커스텀할 수 있다.

OperationCustomizer는 API 문서가 생성될 때 특정 정보를 수정하거나 추가할 수 있도록 도와주는데, 

여기서는 ApiErrorCodeExample 어노테이션을 적용한 메소드에서 에러 코드를 읽고, 그에 따른 응답 예시를 Swagger에 반영하는 과정을 설정한다.

@Bean
public OperationCustomizer customize() {
    return (operation, handlerMethod) -> {
        ApiErrorCodeExample apiErrorCodeExample = handlerMethod.getMethodAnnotation(ApiErrorCodeExample.class);
        // ApiErrorCodeExample 어노테이션이 적용된 메소드에 대해 응답 예시를 설정
        if (apiErrorCodeExample != null) {
            generateErrorCodeResponseExample(operation, apiErrorCodeExample.value());
        }
        return operation;
    };
}

여기서 ApiErrorCodeExample 어노테이션이 달린 메소드를 확인한 후, 해당 에러 코드를 기반으로 응답 예시를 생성하는 작업을 진행한다.

위처럼 리플랙션을 사용해서 해당메서드의 ApiErrorCodeExample 어노테이션이 달려있다면 스웨거 예시 응답값의 커스텀을 진행한다. 

중요한점은 apiErrorCodeExample.value()  BaseErrorCode 타입의 예시를 가져올 수 있다는 점이다.

2.2. 에러 코드 응답 예시 생성

이제 generateErrorCodeResponseExample 메서드를 통해 각 에러 코드와 그에 따른 응답 예시를 Swagger에 반영하는 과정을 설정한다.

이 메서드는 각 에러 코드와 상태 코드에 따라 Swagger에서 사용할 예시 응답을 설정한다.

 

private void generateErrorCodeResponseExample(
        Operation operation, Class<? extends BaseErrorCode> type) {
    ApiResponses responses = operation.getResponses();
	// 해당 이넘에 선언된 에러코드들의 목록을 가져옵니다.
    BaseErrorCode[] errorCodes = type.getEnumConstants();
	// 400, 401, 404 등 에러코드의 상태코드들로 리스트로 모읍니다.
    // 400 같은 상태코드에 여러 에러코드들이 있을 수 있습니다.
    Map<Integer, List<ExampleHolder>> statusWithExampleHolders =
            Arrays.stream(errorCodes)
                    .map(
                            baseErrorCode -> {
                                try {
                                    ErrorReason errorReason = baseErrorCode.getErrorReason();
                                    return ExampleHolder.builder()
                                            .holder(
                                                    getSwaggerExample(
                                                            baseErrorCode.getExplainError(),
                                                            errorReason))
                                            .code(errorReason.getStatus())
                                            .name(errorReason.getCode())
                                            .build();
                                } catch (NoSuchFieldException e) {
                                    throw new RuntimeException(e);
                                }
                            })
                    .collect(groupingBy(ExampleHolder::getCode));
	// response 객체들을 responses 에 넣습니다.
    addExamplesToResponses(responses, statusWithExampleHolders);
}
//ExampleHolder
@Getter
@Builder
public class ExampleHolder {
	// 스웨거의 Example 객체입니다. 위 스웨거 분석의 Example Object 참고.
    private Example holder;
    private String name;
    private int code;
}
//
private Example getSwaggerExample(String value, ErrorReason errorReason) {
//ErrorResponse 는 클라이언트한 실제 응답하는 공통 에러 응답 객체입니다.
    ErrorResponse errorResponse = new ErrorResponse(errorReason, "요청시 패스정보입니다.");
    Example example = new Example();
    example.description(value);
    example.setValue(errorResponse);
    return example;
}

코드 설명:

  1. BaseErrorCode[] errorCodes = type.getEnumConstants();
    여기서 BaseErrorCode를 구현한 에러 코드 enum을 가져온다.. 각 에러 코드는 상태 코드, 에러 코드, 설명을 가지고 있다.
  2. Map<Integer, List<ExampleHolder>> statusWithExampleHolders
    각 상태 코드와 그에 해당하는 에러 응답 예시를 매핑한다. 예를 들어, 400, 401 등의 상태 코드에 여러 에러 코드가 포함될 수 있으므로 이를 리스트로 묶는다.
  3. addExamplesToResponses
    생성된 예시 객체를 ApiResponses에 추가한다.

ExampleHolder를 만들어서 Swagger의 Example을 담는 객체를 만들어준다.

 

스웨거는 Example 객체를 이용해서 예시를 만들어낸다.

그래서 따라서 스웨거에 직접적으로 해당 값을 이용해서 넣어주면 된다.

2.3. 응답 예시를 Swagger에 추가 (스웨거 예시 응답값 커스텀)

addExamplesToResponses 메서드는 상태 코드별로 생성된 에러 응답 예시를 Swagger 문서에 추가한다.

이 메서드는 상태 코드(status), 예시 응답(ExampleHolder 리스트)을 기반으로 ApiResponse 객체를 생성하고, 이를 Swagger 응답에 추가한다. MediaType 객체를 통해 JSON 형식으로 응답 예시를 추가하고, addApiResponse 메서드를 통해 상태 코드에 맞는 응답을 설정한다.

 

private void addExamplesToResponses(ApiResponses responses, Map<Integer, List<ExampleHolder>> statusWithExampleHolders) {
    statusWithExampleHolders.forEach((status, examples) -> {
        Content content = new Content();
        MediaType mediaType = new MediaType();
        ApiResponse apiResponse = new ApiResponse();

        examples.forEach(exampleHolder -> mediaType.addExamples(exampleHolder.getName(), exampleHolder.getHolder()));

        content.addMediaType("application/json", mediaType);
        apiResponse.setContent(content);
        responses.addApiResponse(status.toString(), apiResponse);  // 상태 코드별 응답 추가
    });
}

 

2.4. Example 객체 생성

getSwaggerExample 메서드는 에러 코드와 그 설명을 기반으로 ErrorResponse 객체를 생성해 Swagger에 사용할 예시 응답을 만든다. 이 과정에서 실제 API가 반환할 에러 응답 형식을 미리 정의하게 된다.

Swagger에 표시될 에러 응답 예시는 Example 객체를 통해 생성된다.

private Example getSwaggerExample(String value, ErrorReason errorReason) {
    ErrorResponse errorResponse = new ErrorResponse(errorReason, "요청시 패스정보입니다.");
    Example example = new Example();
    example.description(value);
    example.setValue(errorResponse);  // 에러 응답 예시 설정
    return example;
}

 

여기서 ErrorResponse는 에러 응답에 대한 공통 포맷으로, 클라이언트에게 반환될 정보를 담고 있다. 이를 Swagger 문서에서 예시로 사용할 수 있다.

3. 에러 코드와 응답 형식 예시

3.1. 에러 코드 정의

BaseErrorCode 인터페이스를 구현하여 각 도메인에서 발생할 수 있는 에러 코드를 정의한다. 예를 들어, WorkspaceErrorCodes라는 에러 코드를 다음과 같이 정의할 수 있다.

@Getter
@AllArgsConstructor
public enum WorkspaceErrorCodes implements BaseErrorCode {
    WORKSPACE_NOT_FOUND(404, "WORKSPACE_404_1", "워크스페이스를 찾을 수 없습니다."),
    INVALID_ACCESS_TOKEN(401, "AUTH_401_1", "잘못된 액세스 토큰입니다.");

    private final Integer status;
    private final String code;
    private final String reason;

    @Override
    public ErrorReason getErrorReason() {
        return ErrorReason.builder().reason(reason).code(code).status(status).build();
    }

    @Override
    public String getExplainError() throws NoSuchFieldException {
        return this.reason;
    }
}

 

3.2. 응답 예시 형식

Swagger 문서에서 제공될 에러 응답 예시는 다음과 같은 형식으로 표시된다.

 

{
  "status": 404,
  "code": "WORKSPACE_404_1",
  "reason": "워크스페이스를 찾을 수 없습니다.",
  "timestamp": "2024-09-19T11:24:42.948392",
  "path": "/api/v1/workspace/info"
}

 

이와 같은 형식의 응답 예시를 통해 클라이언트는 해당 API 경로에서 발생할 수 있는 에러와 그에 따른 응답 데이터를 쉽게 확인할 수 있다.

 

 

4. 컨트롤러에 적용하기

이제 컨트롤러에 붙혀줄 ApiErrorExceptionsExample 어노테이션을 만들어주자.

package com.groomiz.billage.global.anotation;

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

import com.groomiz.billage.global.interfaces.SwaggerExampleExceptions;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiErrorExceptionsExample {
	Class<? extends SwaggerExampleExceptions> value();
}

에러 example을 위해 이 어노테이션 도 만들어주고

인터페이스로 SwaggerExampleExceptions 를 만들어준다.

package com.groomiz.billage.global.interfaces;

import com.groomiz.billage.global.anotation.ExceptionDoc;

@ExceptionDoc
public interface SwaggerExampleExceptions {
}

4.1 ApiErrorExceptionsExample과 ApiErrorCodeExample의 차이점?

지금 헷갈릴 수 있다. 왜 굳이 2개를 만드는가?

ApiErrorExceptionsExample 의 경우는 각 컨트롤러 경로 또는 각 상황에 대해서 일어날 수 있는 오류들을 모아서 보여줄 때 사용한다고 생각하면 되고, ApiErrorCodeExample의 경우는 각 도메인별로 일어날 수 있는 오류들을 정의한다고 생각하면 된다.

 

4.2 문서화를 위한 에러 문서 클래스 정의하기 (ApiErrorExceptionsExample)

이제 실제 문서화를 위한 코드를 작성해보자.

이건 도메인 별이 아닌, 각 경로별로 이렇게 적어준 클래스를 붙혀준다고 생각하면 된다.

@ExceptionDoc
public class WorkspaceGetExceptionDocs implements SwaggerExampleExceptions {
    @ExplainError("유저가 Workspace 에 참여해 있지 않은 경우 발생합니다.")
    public GlobalCodeException 워크스페이스_없음 = new WorkSpaceException(WorkSpaceErrorCode.NO_WORKSPACE);
    @ExplainError("엑세스 토큰이 만료된 경우 발생합니다.")
    public GlobalCodeException 토큰_만료 = new AuthException(AuthErrorCode.TOKEN_EXPIRED);
    @ExplainError("엑세스 토큰이 유효하지 않은 경우 발생합니다.")
    public GlobalCodeException 토큰_유효하지_않음 = new AuthException(AuthErrorCode.INVALID_TOKEN);
    @ExplainError("엑세스 토큰이 없는 경우 발생합니다.")
    public GlobalCodeException 토큰_없음 = new AuthException(AuthErrorCode.ACCESS_TOKEN_NOT_EXIST);
}

 

ExceptionDoc어노테이션도 문서화를 위해 나타내주자.

package com.groomiz.billage.global.anotation;

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

import org.springframework.core.annotation.AliasFor;
import org.springframework.stereotype.Component;

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface ExceptionDoc {
    @AliasFor(annotation = Component.class)
    String value() default "";
}

 

4.3 컨트롤러 사용 예시 (ApiErrorExceptionsExample)

@GetMapping("/member")
@Operation(summary = "워크스페이스 멤버 조회")
@ApiErrorExceptionsExample(WorkspaceGetExceptionDocs.class)
public List<UserInfoDto> getMember() {
    return workspaceService.getMemebers();
}

@GetMapping("/info")
@Operation(summary = "워크스페이스 정보 조회")
@ApiErrorExceptionsExample(WorkspaceGetExceptionDocs.class)
public WorkspaceInfo getInfo() {
    return workspaceService.getWorkspaceInfo();
}

@PostMapping("/join")
@Operation(summary = "워크스페이스 가입 이전에 초대 받은 유저는 join을 콜하면 가입됩니다.", responses = {
    @ApiResponse(responseCode = "200", description = "워크스페이스 가입 성공",
       content = @Content(schema = @Schema(implementation = WorkspaceInfo.class))),
})
@ApiErrorExceptionsExample(WorkspaceJoinExceptionDocs.class)
public WorkspaceInfo join() {
    return workspaceService.joinSpace();
}

이런식으로 각 경로에 해당하는 어노테이션을 붙혀주면 된다.

 

4.4 GlobalErrorCode 만들기(ApiErrorCodeExample)

이전에 Auth에서 발생할 수 있는 에러를 예시로 만들었었지만, 이번에는 전역적으로 발생할 수 있는 에러를 정의한 클래스인  GlobalErrorCode를 만들어 보자.

이전에 만들었던 BaseErrorCode 인터페이스를 구현해서 다음과 같이 만들었다.

package com.groomiz.billage.global.exception;

import static com.groomiz.billage.global.consts.BillageStatic.*;

import java.lang.reflect.Field;
import java.util.Objects;

import com.groomiz.billage.global.anotation.ExplainError;
import com.groomiz.billage.global.dto.ErrorReason;

import lombok.AllArgsConstructor;
import lombok.Getter;

/**
 * 글로벌 관련 예외 코드들이 나온 곳입니다. 인증 , global, aop 종류등 도메인 제외한 exception 코드들이 모이는 곳입니다. 도메인 관련 Exception
 * code 들은 도메인 내부 exception 패키지에 위치시키면 됩니다.
 */
@Getter
@AllArgsConstructor
public enum GlobalErrorCode implements BaseErrorCode {
	@ExplainError("백엔드에서 예시로만든 에러입니다. 개발용")
	EXAMPLE_NOT_FOUND(NOT_FOUND, "EXAMPLE_404_1", "예시를 찾을 수 없는 오류입니다."),

	@ExplainError("밸리데이션 (검증 과정 수행속 ) 발생하는 오류입니다.")
	ARGUMENT_NOT_VALID_ERROR(BAD_REQUEST, "GLOBAL_400_1", "검증 오류"),

	@ExplainError("500번대 알수없는 오류입니다. 서버 관리자에게 문의 주세요")
	INTERNAL_SERVER_ERROR(INTERNAL_SERVER, "GLOBAL_500_1", "서버 오류. 관리자에게 문의 부탁드립니다."),

	OTHER_SERVER_BAD_REQUEST(BAD_REQUEST, "FEIGN_400_1", "다른 서버로 한 요청이 Bad Request 입니다."),
	OTHER_SERVER_UNAUTHORIZED(BAD_REQUEST, "FEIGN_400_2", "다른 서버로 한 요청이 Unauthorized 입니다."),
	OTHER_SERVER_FORBIDDEN(BAD_REQUEST, "FEIGN_400_3", "다른 서버로 한 요청이 Forbidden 입니다."),
	OTHER_SERVER_EXPIRED_TOKEN(BAD_REQUEST, "FEIGN_400_4", "다른 서버로 한 요청이 Expired Token 입니다."),
	OTHER_SERVER_NOT_FOUND(BAD_REQUEST, "FEIGN_400_5", "다른 서버로 한 요청이 Not Found 입니다."),
	OTHER_SERVER_INTERNAL_SERVER_ERROR(
		BAD_REQUEST, "FEIGN_400_6", "다른 서버로 한 요청이 Internal Server Error 입니다."),
	SECURITY_CONTEXT_NOT_FOUND(500, "GLOBAL_500_2", "SecurityContext를 찾을 수 없습니다."),

	BAD_FILE_EXTENSION(BAD_REQUEST, "FILE_400_1", "파일 확장자가 잘못 되었습니다."),
	TOO_MANY_REQUEST(429, "GLOBAL_429_1", "과도한 요청을 보내셨습니다. 잠시 기다려 주세요.");
	private final Integer status;
	private final String code;
	private final String reason;

	@Override
	public ErrorReason getErrorReason() {
		return ErrorReason.builder().reason(reason).code(code).status(status).build();
	}

	@Override
	public String getExplainError() throws NoSuchFieldException {
		Field field = this.getClass().getField(this.name());
		ExplainError annotation = field.getAnnotation(ExplainError.class);
		return Objects.nonNull(annotation) ? annotation.value() : this.getReason();
	}
}

이후 GlobalErrorCode 를 정의하면 다음과 같이 사용할 수 있다고 보면 된다.

아직 모든 부분을 구현하진 않았지만 현재 하고자 하는 건 오류를 다음과 같이 보여줄 수 있는 형식을 만들고자 하는 것이다.

 

4.5 에러를 위한 컨트롤러 만들기 (ApiErrorCodeExample)

package com.groomiz.billage.global.document;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.groomiz.billage.global.anotation.ApiErrorCodeExample;
import com.groomiz.billage.global.exception.GlobalErrorCode;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;

@RestController
@RequestMapping("/v1/example")
@RequiredArgsConstructor
@Tag(name = "Exception Document", description = "예제 에러코드 문서화")
public class ExampleController {
    @GetMapping("/global")
    @ApiErrorCodeExample(GlobalErrorCode.class)
    @Operation(summary = "글로벌 (aop, 서버 내부 오류등)  관련 에러 코드 나열")
    public void example() {
    }
}

이렇게 사용을 해주면 된다.

 

 

5. Swagger Config 최종본

이전의 SwaggerConfig를 결국 변경하면 다음과 같은 모습이 된다.

@Configuration
public class SwaggerConfig {
    @Bean
    public OpenAPI openAPI() {
       Info info = new Info()
          .title("빌리지 API Document")
          .version("1.0")
          .description(
             "환영합니다! [발리지](https://example.com)는 서울과학기술대학교 강의실을 빌리기 위해서 위해 만들어진 플랫폼입니다. 이 API 문서는 빌리지의 API를 사용하는 방법을 설명합니다.\n")
          .contact(new io.swagger.v3.oas.models.info.Contact().email("billage.official@gmail.com"));

       String jwtScheme = "jwtAuth";
       SecurityRequirement securityRequirement = new SecurityRequirement().addList(jwtScheme);
       Components components = new Components()
          .addSecuritySchemes(jwtScheme, new SecurityScheme()
             .name("Authorization")
             .type(SecurityScheme.Type.HTTP)
             .in(SecurityScheme.In.HEADER)
             .scheme("Bearer")
             .bearerFormat("JWT"));

       return new OpenAPI()
          .addServersItem(new Server().url("http://localhost:8080"))
          .components(new Components())
          .info(info)
          .addSecurityItem(securityRequirement)
          .components(components);
    }
    	/**
	 * BaseErrorCode 타입의 이넘값들을 문서화 시킵니다. ExplainError 어노테이션으로 부가설명을 붙일수있습니다. 필드들을 가져와서 예시 에러 객체를
	 * 동적으로 생성해서 예시값으로 붙입니다.
	 */
	private void generateErrorCodeResponseExample(
		Operation operation, Class<? extends BaseErrorCode> type) {
		ApiResponses responses = operation.getResponses();

		BaseErrorCode[] errorCodes = type.getEnumConstants();

		Map<Integer, List<ExampleHolder>> statusWithExampleHolders =
			Arrays.stream(errorCodes)
				.map(
					baseErrorCode -> {
						try {
							ErrorReason errorReason = baseErrorCode.getErrorReason();
							return ExampleHolder.builder()
								.holder(
									getSwaggerExample(
										baseErrorCode.getExplainError(),
										errorReason))
								.code(errorReason.getStatus())
								.name(errorReason.getCode())
								.build();
						} catch (NoSuchFieldException e) {
							throw new RuntimeException(e);
						}
					})
				.collect(groupingBy(ExampleHolder::getCode));

		addExamplesToResponses(responses, statusWithExampleHolders);
	}

	/**
	 * SwaggerExampleExceptions 타입의 클래스를 문서화 시킵니다. SwaggerExampleExceptions 타입의 클래스는 필드로
	 * GlobalCodeException 타입을 가지며, GlobalCodeException 의 errorReason 와,ExplainError 의 설명을
	 * 문서화시킵니다.
	 */
	private void generateExceptionResponseExample(Operation operation, Class<?> type) {
		ApiResponses responses = operation.getResponses();

		// ----------------생성
		Object bean = applicationContext.getBean(type);
		Field[] declaredFields = bean.getClass().getDeclaredFields();
		Map<Integer, List<ExampleHolder>> statusWithExampleHolders =
			Arrays.stream(declaredFields)
				.filter(field -> field.getAnnotation(ExplainError.class) != null)
				.filter(field -> field.getType() == GlobalCodeException.class)
				.map(
					field -> {
						try {
							GlobalCodeException exception =
								(GlobalCodeException)field.get(bean);
							ExplainError annotation =
								field.getAnnotation(ExplainError.class);
							String value = annotation.value();
							ErrorReason errorReason = exception.getErrorReason();
							return ExampleHolder.builder()
								.holder(getSwaggerExample(value, errorReason))
								.code(errorReason.getStatus())
								.name(field.getName())
								.build();
						} catch (IllegalAccessException e) {
							throw new RuntimeException(e);
						}
					})
				.collect(groupingBy(ExampleHolder::getCode));

		// -------------------------- 콘텐츠 세팅 코드별로 진행
		addExamplesToResponses(responses, statusWithExampleHolders);
	}

	/**
	 * : 주어진 ErrorReason을 사용하여 ErrorResponse 객체를 생성합니다. 이 객체는 예시 응답에 포함됩니다.
	 */
	private Example getSwaggerExample(String value, ErrorReason errorReason) {
		ErrorResponse errorResponse = new ErrorResponse(errorReason, "요청시 패스정보입니다.");
		Example example = new Example();
		example.description(value);
		example.setValue(errorResponse);
		return example;
	}

	/**
	 * 상태 코드별로 예시를 API 응답에 추가합니다.
	 */
	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(status.toString(), apiResponse);
			});
	}

	/**
	 * API 메서드 및 클래스에 정의된 Tag 어노테이션을 가져와 태그 이름을 추출하고 리스트에 추가 합니다.
	 */
	private static List<String> getTags(HandlerMethod handlerMethod) {
		List<String> tags = new ArrayList<>();

		Tag[] methodTags = handlerMethod.getMethod().getAnnotationsByType(Tag.class);
		List<String> methodTagStrings =
			Arrays.stream(methodTags).map(Tag::name).collect(Collectors.toList());

		Tag[] classTags = handlerMethod.getClass().getAnnotationsByType(Tag.class);
		List<String> classTagStrings =
			Arrays.stream(classTags).map(Tag::name).collect(Collectors.toList());
		tags.addAll(methodTagStrings);
		tags.addAll(classTagStrings);
		return tags;
	}

    
}

 

결국 스웨거에는 다음과 같은 화면으로 오류가 나오게 됩니다.

 

 

이글은 아래를 참고해 정리로 만들어진 글 입니다. 

 

https://devnm.tistory.com/29

 

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

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

devnm.tistory.com

https://github.com/Gosrock/DuDoong-Backend/tree/dev

 

GitHub - Gosrock/DuDoong-Backend: 모두를 위한 새로운 공연 라이프, 두둥!

모두를 위한 새로운 공연 라이프, 두둥! Contribute to Gosrock/DuDoong-Backend development by creating an account on GitHub.

github.com

 

 

 

728x90