Swagger API in Spring Boot

트곰·2024년 10월 2일
0

개발을 하다보면, 프로젝트에 대한 문서가 필요한 경우가 있다.
프론트, 앱 개발자와 소통하는 목적일 수도 있고,
어떤 API가 구현되있는지 확인하는 목적일 수도 있다.

Spring Boot에서 가장 흔하게 사용하는 DOCS는 Spring Docs와 Swagger가 있다.
Spring Docs는 Controller Test를 만들어야 Docs가 생성되기 때문에 1석 2조로 활용할 수 있지만, 그만큼 개발 비용이 많이 든다.
Swagger API 는 간단하게 만들 수 있지만, Controller 메소드 상단에 annotation으로 추가해서, 실제 코드의 가독성을 떨어트리는 단점이 있다.

만약, Controller에 작성하지 않고 간단하게 Swagger API를 사용할 수 있다면?!!

Swagger를 활용해서 example로 샘플 데이터를 보여주는 경우가 있는데
샘플 데이터가 너무 길어지면, swagger에 대한 설정만 10줄은 쉽게 넘기는 경우가 있었다.
이런 경우, example를 yml 또는 properties 파일로 분리하고 해당 내용을 호출해서 사용할 수 있을까?하고 확인해보았다.

  1. example.yml 파일 생성
    spring boot에서는 application.yml 파일 이외의 값은 import되지 않아서
    swagger 내용에만 활용할 yml 파일을 만든다면, application.yml에 해당 파일을import한다는 설정이 필요하다.
//application.yml

spring:
  config:
    import: classpath:docs/example.yml, classpath:docs/totalDashboard.yml //swagger에 example이 될만한 코드를 import
## 스웨거 설정(spring doc)
springdoc:
  swagger-ui:
    path: /api/promotion/swagger-ui
    defaultModelsExpandDepth: -1  # 모델(Schemas) 영역을 최소화 / -1은 아예 화면에서 사라짐, 0은 최소화
    

classpath까지는 src/resources의 경로고, 그 하위에 디렉토리 및 파일 위치를 명시하면 된다.만약 추가하는 파일이 2개 이상이라면, 콤마(,)로 이어서 작성하면 된다.

swagger api는 api영역과 schemas(dto)영역이 존재하는데
schemas는 결국 api 하위에 포함된 내용으로 해당 내용을 최소화하고 싶다면, 위와 같은 설정을 추가하면 된다.

  1. example.yml 파일
landingPageThemeByPromotionType:
        response:
                example: |
                        {
                          "statusCode": 200,
                          "status": "정상",
                          "data": [
                            {
                              "idx": 8,
                              "themeName": "캐릭터",
                              "sampleImagePath": "S3 버킷 주소"
                            },
                            ...
                          ]
                        }

S3 버킷 주소는 회사명이 담겨서 string 값으로 변경하고, 이런 식으로 값을 추가하면 된다.
| 하위의 내용은 알아서 줄바꿈이 yml 파일과 동일하게 적용되서, swagger api에서 보인다.

  1. Controller 내 example에 추가 (실패한 방법)
@Value("${landingPageThemeByPromotionType.response.example}")
private String themeResponseExample;


    @Operation(summary = "프로모션 타입별 랜딩 페이지 테마 조회 API", description = "프로모션 등록 시, 업체에 해당하는 테마가 보이게 하는 조회 API")
    @ApiResponses(value = {
            @ApiResponse(responseCode = "200",
                content = {@Content(schema = @Schema(implementation = ResponseThemeDto.class),
                mediaType = "application/json",
                examples = @ExampleObject(value = themeResponseExample))})
    })

@Value 어노테이션은 Java 상수나 필드에서만 사용 가능해서, @ExampleObject의 value 속성에 직접 선언하는 방식은 지원하지 않는다.
그래서 @Value로 선언하고, 이를 다시 호출하는 방식으로 사용해야 한다.

그런데, Attribute value must be constant 라고 컴파일이 되지 않는다!!
@Value는 어노테이션의 속성으로 사용할 수 없어서, 선언 후 호출하더라도 결국 에러가 발생하는 것이다.
이 문제를 해결하려면 미리 데이터를 읽어와서 주입한 후에 사용하는 방법을 사용해야 한다고 한다.

import org.springdoc.core.customizers.OpenApiCustomizer;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SwaggerConfig {

    @Value("classpath:docs/example.yml")
    private String themeResponseExample;

    @Bean
    public OpenApiCustomizer customizeOpenApi() {
        return openApi -> {
            openApi.getPaths().get("/theme").getGet().getResponses().get("200")
                .getContent().get("application/json").getExamples().put("example-1",
                new io.swagger.v3.oas.models.examples.Example().value(themeResponseExample));
        };
    }
}

