파란하늘의 지식창고
article thumbnail
반응형

swagger annotation을 사용하여 문서화 작업을 하면 너무 많은 양의 swagger annotation이 오히려 코드의 가독성을 많이 떨어트리게 되어 이에 대해 좀 더 정리하여 사용해보려고 한다.

swagger annotation에 대한 자세한 설명은 아래 github wiki에 있다.
https://github.com/swagger-api/swagger-core/wiki/Swagger-2.X---Annotations

설정하기

spring boot 프로젝트에서 springdoc-openapi의 사용은 다음과 같이 의존성을 추가하면 된다.

webmvc의 경우 아래 dependency를 추가하고 webflux인 경우 springdoc-openapi-starter-webflux-ui를 추가하면 된다.

<dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
</dependency>

해당 설정을 하면 별도의 설정을 하지 않은 경우 기본 호출 주소인 /swagger-ui.html 혹은 /swagger-ui/index.html을 호출하면 swagger-ui에 spring controller의 openapi 문서 목록이 보이게 된다.

springdocs-openapi의 자세한 설정은 아래 springdoc-openapi documentetaion에서 참고하면 된다.

https://springdoc.org/#properties

간단한 Post, Get method Controller 만들어보기

간단한 Spring Boot의 post, get mapping을 만들어보았다.

다만 Post의 경우 multipart/form-data 요청이 아닌 json RequestBody를 전달받는 경우로 가정하였다. (이후 예제에서 swagger requesy body를 사용하는 경우를 설명하기 위해)

@RestController
@RequestMapping(value = "/somepath", produces = MediaType.APPLICATION_JSON_VALUE)
public class SomeController {

    @PostMapping
    public SomeObject somePostMethod(
         @org.springframework.web.bind.annotation.RequestBody SomeRequestBody requestBody) {
        // ... 중간 생략
        var someObject = new SomeObject();
        return someObject;
    }

    @GetMapping
    public SomeObject someGetMethod(
        SomeParameter parameter) {
        // ... 중간 생략
        var someObject = new SomeObject();
        return someObject;
    }
}

swagger ui를 호출하면 해당 controller의 post, get 요청이 등록된 것을 확인할 수 있다.

해당 탭을 클릭해서 확인해 보면 object의 기본 값으로 테스트할 요청이 설정되어 있는 것을 확인할 수 있다.

Try it out을 버튼을 선택하면 요청값을 수정할 수 있게 input 값이 활성화되고 실제로 요청할 수 있는 execute 버튼이 생긴다.

설정된 parameter나 request body의 값을 보면 java domain을 사용하면 제공되는 기본 값으로 모든 필드의 값이 설정되어 있는 것을 확인할 수 있다.

별 문제없어 보이지만 실제로 요청할 때엔 해당 도메인의 모든 값을 사용하지 않고 필요한 값들만 전달받아 사용하게 된다.
또한 기본 값이 아닌 어떤 정해진 규칙의 요청값이 있을 것이다.

위처럼 기본 제공되는  값만으로는 어떻게 요청해야 할지 파악하는데 어려움이 있다.
어떻게 요청할지 파악하여 알고 있다 해도 매번 일일이 요청할 값을 수정해서 사용하는 것도 불편하다.

이때 유용한 게 swagger의 example이다.

example을 미리 작성하여 제공하면 기본 값이 아닌 설정한 example의 값이 제공되고 요청에 필요한 필드들만 설정할 수 있어 해당 요청에 필요한 값이 어떤 것인지 어떤 값을 설정해야 하는지 파악하기 쉽다.

기본적인 post, get 요청 swagger annotation example 작성

위에 만든 post, get 두 요청에 대한  swagger example 작성 예제는 다음과 같다.

import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.parameters.RequestBody;

@RestController 
@RequestMapping(value = "/somepath", produces = MediaType.APPLICATION_JSON_VALUE) 
public class SomeController { 

    @PostMapping 
    public SomeObject somePostMethod( 
        @RequestBody(content = @Content( 
            examples = { 
                @ExampleObject(name = "someExample1", value = """ 
                    { 
                        "someKey1" : "someValue1", 
                        "someKey2" : "someValue2", 
                        "someKey3" : "someValue3"
                    } 
                """), 
                @ExampleObject(name = "someExample2", value = """  
                    { 
                        "someKey1" : "someValue1", 
                        "someKey2" : "someValue2", 
                        "someKey3" : "someValue3"
                    } 
                """) 
            } 
        )) 
        @org.springframework.web.bind.annotation.RequestBody
        SomeRequestBody requestBody) { 
        // ... 중간 생략 
        var someObject = new SomeObject();
        return someObject; 
    } 


