본문 바로가기
삵
Development

Spring Boot 3에서 Swagger UI 자동 생성

포포스트맨·2026년 9월 10일·조회 12

REST API를 만들고 나면 문서가 항상 뒤로 밀린다. 코드는 계속 바뀌는데 위키나 노션에 손으로 적어둔 파라미터 표는 금세 낡는다. 저는 이럴 때 문서를 사람이 쓰지 않고 코드에서 뽑아내는 방향으로 정리한다. Spring Boot 3라면 springdoc-openapi 한 줄로 시작할 수 있다.

결론부터 말하자면, springdoc-openapi-starter-webmvc-ui 의존성 하나를 넣으면 컨트롤러를 스캔해 OpenAPI 3 스펙(/v3/api-docs)과 Swagger UI(/swagger-ui.html)를 자동으로 만들어준다. 여기에 어노테이션으로 예시와 JWT 인증을 덧붙이고, 그룹을 나눠 문서를 쪼갠 뒤, 그 스펙을 Postman으로 가져와 newman으로 테스트까지 돌리는 순서로 진행한다.

OpenAPI와 springdoc-openapi가 무엇인가

OpenAPI는 REST API의 엔드포인트, 파라미터, 응답 구조, 인증 방식을 기계가 읽을 수 있는 JSON 또는 YAML로 기술하는 표준 규격이다. 예전 이름이 Swagger라서 지금도 두 단어가 섞여 쓰인다. 규격이 OpenAPI, 그 스펙을 화면으로 그려주는 도구가 Swagger UI라고 보면 된다.

springdoc-openapi는 스프링 컨트롤러와 DTO를 런타임에 훑어 이 OpenAPI 스펙을 만들어주는 라이브러리다. 과거 springfox를 쓰던 프로젝트라면 이제 springdoc으로 넘어와야 한다. springfox는 Spring Boot 3와 Jakarta 네임스페이스를 지원하지 않는다.

버전 선택: Spring Boot 3는 springdoc 2.x

Spring Boot 3는 javax.*에서 jakarta.*로 넘어갔기 때문에 springdoc도 2.x 계열을 써야 한다. 1.x는 Spring Boot 2 전용이다. 현재 2.x 최신은 2.8 계열이고 Java 17 이상을 요구한다. 아티팩트 이름도 1.x와 달라졌으니 주의한다.

Maven이면 이렇게 넣는다. 버전 번호는 릴리스 상태에 따라 다를 수 있으니 최신 2.x를 확인하고 고정한다.

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

Gradle이면 한 줄이다.

implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.6'

WebFlux 프로젝트라면 -webmvc-ui 대신 springdoc-openapi-starter-webflux-ui를 쓴다. 여기서는 가장 흔한 Spring MVC 기준으로 설명한다.

의존성만 넣고 바로 확인하기

추가 설정 없이 앱을 띄우면 두 경로가 열린다. 스펙 JSON과 Swagger UI다.

$ curl -s http://localhost:8080/v3/api-docs | head -c 200
{"openapi":"3.1.0","info":{"title":"OpenAPI definition","version":"v0"},"servers":[{"url":"http://localhost:8080","description":"Generated server url"}],"paths":{...}}

브라우저로는 http://localhost:8080/swagger-ui.html에 접속한다. 이 주소는 실제로 /swagger-ui/index.html로 리다이렉트된다. 여기서 한 번 막히기 쉬운데, 컨텍스트 패스(server.servlet.context-path)를 설정했다면 그 앞에 컨텍스트 패스가 붙는다. 404가 나면 이 경로부터 확인한다.

YAML로 받고 싶으면 /v3/api-docs.yaml을 호출한다. 뒤에서 Postman으로 가져올 때는 JSON 쪽이 편하다.

경로를 바꾸거나 운영에서 끄기

application.yml에서 경로와 노출 여부를 조정한다. 운영 환경에서 문서를 닫고 싶으면 enabled를 false로 둔다.