이런 식으로 SwaggerConfig 클래스에 동적으로 데이터를 주입하는 방법을 사용해야 한다고 한다.

Swagger API를 Controller와 SwaggerConfig 2개 클래스에서 관리한다면, 유지보수의 번거로움도 있고 가독성이 역시 좋지 않다고 생각이 들었다.
그러면 SwaggerConfig에 합쳐서 작성하는 방법은 없을까?

  1. SwaggerConfig 클래스에 api 명세에 대한 모든 내용을 추가하자!

import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.responses.ApiResponse;
import io.swagger.v3.oas.models.media.Content;
import io.swagger.v3.oas.models.media.MediaType;
import io.swagger.v3.oas.models.examples.Example;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.responses.ApiResponses;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.parameters.Parameter;
import io.swagger.v3.oas.models.PathItem;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import org.springdoc.core.customizers.OpenApiCustomizer;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;

import java.io.IOException;
import java.util.Arrays;
import java.util.List;

@Configuration
@PropertySource(value = "classpath:docs/example.yml", encoding = "UTF-8")  // UTF-8로 인코딩 설정
public class SwaggerConfig {
    @Value("${server.name}")
    private String serverName;

    @Value("${landingPageThemeByPromotionType.response.example}")
    private String themeResponseExample;

    @Bean
    public OpenAPI customOpenAPI() {
        return new OpenAPI()
                .info(new Info()
                        .title("promotion API document - " + serverName)
                        .description("[" + serverName + "] 프로모션 백엔드 api 문서 - ")
                        .version("1.0.0"))
                .components(new Components()
                        .addSecuritySchemes("bearerAuth",
                                new SecurityScheme()
                                        .type(SecurityScheme.Type.HTTP)
                                        .scheme("bearer")
                                        .bearerFormat("JWT")))
                .addSecurityItem(new SecurityRequirement().addList("bearerAuth"));
    }


    @Bean
    public OpenApiCustomizer customizeLandingPageThemeApi() throws IOException {
        // JSON 문자열을 객체로 변환
        ObjectMapper objectMapper = new ObjectMapper();
        Object themeResponseExampleObject = objectMapper.readValue(themeResponseExample, Object.class);

        return openApi -> {
            openApi.path("/api/promotion/landingPage/theme", new PathItem()
                    .get(new Operation()
                            .summary("프로모션 타입별 랜딩 페이지 테마 조회 API")
                            .description("프로모션 등록 시, 업체에 해당하는 테마가 보이게 하는 조회 API")
                            .addTagsItem("LandingImageController") // Tag 추가
                            .responses(new ApiResponses()
                                    .addApiResponse("200", new ApiResponse()
                                            .description("정상 응답")
                                            .content(new Content()
                                                    .addMediaType("application/json", new MediaType()
                                                            .schema(new Schema().$ref("#/components/schemas/ResponseThemeDto"))
                                                            .example(new Example().value(themeResponseExampleObject)))))
                            )
                            .addParametersItem(new Parameter()
                                    .name("customerCd")
                                    .in("query") //파라미터가 어디에 포함되는지 나타냄
                                    .required(true)
                                    .schema(new Schema<Integer>().type("integer"))
                                    .description("고객 코드")
                                    .example(121438))
                            .addParametersItem(new Parameter()
                                    .name("promotionType")
                                    .in("query")
                                    .required(true)
                                    .schema(new Schema<Integer>().type("integer"))
                                    .description("프로모션 타입")
                                    .example(1))
                    ));
        };
    }

    
}

이렇게 하면, swagger 코드가 길어지면서 controller의 가독성을 해치는 문제도 해결되고
모든 api의 내용을 한 파일에서 관리할 수 있게 된다.

메소드 내부 내용
1. .in .는 OpenAPI 명세에서 파라미터가 어디 위치하는지 나타내는 속성이다.
1. query: URL의 쿼리 문자열에 파라미터가 포함됩니다.
예: /api/users?id=123
path: URL 경로의 일부로 파라미터가 포함됩니다.
예: /api/users/{id}
header: HTTP 헤더에 파라미터가 포함됩니다.
cookie: 쿠키에 파라미터가 포함됩니다.
body: HTTP 요청 본문에 파라미터가 포함됩니다 (주로 POST, PUT 요청에서 사용).

만약 컨트롤러가 있지만, 당장 사용하고 있지 않는 내용이라면?
@Hidden을 Controller layer에 붙이면, 해당 컨트롤러에 대한 api는 swagger에 포함되지 않는다.
method layer에도 물론 추가할 수 있는 어노테이션이다.

오히려 너무 많은 메소드가 담기면, 가독성이 더 떨어지려나?

profile
개발자가 되기 위해서 공부중입니다 :ㅡ)

0개의 댓글