    @GetMapping 
    public SomeObject  someGetMethod( 
        @Parameter(examples = { 
            @ExampleObject(name = "example1", value = """ 
                { 
                    "someKey1" : "someValue1", 
                    "someKey2" : "someValue2", 
                    "someKey3" : "someValue3"
                } 
            """), 
                        @ExampleObject(name = "example2", value = """ 
                { 
                    "someKey1" : "someValue1", 
                    "someKey2" : "someValue2", 
                    "someKey3" : "someValue3"
                } 
            """) 
        }) 
        SomeParameter parameter) { 
        // ... 중간 생략 
        var someObject = new SomeObject();
        return someObject; 
    } 
}

이렇게 example을 작성하고 다시 swagger ui에서 확인하면 해당 설정된 값의 example이 보이는 것을 확인할 수 있다.

select 메뉴를 통해 설정한 여러 example 중 하나를 선택하여 사용할 수도 있고 설정된 값을 적절히 수정해서 요청할 수 있다.

 

참고해야 할 부분은 위 예제의 POST요청은 @RequestBody로 json 데이터를 전달하는 경우이다.
만약 multipart/form-data로 데이터를 전달하는 경우라면 Spring의 @RequesyBody 지정이 필요 없고 Get과 동일하게 @Parameter로 example을 작성하면 된다.

method argument annotation을 method annotation 위치로 옮겨보기

위 예제에서 @RequestBody, @Parameter의 선언 위치가 대상 object가 위치한 method argument이다.

공통화를 하려면 우선 method argument 위치에 지정된 내용을 method로 옮겨야 한다.

위 예제는 다음과 같이 변경할 수 있다.

@RestController 
@RequestMapping(value = "/somepath", produces = MediaType.APPLICATION_JSON_VALUE) 
public class SomeController { 

    @RequestBody(content = @Content( 
        examples = { 
            @ExampleObject(name = "someExample1", value = """ 
                { 
                    "someKey1" : "someValue1", 
                    "someKey2" : "someValue2", 
                    "someKey3" : "someValue3"
                } 
            """), 
            @ExampleObject(name = "someExample2", value = """  
                { 
                    "someKey1" : "someValue1", 
                    "someKey2" : "someValue2", 
                    "someKey3" : "someValue3"
                } 
            """) 
        } 
    )) 
    @PostMapping 
    public SomeObject somePostMethod(
        @RequestBody
        @org.springframework.web.bind.annotation.RequestBody
        SomeRequestBody requestBody) { 
        // ... 중간 생략 
        var someObject = new SomeObject();
        return someObject; 
    } 

    @Parameter(name="parameter", examples = { 
        @ExampleObject(name = "example1", value = """ 
            { 
                "someKey1" : "someValue1", 
                "someKey2" : "someValue2", 
                "someKey3" : "someValue3"
            } 
        """), 
                    @ExampleObject(name = "example2", value = """ 
            { 
                "someKey1" : "someValue1", 
                "someKey2" : "someValue2", 
                "someKey3" : "someValue3"
            } 
        """) 
    }) 
    @GetMapping 
    public SomeObject someGetMethod(SomeParameter parameter) { 
        // ... 중간 생략 
        var someObject = new SomeObject();
        return someObject; 
    } 
}

requestBody의 경우 method당 하나만 선언할 수 있다.
method 위치로 세부 설정이 있는 @RequestBody annotaton을 옮긴 경우 해당 값을 사용할 method argument의 parameter에 @RequestBody annotation 지정을 해주어야 한다.
(위 예제에선 argument가 하나지만 여러 개의 argument를 사용하는 경우 @RequesyBody를 지정하지 않으면 어느 값이 requestBody와 맵핑되는지 알 수가 없다.)

parameter 설정의 경우 method에 대해 여러 parameter가 있을 수 있다.
따라서 method 위치로 @Parameter annotation을 옮긴 경우 method argument에 맵핑할 parameter의 name을 지정해주어야 한다.

@Operation annotation 내로 옮겨보기

@Operation annotation은 예제를 보여주기 위해 사용한 @RequestBody, @Parameter를 비롯하여 해당 요청에 대한 자세한 여러 내용(method, tag, summary, description, @ExteranlDocumentation, @ApiResponse 등...)을 명세할 수 있는 상위 단위의 annotation이다.
이제까지 사용한 @RequestBody, @Parameter 두 요소 또한 @Operation 안에 포함하여 선언할 수 있다.

@RestController 
@RequestMapping(value = "/somepath", produces = MediaType.APPLICATION_JSON_VALUE) 
public class SomeController { 

    @Operation( 
        requestBody = @RequestBody(content = @Content( 
            examples = { 
                @ExampleObject(name = "someExample1", value = """ 
                    { 
                        "someKey1" : "someValue1", 
                        "someKey2" : "someValue2", 
                        "someKey3" : "someValue3"
                    } 
                """), 
                @ExampleObject(name = "someExample2", value = """  
                    { 
                        "someKey1" : "someValue1", 
                        "someKey2" : "someValue2", 
                        "someKey3" : "someValue3"
                    } 
                """) 
            } 
        )) 
    ) 
    @PostMapping 
    public SomeObject somePostMethod(
        @RequestBody
        @org.springframework.web.bind.annotation.RequestBody
        SomeRequestBody requestBody) { 
        // ... 중간 생략 
        var someObject = new SomeObject();
        return someObject; 
    } 

