개발/Library

[스웨거] Swagger Error Example Customize

prime9999 2024. 7. 29. 18:29
반응형

프로젝트 기한이 2달 밖에 되지않아 Swagger Error Document작업을 하지 못했는데

작업 방법을 찾아보다 여러 블로그 글들 중 좋은 포스팅이 보여 참고 하여 작업을 시작했다.

 

 

https://devnm.tistory.com/29

 

블로그 내용만으로는 구현하기에는 내 능지가 이분의 능지를 따라가기 어렵다고 판단 했으나 오픈소스코드를 제공해주셔서 참고하며 잘 마무리 했다.

 

의도

 

참고한 블로그 저자의 의도는 어노테이션과 리플렉션을 활용하여 별도 커스텀 에러코드 어노테이션을 통해 에러코드 정보들을 기술하여 제공하려고 한다.

 

여기서 리플렉션이란?

리플렉션이란?

리플렉션(Reflection)은 자바에서 매우 강력한 기능으로, 프로그램이 실행 중인 동안에 자신의 구조를 검사하고, 수정할 수 있는 능력을 의미한다. 리플렉션을 사용하면 클래스, 인터페이스, 필드, 메소드 등에 대한 정보를 런타임에 조사하고, 이들에 접근하거나 수정할 수 있다.

 

말 그대로 스웨거 도큐먼트 접근 시 메소드 단위에서 내가 커스텀한 에러코드 어노테이션들이 선언 되어 있다면 해당 Example들을 보여줄 수 있다는 것이다.

 

좀 더 상세한 내용은 위 내가 참고한 블로그 저자 이찬진님의 글을 참고 하길 바란다.

 

1. Swagger Config file 설정.

Swagger 문서에서 모델(데이터 객체)의 표현 방식을 Customize하기 위해서 ModelResolver 등록.

 

@Bean public ModelResolver modelResolver(ObjectMapper objectMapper) {
    return new ModelResolver(objectMapper); 
}

 

OperationCustomizer를 커스텀 하고 해당 어노테이션이 붙은 API라면 Operation 정보에 응답코드와 예시들을 적절히 보여주도록 한다.

 

@Bean public OperationCustomizer customize() {
return (Operation operation, HandlerMethod handlerMethod) -> {
    DisableSwaggerSecurity methodAnnotation = handlerMethod.getMethodAnnotation(DisableSwaggerSecurity.class); 
    ApiErrorExceptionsExample apiErrorExceptionsExample = handlerMethod.getMethodAnnotation(ApiErrorExceptionsExample.class); 
    ApiErrorCodeExample apiErrorCodeExample = handlerMethod.getMethodAnnotation(ApiErrorCodeExample.class); List<String> tags = getTags(handlerMethod); // DisableSecurity 어노테이션있을시 스웨거 시큐리티 설정 삭제 
    if (methodAnnotation != null) { 
        operation.setSecurity(Collections.emptyList()); 
    } // 태그 중복 설정시 제일 구체적인 값만 태그로 설정 
    if (!tags.isEmpty()) { 
        operation.setTags(Collections.singletonList(tags.get(0))); 
    } 
    // ApiErrorExceptionsExample 어노테이션 단 메소드 적용 
    //실제 메소드 단위에서 어노테이션 설정시 
    if (apiErrorExceptionsExample != null) { 
        generateExceptionResponseExample(operation, apiErrorExceptionsExample.value()); 
    } 
    // ApiErrorCodeExample 어노테이션 단 메소드 적용 
    //에러 도큐먼트로서 전체적인 에러코드들을 보여주기 위한 설정. 
    if (apiErrorCodeExample != null) { 
        generateErrorCodeResponseExample(operation, apiErrorCodeExample.value()); 
    } return operation; }; 
}

내가 참고한 프로젝트에선 해당 메소드들을 사용한 곳이 안보여 엄청 고생했는데

GroupedOpenApi 내에 addOperationCustomizer 메소드에 커스텀한 정보를 set 해줘야 한다.

@Bean public GroupedOpenApi exampleGroup() { 
    return GroupedOpenApi.builder() .group("Examples") 
                .pathsToMatch("/**/examples/**").addOperationCustomizer(customize())<= 내가 추가한 부분 .build(); 
    }

1-1. generateExceptionResponseExample (API 단위 에러문서)

SwaggerExampleExceptions 타입의 클래스를 문서화 시킨다.

타입의 클래스를 문서화 시키고 SwaggerExampleExceptions 타입의 클래스는 필드로

DivisionCodeException<-(Project Custom Exception Class) 타입을 가지며, DivisionCodeException 의 errorReason 와,ExplainError 의 설명을

문서화시키도록 한다.

 

 

1-2. addExamplesToResponses

Responses에 response 데이터 추가.

private void addExamplesToResponses( ApiResponses responses, Map<Integer, List<ExampleHolder>> statusWithExampleHolders) { 
    statusWithExampleHolders.forEach( (status, v) -> { 
        Content content = new Content(); MediaType mediaType = new MediaType(); // 상태 코드마다 ApiResponse을 생성합니다. 
        ApiResponse apiResponse = new ApiResponse(); // List<ExampleHolder> 를 순회하며, mediaType 객체에 예시값을 추가합니다. 
        
        v.forEach( exampleHolder -> { 
            mediaType.addExamples( exampleHolder.getName(), exampleHolder.getHolder()); }); // ApiResponse 의 Content 에 MediaType을 추가합니다. 
            content.addMediaType("application/json", mediaType); 
            apiResponse.setContent(content); // 상태코드를 key 값으로 responses 에 추가합니다. 
            responses.addApiResponse(status.toString(), apiResponse); }); 
    }

 

