프로젝트

(5)-1 Spring Boot3에서 Swagger 사용하기 : Swagger 설치, Swagger Config 설정

nkdev 2025. 3. 30. 20:07

API 문서를 작성해보자.

나는 HTTP 프로토콜을 사용하는 REST API를 사용할 것이기 때문에 먼저 HTTP 프로토콜의 주요 개념과 REST API가 어떻게 동작하는지 먼저 간단히 정리하고 Swagger 설치하는 법을 알아보도록 하겠다.

 

REST API

REST API 설계 원칙

REST는 HTTP 기반으로 클라이언트가 서버의 리소스에 접근하는 방식을 규정한 아키텍처이다. REST한 방식으로 API를 설계하려면 원칙이 있다.

 

  1. 자원(URI), 행위(HTTP request method), 표현(payload)를 사용하기
  2. URI는 리소스를 표현
  3. 리소스에 대한 행위는 HTTP request method로 표현. URI로 표헌하지 않기

이런 이론적인 얘기를 먼저 하면 당연히 안 와닿을테니 예시를 보자. 회원 관리 시스템 API를 설계했다.

위처럼 설계하면 RESTful하지 않고, 아래처럼 설계해야 RESTful하다.

 

위는 URI가 리소스에 대한 동작을 포함하고 있다. URI에는 read, create, delete와 같은 동작이 포함되면 안 된다. URI는 일관된게 '리소스'만을 포함해야 하고, 리소스에 대한 동작은 HTTP 요청 메서드로 구분되게 해야 한다.

 
회원 목록 조회
GET
회원 조회
GET
회원 등록
POST
회원 삭제
DELETE
회원 수정
PUT
회원 수정
PATCH

▲ RESTful하지 않은 예

회원 목록 조회
GET
회원 조회
GET
회원 등록
POST
회원 삭제
DELETE
회원 수정
PUT
회원 수정
PATCH

▲ RESTful한 예

 

예외

물론 REST API의 설계 원칙을 항상 지킬 수는 없다. 특히 HTML form 태그를 사용해 API 요청을 처리할 때는 form이 GET, POST 요청만 지원하기 때문에 DELETE, PUT을 사용해 리소스 동작을 표현하는 것이 제한된다. 이런 경우는 HTTP 요청 메서드만으로 명확히 API를 이해하기 어려우므로 URI에 동사를 포함시켜 설계하기도 한다. 이를 컨트롤 URI라고 하며, 가독성과 명확성을 유지하기 위해 선택적으로 컨트롤 URI를 사용할 수 있다. 그러나 원칙적인 관점에서는 URI에 동사를 제외하고 리소스만 포함시키고, 리소스의 동작을 HTTP 요청 메서드로 표현해야 API가 일관성 있고 이해하기 쉽게 설계된다.

 

 

Swagger 사용하기

스웨거 버전이 2.0에서 3.0으로 바뀌면서 이름도 swagger에서 OpenAPI로 바뀌었다.

swagger 2.0 -> OpenAPI 3.0

 

2.0버전에서는 Swagger UI를 만들어주는 구현체로 SpringFox를 썼는데 3.0버전으로 넘어가면서 이제 SpringDoc을 사용하고 있다. SpringFox는 2020년 7월 이후 더 이상 업데이트되지 않고 Swagger3.0버전, SpringBoot v3와도 호환되지 않기 때문에 SpringDoc으로 마이그레이션하는 사람들이 많아보인다.

 

내 프로젝트는 SpringBoot v3를 쓰고 있으니 SpringDoc을 사용해보겠다.

 

1. 의존성 추가

스웨거 버전은 SpringBoot v3 환경에서 springdoc-openapi 2.x 버전이 가장 호환이 잘 되기 때문에 2.6.0으로 넣어줬다.

implementation 'org.springframework.boot:spring-boot-starter-web'	
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0'
 

* 공식 문서

의존성 추가할 때마다 버전 선택이 참 어렵다. 구글링으로 찾아도 되지만 줏대있게 공식 문서를 보면서 설치하고싶으면 아래 페이지를 참고하자.

 

OpenAPI 3 Library for spring-boot (springdoc.org)

이 페이지에서 본인 프로젝트의 스펙과 맞는 springdoc-openapi 라이브러리를 찾으면 된다.

나는 spring-boot-starter-web과 같이 사용할 예정이므로 webmvc를 선택했다. 그리고 webmvc-api는 ui 없이 OpenAPI 문서만 제공하는 것이고 webmvc-ui는 ui와 OpenAPI 문서를 함께 제공하는 것인데 나는 둘 다 사용할 거라서 ui를 선택했다.

 

* Swagger-UI

