본문 바로가기
Development

Spring Boot 3에서 Swagger UI 자동 생성

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

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 기준으로 설명한다.

의존성 추가, 경로 확인, 어노테이션 보강, JWT 표기, 그룹 분리, Postman과 newman 연결의 6단계 흐름도
의존성 한 줄로 시작한 문서가 어떻게 CI의 테스트 게이트까지 이어지는지 보여준다.

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

추가 설정 없이 앱을 띄우면 두 경로가 열린다. 스펙 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이 그 제약을 스펙의 requiredmaxLength로 자동 반영한다. 그래서 @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

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

아직 댓글이 없습니다.