1-3. generateErrorCodeResponseExample (ERROR 페이지 문서)

BaseErrorCode 타입의 이넘값들을 문서화 시키며 필드들을 가져와서 예시 에러 객체를 동적으로 생성해서 예시값으로 붙인다.

 

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);
                                    }

1-4. getSwaggerExample

Holder에 담겨질 Example 정보들.

필자는 해당 작업을 진행하기 전에 관련지식이 부족해서 애를 좀 많이 먹었다. 그러다 보니 해당

해당 사항을 적용하기 전에 미리 셋팅되어야할 커스텀 annotation 과

Dto Class 및 인터페이스들을 정리했다.

Package 구성

 

- www - common - swg - customizer <- class - annotation <- annotation - dto <- class - exception <- enum - example <- document page 처리를 위한 controller class

 

Interface

  1. BaseErrorCode
  2. SwaggerExampleExceptions

Class

  1. DivisionCodeException
  2. EnumValuePropertyCustomizer

Annotation

  1. ApiErrorCodeExample
  2. ApiErrorExceptionsExample
  3. DevelopOnlyApi
  4. DisableSwaggerSecurity
  5. ExceptionDoc
  6. ExplainError

Enum

enum의 경우 swg 패키지 하위 exception 패키지에 위치시켰다.

관계

service logic 구성된 docs 하위 class들은 SwaggerExampleExceptions 를 implements 하고 있으며

exception 하위 class들은 DivisionCodeException extend 하고 있다.

  1. docs 하위 <-> SwaggerExampleExceptions : implements
  2. exception 하위 <-> DivisionCodeException : extend
  3. DivisionCodeException <-> RuntimeException : extend
  4. MemberSignUpErrorCode <-> BaseErrorCode : implements

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

2. 커스텀 어노테이션 생성하기

ApiErrorCodeExample 어노테이션을 만들고 BaseErrorCode를 Extend한 Class만 받도록 한다.

예시)

 

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

 

 

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

필자가 참고한 예시는 여기까지 였으나 참고한 블로그 저자의 소스코드를 분석하여 좀 더 작성해보도록 했다. 스웨거 관련 커스텀한 정보들은 swg(swagger)package 를 따로 구성했고 각각 메소드 단위에 doc나 exception 항목들은 service단위에 위치 시켰다.

메소드

- www - common - docs <- 해당 위치 - exception <- 해당 위치 - mapper - service - model

나머지 설정 및 파일

- www - common - swg - annotation - dto - exception - example

3.적용순서

3-1. Swagger 문서 페이지 접속시 OperationCustomizer 를 커스텀 한 customize() 메소드 실행

3-2.Controller에 선언된 API어노테이션 정보를 Search

#@ApiErrorExceptionsExample은 SwaggerExampleExceptions extend한 class만 받는다.
@ApiErrorExceptionsExample(MemberSignUpExceptionDocs.class)

3-3. 선언된 API 어노테이션 내 Doc Class를 따라가 보자 MemberSignUpExceptionDocs

해당 class는 SwaggerExampleExceptions 상속 받고 있고 DivisionCodeException 를 상속 받은

SignupDuplicatePlayerApplException의 Exception을 사용하고 있다.

 

@ExceptionDoc public class MemberSignUpExceptionDocs implements SwaggerExampleExceptions { 

    @ExplainError("회원가입시 이미 선수 신청한 건이 존재하는 경우") 
    public DivisionCodeException SIGNUP_400_1 = SignupDuplicatePlayerApplException.EXCEPTION;
    
}

3-4. SignupDuplicatePlayerApplException은 exception 하위에 구성되어 있다. 해당 Class를 살펴보면 DivisionCodeException 상속받고 있고 부모객체 생성자로 MemberSignUpErrorCode.MEMBER_SIGNUP_DUPLICATE_PLAYER_APPL를

생성하고 있다. DivisionCodeException에서는 BaseErrorCode를 받을수 있다.

 

@Getter 
@AllArgsConstructor 
public class DivisionCodeException extends RuntimeException{ 
    private BaseErrorCode errorCode; 
    
    public ErrorReason getErrorReason() { 
        return this.errorCode.getErrorReason(); 
    } 
    
}

당연히 MemberSignUpErrorCode.MEMBER_SIGNUP_DUPLICATE_PLAYER_APPL enum class에서는 BaseErrorCode를 implements하고 있다.

 

public class SignupDuplicatePlayerApplException extends DivisionCodeException { 

    public static final DivisionCodeException EXCEPTION = new SignupDuplicatePlayerApplException(); 
    
    private SignupDuplicatePlayerApplException() { 
        super(MemberSignUpErrorCode.MEMBER_SIGNUP_DUPLICATE_PLAYER_APPL); 
    } 
    
}

마무리

이렇게 설정이 완료되면 스웨거 exception example이 완성된다. 설명을 적다보니 아직도 부족한 부분이 있는데 필자가 참조한 블로그에서 제공하는 repository를 clone 해서 살펴보는게 더 나을수도 있다.

반응형