의존성만 추가해도 서버 실행 후 http://server:port/context-path/swagger-ui.html 에 접속하면 바로 Swagger UI를 볼 수 있다. 설치된 스웨거 라이브러리에 정적 파일이 내장되어있어서 spring boot에 의해 경로가 매핑되어 ui가 뜬다.

 

나는 로컬 서버에 포트 번호가 8080이라서 http://localhost:8080/swagger-ui.html 로 접속했다.

 

* Swagger-UI를 json/yaml파일로 바꾸기

이 UI를 다른 사람들(예를 들면 클라이언트 또는 프론트엔드 작업자)과 공유하고싶은데 지금처럼 서버를 실행시키고 url을 쳐야만 페이지를 볼 수 있다면 난감할 것이다. 대신 이 페이지에 정의된 API 스펙들을 json 또는 yaml 형식의 document로 변환시켜서 전달해주면 서버 실행 없이도 우리와 똑같은 ui를 볼 수 있다.

 

http://server:port/context-path/v3/api-docs 로 접속하면 아래와 같은 화면이 뜨는데, 이게 그 document이다. 이렇게 API 스펙을 json 또는 yaml 형식으로 나타낸 문서를 OpenAPI라고 한다. 이 파일을 깃에 올려서 API 스펙이 변경될 때마다 업데이트해주는 식으로 공유하면 될 듯 하다.

 

 

참고로 'OpenAPI'라는 용어는 'Open API'와는 다른 고유명사이다. 'Open API'는 말 그대로 공개된 API이고 'OpenAPI'는 RESTful API에 대한 정의 표준이다. 그러니까 RESTful API의 정의(Specification)를 json이나 yaml로 표현한 방식이라고 할 수 있다.

 

* OpenAPI가 뭔데

추가적으로 말하자면,, OpenAPI에 대해 찾아보다가 Swagger과 OpenAPI의 관계를 알게 됐는데 원래 Swagger는 2010년대 초반에 회사 자체 API의 UI로만 쓰였었다. 그래서 이름이 Swagger 2.0 이런 식으로 불렸었는데 2015년 말 OpenAPI Initiative에 Swagger가 기부되면서 OpenAPI Specification으로 이름이 변경되었고, 현재 3.0버전이 되면서 OpenApi 3.0 Specification으로 불리고 있다고 한다.

 

그러니까 OpenAPI는 RESTful API 정의 표준이고 Swagger는 OpenAPI를 표현하는 tool인데 아무튼 이름은 OpenAPI로 불린다는 거..?

 

 

 

 

 

*plugin 설정 (선택사항)

plugins {
    id "org.springframework.boot" version "2.7.0"
    id "org.springdoc.openapi-gradle-plugin" version "1.9.0"
}
 

이 플러그인을 설정하면 빌드 결과로 OpenAPI spec이 담긴 openapi.json문서를 자동으로 생성해준다. 또는 gradle generateOpenApiDocs 명령어를 실행하면 spring boot가 forkedSpringBootRun이라는 태스크를 수행해서 해당 문서를 생성해준다. 빌드 단계에 필요한 플러그인인것 같아서 나는 추가하지 않았다. 나중에 프로젝트가 완성된 후에 빌드하고 배포할 때 배포스크립트에 이 파일을 사용해서 자동으로 API스펙을 업데이트하는.. 그런 작업에 사용되지 않을까?

2. Properties 설정

3. configuration 설정

Swagger Config파일로 UI를 내가 보기 편하게 커스터마이징할 수 있다. main패키지 하위에 SwaggerConfig클래스를 생성해서 @Configuration을 지정해준 후 안에 빈을 설정한다.

  @Bean
  public OpenAPI springShopOpenAPI() {
      return new OpenAPI()
              .info(new Info().title("SpringShop API")
              .description("Spring shop sample application")
              .version("v0.0.1")
              .license(new License().name("Apache 2.0").url("http://springdoc.org")))
              .externalDocs(new ExternalDocumentation()
              .description("SpringShop Wiki Documentation")
              .url("https://springshop.wiki.github.org/docs"));
  }

기본 구조는 위와 같다. 이 코드를 넣으면 Info(), License(), ExternalDocumentation()부분에 필요한 패키지를 import하라고 뜰 텐데 annotation이 아니라 model을 넣으면 된다. Info, License, ExternalDocumentation클래스가 Swagger API 문서를 정의하기 위한 모델 클래스이기 때문이라는데 무슨 소린지는 잘 모르겠다.

 
model을 import하세요
package lems.cowshed.config;

import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.models.ExternalDocumentation;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.servers.Server;
import org.springdoc.core.customizers.GlobalOpenApiCustomizer;
import org.springdoc.core.models.GroupedOpenApi;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.List;