    @Operation(parameters = { 
        @Parameter(name="parameter", examples = { 
            @ExampleObject(name = "example1", value = """ 
                { 
                    "someKey1" : "someValue1", 
                    "someKey2" : "someValue2", 
                    "someKey3" : "someValue3"
                } 
            """), 
                        @ExampleObject(name = "example2", value = """ 
                { 
                    "someKey1" : "someValue1", 
                    "someKey2" : "someValue2", 
                    "someKey3" : "someValue3"
                } 
            """) 
        }) 
    }) 
    @GetMapping 
    public SomeObject someGetMethod(SomeParameter parameter) { 
        // ... 중간 생략 
        var someObject = new SomeObject();
        return someObject; 
    } 
}

method annotation을 custom annotation으로 사용하기

method annotation은 해당 내용을 묶어서 따로 annotation을 선언할 수 있다.
위에서 사용한 post와 get에 대한 annotation의 경우 다음과 같이 선언한다.

@Target(METHOD) 
@Retention(RetentionPolicy.RUNTIME) 
@Inherited 
@Operation( 
    requestBody = @RequestBody(content = @Content( 
        examples = { 
            @ExampleObject(name = "someExample1", value = """ 
                { 
                    "someKey1" : "someValue1", 
                    "someKey2" : "someValue2", 
                    "someKey3" : "someValue3"
                } 
            """), 
            @ExampleObject(name = "someExample2", value = """  
                { 
                    "someKey1" : "someValue1", 
                    "someKey2" : "someValue2", 
                    "someKey3" : "someValue3"
                } 
            """) 
        } 
    )) 
) 
public @interface SomePostOperation { 
} 
@Target(METHOD) 
@Retention(RetentionPolicy.RUNTIME) 
@Inherited 
@Operation(parameters = { 
        @Parameter(name="parameter", examples = { 
            @ExampleObject(name = "example1", value = """ 
                { 
                    "someKey1" : "someValue1", 
                    "someKey2" : "someValue2", 
                    "someKey3" : "someValue3"
                } 
            """), 
                        @ExampleObject(name = "example2", value = """ 
                { 
                    "someKey1" : "someValue1", 
                    "someKey2" : "someValue2", 
                    "someKey3" : "someValue3"
                } 
            """) 
        }) 
    }) 
public @interface SomeGetOperation { 
}

선언한 custom annotation을 기존 controller에 사용하면 다음과 같다.

@RestController 
@RequestMapping(value = "/somepath", produces = MediaType.APPLICATION_JSON_VALUE) 
public class SomeController {

    @SomePostOperation 
    @PostMapping 
    public SomeObject somePostMethod(
        @RequestBody
        @org.springframework.web.bind.annotation.RequestBody
        SomeRequestBody requestBody) { 
        // ... 중간 생략 
        var someObject = new SomeObject();
        return someObject; 
    } 

    @SomeGetOperation 
    @GetMapping 
    public SomeObject someGetMethod(SomeParameter parameter) { 
        // ... 중간 생략 
        var someObject = new SomeObject();
        return someObject; 
    } 
}

이제 기존 controller의 코드는 각 호출 별로 annotation이 하나 더 들어가고 requestBody 요청인 경우 @RequestBody 선언만 추가하면 되기 때문에 swagger annotation으로 인해 코드 가독성이 크게 떨이지게 되지 않게 되었다.

위의 예시는 example을 사용하는 경우를 중심으로 정리하였지만 swagger의 다양한 annotaion을 사용하여 문서를 작성하는 경우도 동일하게 깔끔하게 정리할 수 있다.

만약 controller가 상위 interface를 구현하는 식으로 작성되었다면 swagger annotation을 interface에 지정해도 그대로 동작한다.

java가 text block을 지원하기 시작하면서 swagger annotation으로 예제 작성이 쉬워졌다.

asciidoctor를 사용하는 것과 비교하면 상세 설명은 아무래도 asciidoctor가 편하지만 예제 api의 작성이나 테스트 제공은 아무래도 swagger ui를 사용하는 게 편하다.

기존에 asciidoctor에서 restdoc과 restdocs-api-spec을 조합하여 openapi 문서를 작성한 경우는 테스트 코드 작성의 과정이 필요하지만 springdocs-openapi를 사용하면 과정이 간결해지는 장점이 있다.

어느 게 좋다고 말할 수 없으니 두 가지 방법을 적절히 사용하면 좋지 않을까 싶다.

반응형
profile

파란하늘의 지식창고

@Bluesky_

도움이 되었다면 광고를 클릭해주세요