springdoc:
  api-docs:
    path: /v3/api-docs      # 스펙 JSON 경로
    enabled: true
  swagger-ui:
    path: /swagger-ui.html  # UI 진입 경로
    enabled: true
    tags-sorter: alpha
    operations-sorter: alpha

어노테이션으로 문서에 살 붙이기

자동 생성만으로는 설명이 비어 있다. 컨트롤러와 DTO에 어노테이션을 달아 제목, 설명, 예시, 응답 코드를 채운다. 핵심 어노테이션은 몇 개 안 된다.

  • @Tag: 컨트롤러 단위로 문서를 묶는 그룹 이름
  • @Operation: 엔드포인트 하나의 요약과 설명
  • @Parameter: 쿼리, 패스 파라미터 설명과 예시
  • @ApiResponse: HTTP 상태 코드별 응답 문서화
  • @Schema: DTO 필드의 설명, 예시, 필수 여부
  • @Hidden: 문서에서 제외

컨트롤러 예시다. 각 응답 코드에 대표 예시(JSON 문자열)를 붙였다.

@Tag(name = "members", description = "회원 조회와 가입")
@RestController
@RequestMapping("/api/members")
public class MemberController {

    @Operation(summary = "회원 단건 조회", description = "ID로 회원 한 명을 조회한다.")
    @ApiResponses({
        @ApiResponse(responseCode = "200", description = "조회 성공",
            content = @Content(schema = @Schema(implementation = MemberResponse.class),
                examples = @ExampleObject(value = """
                    {\"id\": 1, \"email\": \"kim@sarc.io\", \"nickname\": \"kim\"}"""))),
        @ApiResponse(responseCode = "404", description = "회원 없음",
            content = @Content(examples = @ExampleObject(value = """
                {\"code\": \"MEMBER_NOT_FOUND\", \"message\": \"회원을 찾을 수 없다\"}""")))
    })
    @GetMapping("/{id}")
    public MemberResponse getMember(
        @Parameter(description = "회원 ID", example = "1")
        @PathVariable Long id) {
        return memberService.findById(id);
    }
}

DTO에는 @Schema로 필드 의미와 예시를 적는다. 이걸 채워두면 Swagger UI의 Example Value와 Schema 탭이 그럴듯하게 그려진다.

public record MemberResponse(
    @Schema(description = "회원 ID", example = "1")
    Long id,
    @Schema(description = "이메일", example = "kim@sarc.io")
    String email,
    @Schema(description = "닉네임", example = "kim", requiredMode = Schema.RequiredMode.REQUIRED)
    String nickname
) {}

여기서 자주 걸리는 함정 하나. Bean Validation 어노테이션(@NotNull, @Size 등)을 DTO에 달아두면 springdoc이 그 제약을 스펙의 required와 maxLength로 자동 반영한다. 그래서 @Schema에 필수 여부를 중복으로 적을 필요가 없는 경우가 많다. 두 곳이 어긋나면 검증 어노테이션 쪽이 실제 동작이니 그쪽을 기준으로 맞춘다.

JWT 인증을 문서에 표기하기

Bearer 토큰을 쓰는 API라면 Swagger UI에서 Authorize 버튼으로 토큰을 넣고 바로 호출해볼 수 있게 만든다. 보안 스킴을 한 번 정의해두는 방식이다. 설정 클래스에서 OpenAPI 빈을 등록한다.

@Configuration
public class OpenApiConfig {

    @Bean
    public OpenAPI sarcOpenAPI() {
        final String schemeName = "bearer-jwt";
        return new OpenAPI()
            .info(new Info().title("sarc API").version("v1")
                .description("sarc.io 백엔드 REST API"))
            .addSecurityItem(new SecurityRequirement().addList(schemeName))
            .components(new Components()
                .addSecuritySchemes(schemeName, new SecurityScheme()
                    .name(schemeName)
                    .type(SecurityScheme.Type.HTTP)
                    .scheme("bearer")
                    .bearerFormat("JWT")));
    }
}