@Configuration
public class SwaggerConfig {
    //metadata for API documentation
    @Value("${lems.openapi.dev-url}")
    private String devUrl;

    @Value("${lems.openapi.prod-url}")
    private String prodUrl;

    @Bean
    public GroupedOpenApi userApi(){
        return GroupedOpenApi.builder()
                .group("user")
                .pathsToMatch("/users/**")
                .build();
    }

    @Bean
    public GroupedOpenApi eventApi(){
        return GroupedOpenApi.builder()
                .group("event")
                .pathsToMatch("/events/**")
                .build();
    }

    @Bean
    public OpenAPI metaData() {
        var info = new Info()
                .title("LEMS API")
                .description("This API exposes endpoints of LEMS application")
                .contact(new Contact()
                        .email("affectionmej@gmail.com")
                        .name("LEMS")
                        .url(prodUrl));
        var devServer = new Server()
                .url(devUrl)
                .description("Server URL in Development environment");
        var prodServer = new Server()
                .url(prodUrl) //null cuz haven't deployed yet
                .description("Server URL in Production environment");

        return new OpenAPI()
                .info(info)
                .servers(List.of(devServer, prodServer));
    }
}

지금까지 완성된 SwaggerConfig파일이다.

 

나는 User컨트롤러와 Event컨트롤러의 API를 분리해서 보고싶어서 GroupedOpenApi를 사용해서 두 그룹으로 나누었다. 이렇게 그룹을 나누면 Swagger UI의 오른쪽 위 부분의 드롭다운으로 user와 event가 나뉘어져 보인다.

 

