Swagger

OpenApiMethodFilter 사용하여 Swagger API 분류하기

kidmillionaire1998 2023. 7. 20. 00:28

문제 상황 

Swagger을 사용하게 되면, GroupedOpenAPI를 builder 패턴으로 생성하여 Bean으로 등록하여, API들을 그룹별로 나눌 수가 있다. 

 

보통, 

pathsToInclude, pathsToExclude

와 같은 메소드를 사용하여, 직접 API의 엔드포인트를 파라미터를 인자로 입력하여, 나누는 경우가 많다.

 

   @Bean
    public GroupedOpenApi SecurityGroupOpenApi() {
        return GroupedOpenApi
                .builder()
                .group("그룹1")
                .addOpenApiCustomiser(buildSecurityOpenApi())
                .pathsToExclude("/api1, /api2")
                .pathsToInclude("/api3, /api4")
                .build();
    }

 말그대로 포함하고 싶은 API들은 pathsToInclude에, 제외하고 싶은 API들은 pathsToExclude의 인자로 하드코딩해주면 된다. 

 

이렇게 GroupedApi를 이용해 Swagger 페이지를 나누게 된 이유는 , 프로젝트를  구현되는 API들이 많아지게 되었고,

토큰 인증 여부에 따라 필요한 API를 그룹별로 나누어 swagger 페이지에 보여주는 것이 좋다고 생각했기 때문이다.  

 

초기에는 위에서 작성한 방법 처럼, pathsToExclude / pathsToMatch을 이용하여 구현을 하였는데, API가 점점 늘어나다보니까, 가독성도 낮아지고, 엔드포인트로만 구분을 하다보니까, 같은 엔드포인트인데도 GET, POST 방식에 따라 구분되는 API들은 위의 방법으로 구분 자체가 불가능하였다. 

 

 

해결 방안 접근 

 현재, 토큰 인증이 필요한 API들은 메서드 위에 @NoAuth를 붙여서 스프링 인터셉터에서 처리하고 있다. 즉, @NoAuth가 포함된 API들은 API들은 인터셉터에서 토큰 검증을 안하고, 해당 어노테이션이 있는 API들만 토큰 검증을 진행한 후 컨트롤러로 요청이 넘어간다. 

    @NoAuth
    @PostMapping("/sign-up")
    public SuccessResponse<UserCreateOutDTO> createUser(@RequestBody @Valid UserCreateInDTO userCreateInDTO){
            ...
    }

 

즉, 현재 API들의 토큰 인증 유무를 결정하는 기준 자체가 @NoAuth라는 커스텀 어노테이션에 따라 구분 되어있기 때문에,  해당 어노테이션의 유무에 따라 GroupedOpenAPI를 build해보면 어떨까 생각이 들었다.

구글링을 해봤는데, pathsToInclude, pathsToExclude로 구분하는 것 밖에 나오지 않아서.. 오픈소스 코드를 직접 찾아보았다. 

 

OpenApiMethodFilter 


@FunctionalInterface
public interface OpenApiMethodFilter {
   
   boolean isMethodToInclude(Method method);

}

공식문서엔 다음과 같이 나와있다. 

 /**
    * Whether the given method should be included in the generated OpenApi definitions. Only methods from classes
    * detected by the relevant loader will be passed to this filter; it cannot be used to load methods that are not
    * annotated with `RequestMethod` or similar mechanisms. Methods that are rejected by this filter will not be
    * processed any further, although methods accepted by this filter may still be rejected by other checks, such as
    * package inclusion checks so may still be excluded from the final OpenApi definition.
    *
    * @param method the method to perform checks against
    * @return whether this method should be used for further processing
    */
    

 

리플렉션 Method 인스턴스가 해당 함수형 인터페이스의 파라미터가 된다. 즉, 필터링의 대상이 된다. 

 

1.  클래스 로더에서 찾은 클래스 내 메소드(리플렉션)들을 가져온다. 