addSecurityItem을 전역에 걸면 모든 엔드포인트에 자물쇠가 붙는다. 로그인이나 회원가입처럼 토큰이 필요 없는 엔드포인트는 전역 요구를 빼고, 보호가 필요한 컨트롤러에만 @SecurityRequirement(name = "bearer-jwt")를 다는 방식이 깔끔하다.

@SecurityRequirement(name = "bearer-jwt")
@Operation(summary = "내 프로필 조회")
@GetMapping("/me")
public MemberResponse me() { ... }

이렇게 하면 Swagger UI 상단 Authorize 창에 토큰을 한 번 붙여넣고, 이후 호출마다 Authorization: Bearer ... 헤더가 자동으로 실린다. bearerFormat은 표시용 힌트라서 실제 검증에 영향을 주지는 않는다.

그룹으로 문서 쪼개기

엔드포인트가 많아지면 하나의 스펙에 다 몰아넣기가 부담스럽다. 공개 API와 관리자 API를 따로 보여주고 싶을 때 GroupedOpenApi로 그룹을 나눈다. 경로 패턴이나 패키지 기준으로 묶을 수 있다.

@Bean
public GroupedOpenApi publicApi() {
    return GroupedOpenApi.builder()
        .group("public")
        .pathsToMatch("/api/**")
        .pathsToExclude("/api/admin/**")
        .build();
}

@Bean
public GroupedOpenApi adminApi() {
    return GroupedOpenApi.builder()
        .group("admin")
        .pathsToMatch("/api/admin/**")
        .build();
}

그룹을 정의하면 스펙 경로가 그룹별로 갈라진다. Swagger UI 우측 상단 드롭다운에서 그룹을 골라 볼 수 있다.