각 그룹에 포함될 API 메서드는 pathsTomatch()로 설정할 수 있다. Event컨트롤러에 포함되는 모든 api메서드의 경로가 events/ 로 시작해서 pathsTomatch(events/**)로 설정했고, User컨트롤러도 마찬가지로 설정해줬다.

 

만약 모든 API 메서드에 동일하게 적용하고싶은 부분이 있다면 GlobalOpenApiCustomizer을 쓰면 되고, 그룹을 나눠서 각 그룹별로 동일하게 적용하고싶다면 OpenApiCustomizer로 별도의 커스터마이징을 하면 된다. 공식 문서에는 아래와 같이 나와있다.

@Bean
public OpenApiCustomizer consumerTypeHeaderOpenAPICustomizer() {
return openApi -> openApi.getPaths().values().stream().flatMap(pathItem -> pathItem.readOperations().stream())
    .forEach(operation -> operation.addParametersItem(new HeaderParameter().$ref("#/components/parameters/myConsumerTypeHeader")));
}

 

사용 방법은 다음과 같다. OpenApiCustomiser을 생성한 다음 GroupedOpenApi에 .addOpenApiCustomiser()로 커스터마이징을 추가해준다.

    @Bean
    public GroupedOpenApi userApi(OpenApiCustomiser userOpenApiCustomiser) {
        return GroupedOpenApi.builder()
                .group("user")
                .pathsToMatch("/user/**")
                .addOpenApiCustomiser(userOpenApiCustomiser) // 그룹별 Customiser 추가
                .build();
    }

    // user 그룹에 적용할 OpenApiCustomiser
    @Bean
    public OpenApiCustomiser userOpenApiCustomiser() {
        return openApi -> openApi.getInfo().title("User API").version("v1.0.0");
    }
 
  • GroupedOpenApi : API들을 그룹으로 나누고싶을 때 사용. 각 그룹은 별도의 문서처럼 취급되며, 경로나 그룹 이름에 따라 분리됨.
    • group() : 그룹 이름 설정
    • pathsToMatch() : 그룹에 포함될 경로 패턴 설정. 예를 들어 "/user/**"경로는 user그룹에 포함됨
    • addOpenApiCustomiser() : OpenApiCustomiser을 추가
  • OpenApiCustomiser : API 문서를 세부적으로 커스터마이징. 특정 그룹의 API 응답 코드, 보안 설정, API 설명 등을 추가하거나 수정할 때 사용됨. GroupedOpenApi와 함께 사용하면 그룹별로 커스터마이징이 가능하며, 전역 설정도 가능.

 

현재 OpenApiCustomiser과 API메서드마다 붙이는 ApiResponse를 같이 쓰니까 ApiResponse가 무시되어서 일단 아래와 같이 설정하였다.

package lems.cowshed.config;

import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.models.ExternalDocumentation;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.media.ArraySchema;
import io.swagger.v3.oas.models.media.Content;
import io.swagger.v3.oas.models.media.MediaType;
import io.swagger.v3.oas.models.responses.ApiResponse;
import io.swagger.v3.oas.models.responses.ApiResponses;
import io.swagger.v3.oas.models.servers.Server;
import org.springdoc.core.customizers.GlobalOpenApiCustomizer;
import org.springdoc.core.customizers.OpenApiCustomizer;
import org.springdoc.core.models.GroupedOpenApi;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.List;

@Configuration
public class SwaggerConfig {
    //metadata for API documentation
    @Value("${lems.openapi.dev-url}")
    private String devUrl;

    @Value("${lems.openapi.prod-url}")
    private String prodUrl;

    @Bean
    public GroupedOpenApi userApi(){
        return GroupedOpenApi.builder()
                .group("user")
                .pathsToMatch("/users/**")
//                .addOpenApiCustomizer(apiResponsesCustomizer())
                .build();
    }

    @Bean
    public GroupedOpenApi eventApi(){
        return GroupedOpenApi.builder()
                .group("event")
                .pathsToMatch("/events/**")
//                .addOpenApiCustomizer(apiResponsesCustomizer())
                .build();
    }

    @Bean
    public OpenAPI metaData() {
        var info = new Info()
                .title("LEMS API")
                .description("This API exposes endpoints of LEMS application")
                .contact(new Contact()
                        .email("affectionmej@gmail.com")
                        .name("LEMS")
                        .url(prodUrl));
        var devServer = new Server()
                .url(devUrl)
                .description("Server URL in Development environment");
        var prodServer = new Server()
                .url(prodUrl) //null cuz haven't deployed yet
                .description("Server URL in Production environment");

        return new OpenAPI()
                .info(info)
                .servers(List.of(devServer, prodServer));
    }

//    @Bean
//    public OpenApiCustomizer apiResponsesCustomizer(){
//        return new OpenApiCustomizer() {
//            @Override
//            public void customise(OpenAPI openApi) {
//                openApi.getInfo().title("User API").description("회원 관련 API");
//                openApi.getPaths().forEach((path, pathItem)-> {
//                    if(path.startsWith("/users") || path.startsWith("/events")){
//                        ApiResponses responses = new ApiResponses();
//                        responses.addApiResponse("201", new ApiResponse().description("201 created 요청 결과 새로운 리소스가 생성되었습니다."));
//                        responses.addApiResponse("202", new ApiResponse().description("202 accepted 요청을 수신하였지만 그에 응하여 행동할 수 없습니다."));
//                        responses.addApiResponse("400", new ApiResponse().description("400 bad request 서버가 요청을 이해할 수 없습니다."));
//                        responses.addApiResponse("404", new ApiResponse().description("404 not found 서버가 요청받은 리소스를 찾을 수 없습니다."));
//                        responses.addApiResponse("500", new ApiResponse().description("500 server error 서버가 처리할 수 없는 요청입니다."));
//                        pathItem.readOperations().forEach(operation -> operation.setResponses(responses));
//                    }
//                });
//            }
//        };
//    }



}
 

4. API 스펙 작성

Controller에 어노테이션을 붙여서 API 스펙을 작성해보자.

@Operation(summary = "Get a user by ID", description = "Returns a user based on ID.")
@ApiResponses(value = {
    @ApiResponse(responseCode = "200", description = "Success"),
    @ApiResponse(responseCode = "404", description = "User not found")
})
@ApiResponse(responseCode = "200", description = "Successfully retrieved user")
@Parameter(name = "id", description = "User ID", required = true)
@RequestBody(description = "User object that needs to be created")
@Schema(description = "User object model")
@Tags(value = {@Tag(name = "User Operations")})
@ApiOperation(value = "Get user by ID")
@Api(tags = {"User API"})
 
@RestController
@RequestMapping("/api/users")
public class UserController {

    @Operation(summary = "Get user by ID", description = "Find a user by their unique ID")
    @ApiResponses(value = {
        @ApiResponse(responseCode = "200", description = "Found the user"),
        @ApiResponse(responseCode = "404", description = "User not found")
    })
    @GetMapping("/{id}")
    public ResponseEntity<User> getUserById(@Parameter(description = "ID of the user to be retrieved", required = true)
                                            @PathVariable("id") Long id) {
        // controller logic here
        return ResponseEntity.ok(new User());
    }
}
 

여기서부터 이제 컨트롤러에 어노테이션을 붙여가면서 해당 API가 어떤 기능을 하는지 Swagger UI로 보여주면 된다. 이 내용은 다른 포스팅을 참고!

 


Swagger Configuration : API 그룹화

https://dkswnkk.tistory.com/752

Swagger Configuration : OpenAPI 작성

https://www.bezkoder.com/spring-boot-swagger-3/

Swagger Configuration : Security 설정, API스펙, DTO스펙 작성

https://sjh9708.tistory.com/169