(Only methods from classes detected by the relevant loader will be passed to this filter)

 

2. 메소드 들 중, @RequestMapping, @PostMapping등 같은 어노테이션을 포함한 메소드들만 그 대상이 된다. 

(it cannot be used to load methods that are not annotated with `RequestMethod` or similar mechanisms.) 

 

- 인자로 받은 Method 인스턴스를 이용하여, isMehtodToInclude 메서드를 구현하여, 해당 Method를 포함할지 말지를 결정한다. 

 

- return형은 boolean이며, 메서드 필터에 포함된다면 true를 반환한다. 

 

addOpenApiMethodFilter 

public Builder addOpenApiMethodFilter(OpenApiMethodFilter methodFilter) {
   this.methodFilters.add(methodFilter);
   return this;
}

-  OpenApiMethodFilter을 구현한 인스턴스를 GroupedOpenApi를 Builder 패턴을 이용해 build하는 과정에 추가를 시켜주면 된다. 

 

구현 및 적용 

내가 지금하고 있는 프로젝트에 적용하기에 구현은 어렵지 않았다. Method 인스턴스에 annotation을 가져와 @NoAuth가 있는 지 확인만 해주고 그에 따른 boolean 값을 return 해주면 되었다. 구현은 다음과 같은 메소드를 사용했다. 

 

getAnnotation 

- 자바 리플렉션의 Method 클래스안에 있는, annotation을 추출할 수 있는 메소드이다. 

/**
 * {@inheritDoc}
 * @throws NullPointerException {@inheritDoc}
 * @since 1.5
 */
@Override
public <T extends Annotation> T getAnnotation(Class<T> annotationClass) {
    return super.getAnnotation(annotationClass);
}

 

//@NoAuth가 존재하는 API들을 필터링합니다 (토큰 인증 필요 x)
OpenApiMethodFilter noAuthFilter = ((method)-> {
    if (method.getAnnotation(NoAuth.class) != null)
        return true;
    return false;
    }
);

- Method객체에서 NoAuth.class가 포함되어있지 않으면 , 즉 토큰 인증이 필요한 API 같은 경우만 포함해주는 람다식을 작성하여 구현했다. 

 

이렇게 구현한 OpenApiMethodFilter을 addOpenApiMethodFilter을 사용해 GroupedOpenAPI에 추가하였다. 

//토큰 인증이 필요한 API 모음
@Bean
public GroupedOpenApi SecurityGroupOpenApi() {
    return GroupedOpenApi
            .builder()
            .group("토큰 인증 필요한 API")
            .addOpenApiMethodFilter(authFilter)
            .build();
}

 

다만, 하나의 GroupedOpenApi에 포함이 안된다고해서, 다른 GroupedOpenApi에 배제되진 않기 때문에,

토큰이 있는 GroupedOpenApi에 적용할 필터와  없는 GroupedOpenApi에 적용할 필터 모두 구현을 해야했다. 

다음은 토큰 인증이 필요 없는 Method들만 필터링 해주는 OpenApiMethodFilter이다. 

    OpenApiMethodFilter authFilter = ((method)-> {
        if (method.getAnnotation(NoAuth.class) == null)
            return true;
        return false;
        }
    );

 

이렇게 구현한 OpenApiMethodFilter을 addOpenApiMethodFilter을 사용해 또 다른 GroupedOpenAPI에 추가하였다. 

    //토큰 인증이 필요 없는 API 모음
    @Bean
    public GroupedOpenApi NonSecurityGroupOpenApi() {
        return GroupedOpenApi
                .builder()
                .group("토큰 인증 불필요한 API")
                .addOpenApiMethodFilter(noAuthFilter)
                .build();
    }

 

다음과 같이 GroupedOpenApi를 만들때, 추가해주면 되고, 이제 토큰 pathsToExclude/pathsToMatch등을 사용하지 않고, Swagger을 통해 구분할 수 있게 되었다.