$ curl -s http://localhost:8080/v3/api-docs/public | head -c 80
{"openapi":"3.1.0","info":{...},"paths":{"/api/members/{id}":{...

$ curl -s http://localhost:8080/v3/api-docs/admin | head -c 80
{"openapi":"3.1.0","info":{...},"paths":{"/api/admin/members":{...

뒤에서 newman으로 테스트할 때 이 그룹별 스펙 경로가 요긴하다. 공개 API만 골라 테스트 컬렉션을 만들 수 있다.

스펙을 Postman으로 가져오기

이제 문서를 테스트로 바꾼다. Postman은 OpenAPI 스펙을 임포트하면 엔드포인트마다 요청이 채워진 컬렉션을 자동으로 만들어준다. 먼저 스펙 파일을 내려받는다.

$ curl -s http://localhost:8080/v3/api-docs/public -o openapi-public.json

Postman 앱에서 Import로 이 파일을 넣고, 임포트 옵션에서 "Generate collection from imported APIs"를 켜면 컬렉션이 생성된다. 각 요청에는 @Example로 넣어둔 예시 값이 바디에 채워진다. 여기에 로그인 후 토큰을 넣는 사전 스크립트나 응답 검증 테스트를 붙인 뒤, 컬렉션과 환경을 파일로 Export한다. 이 두 파일을 CI에서 돌릴 것이다.

sarc-collection.json     # 익스포트한 컬렉션(v2.1 포맷)
sarc-local.postman_environment.json   # baseUrl, token 등 변수

컬렉션을 익스포트할 때 포맷 버전에 주의한다. newman은 Collection v2.1 포맷을 읽는다. Postman 최신 버전에서 도입된 v3 포맷 컬렉션은 newman과 호환되지 않으니, 익스포트 시 v2.1을 선택한다.

newman으로 테스트 자동화

newman은 Postman 컬렉션을 GUI 없이 커맨드라인에서 실행하는 러너다. CI 파이프라인에 넣기 좋다. Node.js가 있으면 npm으로 설치한다.

$ npm install -g newman
$ newman --version
6.2.1

컬렉션과 환경 파일을 넘겨 실행한다. 로컬 앱이 떠 있는 상태에서 돌린다.

$ newman run sarc-collection.json \
    -e sarc-local.postman_environment.json

newman

sarc API

→ members / get member
  GET http://localhost:8080/api/members/1 [200 OK, 512B, 41ms]
  ✓  status is 200
  ✓  body has email field

→ members / get member (404)
  GET http://localhost:8080/api/members/99999 [404 Not Found, 320B, 12ms]
  ✓  status is 404

┌─────────────────────────┬──────────┐
│                        │ executed │   failed │
├─────────────────────────┼──────────┤
│              assertions │        3 │        0 │
└─────────────────────────┴──────────┘

테스트가 하나라도 실패하면 newman은 종료 코드 1로 끝난다. 그래서 CI에서 별도 판정 로직 없이 그대로 게이트로 쓸 수 있다. 응답 시간은 환경에 따라 다르지만 로컬 단순 조회면 대략 수십 밀리초 수준이다.

리포트와 CI 연결

리포터를 바꿔 JSON이나 JUnit XML로 결과를 남긴다. CI 대시보드에 테스트 결과를 표시할 때 JUnit 형식이 유용하다.

$ newman run sarc-collection.json \
    -e sarc-ci.postman_environment.json \
    -r cli,junit \
    --reporter-junit-export newman-report.xml

GitHub Actions라면 앱을 백그라운드로 띄우고, 헬스체크가 통과한 뒤 위 명령을 실행하는 스텝을 넣으면 된다. 스펙이 바뀌면 Postman에서 컬렉션을 다시 임포트해 갱신한다. 코드에서 문서가 나오고, 그 문서가 다시 테스트가 되는 셈이라 한 곳만 관리하면 된다.

정리

Spring Boot 3에서는 springdoc-openapi 2.x 스타터 하나로 Swagger UI와 OpenAPI 스펙을 자동 생성한다. 어노테이션으로 예시와 JWT 인증을 표기하고, GroupedOpenApi로 공개와 관리자 문서를 나눈 뒤, 그 스펙을 Postman으로 임포트해 newman으로 CI에서 돌리면 문서와 테스트를 한 소스에서 유지할 수 있다. 시작은 의존성 한 줄, 확인은 /swagger-ui.html 접속부터 하면 된다.

자주 묻는 질문

springfox를 쓰던 프로젝트를 Spring Boot 3로 올리면 어떻게 하나

springfox는 Spring Boot 3의 jakarta 네임스페이스를 지원하지 않아 그대로는 뜨지 않는다. springdoc-openapi 2.x로 교체한다. 어노테이션도 바뀌어서 @Api는 @Tag, @ApiOperation은 @Operation, @ApiModel은 @Schema로 옮긴다.

Swagger UI 주소로 접속하면 404가 난다

server.servlet.context-path를 설정했다면 그 컨텍스트 패스가 /swagger-ui.html 앞에 붙는다. 또 springdoc.swagger-ui.enabled나 api-docs.enabled를 false로 꺼두지 않았는지 확인한다. /swagger-ui.html은 실제로 /swagger-ui/index.html로 리다이렉트된다.

운영 환경에서 Swagger UI를 숨기고 싶다

application.yml에서 springdoc.swagger-ui.enabled와 springdoc.api-docs.enabled를 false로 둔다. 프로파일별 설정 파일로 운영에서만 끄고 개발에서는 켜두는 구성이 흔하다.

newman이 컬렉션을 못 읽는다는 오류가 난다

newman은 Collection v2.1 포맷을 읽는다. Postman 최신 버전에서 도입된 v3 포맷 컬렉션은 호환되지 않으므로, 익스포트할 때 v2.1을 선택하거나 Postman CLI 사용을 검토한다.

그룹별 OpenAPI 스펙은 어떤 경로로 받나

GroupedOpenApi로 group("public")을 정의하면 기본 스펙 경로 뒤에 그룹명이 붙어 /v3/api-docs/public으로 받을 수 있다. Swagger UI 우측 상단 드롭다운에서도 그룹을 골라 볼 수 있다.

관련 글

댓글 0

로그인 후 댓글을 남길 수 있습니다.

아직 댓글이 없습니다.