Spring Boot 3.x.x ๋ถํฐ๋ JavaEE → Jakarta EE๋ก ๊ต์ฒด๋์์ต๋๋ค.
๊ทธ๋ ๊ธฐ ๋๋ฌธ์ ์ด์ ์ ์ฌ์ฉํ๋ Springfox๋ ํ์ฌ Spring Boot 3.x.x์ ๋ํ ์ ๋ฐ์ดํธ๊ฐ ์ด๋ค์ง์ง ์๊ณ ํ์์ ์๋ ์ฌ์ฉํ ์ ์์์ต๋๋ค. Springdoc ๊ณต์๋ฌธ์์์ ์ด๋ป๊ฒ ์ ์ฉํ ์ ์๋์ง์ ๋ํ ์์ธํ ๋ฐฉ๋ฒ์ด ๋์์์ผ๋ ์ฐธ๊ณ ํ์๋ฉด ์ข์ ๊ฒ ๊ฐ์ต๋๋ค.
์์กด์ฑ ์ถ๊ฐ
Spring Boot 3.x.x๋ถํฐ๋ ์๋ ์์กด์ฑ์ ์ถ๊ฐํด์ฃผ๋ฉด Swagger-ui ์ค์ ์ ๋์ ๋๋ค.
implementation "org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2"
- springdoc-openapi-ui → springdoc-openapi-starter-webmvc-ui ๋ก ๊ต์ฒด๋์์ต๋๋ค.
- springdoc-openapi-webflux-ui → springdoc-openapi-starter-webflux-ui ๋ก ๊ต์ฒด๋์์ต๋๋ค.
ํด๋น ์์กด์ฑ์ ์ถ๊ฐํด์ฃผ๋ ๊ฒ๋ง์ผ๋ก๋ /swagger-ui.html ์ ๊ทผ์ด ๊ฐ๋ฅํฉ๋๋ค.
์ค์ ํ์ผ
ํ์ฌ API ๋ฌธ์์ ์ค๋ช ์ด๋, ๋ฒ์ ๋ฑ๋ฑ ๋ ์ธ๋ถ์ ์ผ๋ก ์ค์ ํด์ค ์ ์์๊น์?
๋ฐ๋ก OpenApi๋ฅผ ๋น์ผ๋ก ๋ฑ๋กํ๋ ๋ฐฉ๋ฒ์ด ์์ต๋๋ค.
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.security.SecurityScheme;
import io.swagger.v3.oas.models.servers.Server;
import org.springdoc.core.models.GroupedOpenApi;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.List;
@Configuration
public class SpringdocConfig {
@Bean
public GroupedOpenApi publicApi() {
return GroupedOpenApi.builder()
.addOpenApiCustomizer(openApi -> openApi
.info(new Info().title("TEST API DOCS")
.description("ํ
์คํธ API ๋ฌธ์์
๋๋ค.")
.version("v1.0"))
.group("Test Group")
.packagesToScan("org.example.controller")
.displayName("This is Test API")
.build();
}
}
application.yml ์ค์
springdoc:
packages-to-scan: org.example.controller
default-consumes-media-type: application/json;charset=UTF-8
default-produces-media-type: application/json;charset=UTF-8
swagger-ui:
path: /docs/test.html
tags-sorter: alpha
operations-sorter: alpha
syntax-highlight:
activated: true
api-docs:
path: /v3/docs
groups:
enabled: true
cache:
disabled: true
- springdoc.swagger-ui.tags-sorter: URI๋ฅผ ๋ค์ ๊ธฐ์ค์ผ๋ก ์ ๋ ฌ
- springdoc.swagger-ui.operations-sorter : HTTP ๋ฉ์๋๋ฅผ ๋ค์ ๊ธฐ์ค์ผ๋ก ์ ๋ ฌ
- alpha: ์ํ๋ฒณ ์์ผ๋ก ์ ๋ ฌ
- method: HTTP ๋ฉ์๋ ์์ผ๋ก ์ ๋ ฌ
๋ ๋ง์ properties ์ค์ ์ ๊ณต์๋ฌธ์์์ ํ์ธํ์๊ธธ ๋ฐ๋๋๋ค.
Spring Security๋ฅผ ์ฌ์ฉํ๊ณ ์๋ค๋ฉด?
implementation 'org.springdoc:springdoc-openapi-security:1.6.14'
์ด๋ฏธ springdoc-openapi-starter-webmvc-ui ์์กด์ฑ์ ์ฌ์ฉ์ค์ด๋ผ๋ฉด, ๋ฐ๋ก ์์กด์ฑ์ ์ถ๊ฐํด์ค ํ์๋ ์์ต๋๋ค. ์ฐ๋ฆฌ๊ฐ ์ถ๊ฐํด์คฌ๋ ์์กด์ฑ์ผ๋ก ์ธํด @AuthenticationPrincipal ์ด๋ ธํ ์ด์ ์ ์๋์ผ๋ก swagger-ui์ ignore ๋๋ฏ๋ก ๋ฐ๋ก ์ฒ๋ฆฌํด์ฃผ์ง ์์๋ ๋ฉ๋๋ค.
Javadoc ์ผ๋ก ๋ฌธ์๋ฅผ ๊พธ๋ฉฐ๋ณด์
springdoc-openapi-starter-webmvc-ui ์์กด์ฑ์ ์ฌ์ฉ์ค์ด๋ผ๋ฉด, ์๋ ์์กด์ฑ๋ค์ ์ถ๊ฐํด์ค์ javadoc์ ํ์ฑํ ์์ผ์ค์ผ ํฉ๋๋ค.
annotationProcessor 'com.github.therapi:therapi-runtime-javadoc-scribe:0.13.0'
implementation 'com.github.therapi:therapi-runtime-javadoc:0.13.0'
์ด์ ์์ฑํ javadoc์ ๋ค์๊ณผ ๊ฐ์ด ์ฒ๋ฆฌ๋ฉ๋๋ค.
- method comment: @Operation(description={}) ์ผ๋ก ์นํ๋ฉ๋๋ค.
- @return: @Operation(response={}) ์ผ๋ก ์นํ๋ฉ๋๋ค.
- attribute comment: @Schema(description={}) ์ผ๋ก ์นํ๋ฉ๋๋ค.
๐ก ๋ง์ฝ Swagger Annotation๊ณผ javadoc์ด ๋์์ ์กด์ฌํ๋ค๋ฉด, Swagger Annotation์ด ์ฐ์ ์ผ๋ก ์ ์ฉ๋ฉ๋๋ค.
Authorization ์ถ๊ฐ
JWT๋ฅผ ์ฌ์ฉํด ์ธ๊ฐ๋ฅผ ํ๊ณ ์๋ค๋ฉด ๋ค์๊ณผ ๊ฐ์ด ์ค์ ์ ์ถ๊ฐํ๋ฉด Swagger์์ ์ ์ญ ์ธ์ฆ์ ๋ณด๋ฅผ ์ค์ ํ์ฌ ์ธ์ฆ์ด ํ์ํ API๋ฅผ ํธ์ถํ ์ ์์ต๋๋ค.
@Configuration
public class SpringdocConfig {
@Bean
public GroupedOpenApi publicApi() {
return GroupedOpenApi.builder()
.addSecurityItem(new SecurityRequirement().addList("Authorization"))
.components(new Components().addSecuritySchemes("Authorization", new SecurityScheme()
.name("Authorization")
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")))
.addOpenApiCustomizer(openApi -> openApi
.info(new Info().title("TEST API DOCS")
.description("ํ
์คํธ API ๋ฌธ์์
๋๋ค.")
.version("v1.0"))
.group("Test Group")
.packagesToScan("org.example.controller")
.displayName("This is Test API")
.build();
}
}
๊ณตํต Pageable ์๋ต ์ด๋ ธํ ์ด์ ๊ฐ๋ฐ
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Parameters({
@Parameter(name = "page", description = "ํ์ด์ง ๋ฒํธ (0..N) [๊ธฐ๋ณธ๊ฐ: 0]", schema = @Schema(type = "integer", defaultValue = "0", nullable = true)),
@Parameter(name = "size", description = "ํ์ด์ง ๋ฒํธ (0..100) [๊ธฐ๋ณธ๊ฐ: 10]", schema = @Schema(type = "integer", defaultValue = "10")),
@Parameter(name = "sort", description = "์ ๋ ฌ (์ปฌ๋ผ,asc|desc) [์์] ์ด๋ฆ์ ๋ด๋ฆผ์ฐจ์ ์ ๋ ฌ ex) 'name,desc'", schema = @Schema(type = "array", name = "์ ๋ ฌ (์ปฌ๋ผ,asc|desc) [์์] ์ด๋ฆ์ ๋ด๋ฆผ์ฐจ์ ์ ๋ ฌ ex)")),
@Parameter(name = "pageable", hidden = true)
})
public @interface ApiPageable {
}
๊ณตํต API ์๋ต ์ด๋ ธํ ์ด์ ๊ฐ๋ฐ
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@ApiResponse(responseCode = "401", description = "UnAuthorized", useReturnTypeSchema = true, content = @Content(schema = @Schema(implementation = YourExample.class), mediaType = APPLICATION_JSON_VALUE, examples = @ExampleObject(value = "example")))
@ApiResponse(responseCode = "403", description = "Forbidden", useReturnTypeSchema = true, content = @Content(schema = @Schema(implementation = YourExample.class),mediaType = APPLICATION_JSON_VALUE, examples = @ExampleObject(value = "example")))
@ApiResponse(responseCode = "500", description = "Internal Server Error", content = @Content(schema = @Schema(implementation = YourExample.class)))
public @interface ApiCommonResponse {
}
Controller ๋ช ์ธ
GET ๋ฐฉ์
@Operation(
summary = "ํ์ ์กฐํ API",
description = "ํ์์ ์กฐํํ ์ ์์ต๋๋ค.",
responses = {
@ApiResponse(responseCode = "200", description = "Success", useReturnTypeSchema = true, headers = @Header(name = "Authorization", required = true), content = @Content(schema = @Schema(implementation = YourExample.class), mediaType = APPLICATION_JSON_VALUE, examples = @ExampleObject(value = "example"))),
@ApiResponse(responseCode = "404", description = "NotFound", useReturnTypeSchema = true, content = @Content(schema = @Schema(implementation = YourExample.class), examples = @ExampleObject(value = "example"))),})
@Parameters({
@Parameter(name = "username", description = "์ฌ์ฉ์ ์ด๋ฆ", example = "john", required = true),
@Parameter(name = "age", description = "์ฌ์ฉ์ ๋์ด", example = "20", required = true)
})
@ApiPageable
@ApiCommonResponse
@GetMapping("/members")
ResponseEntity<MemberDto> searchMembers(
@RequestParam("username") String username,
@RequestParam("age") int age,
@PageableDefault Pageable pageable
){
...
}
POST ๋ฐฉ์
@Operation(summary = "ํ์์ ์์ฑํ๋ API",
requestBody = @RequestBody(description = "ํ์ ์์ฑ์ ์ํ DTO"),
responses = {
@ApiResponse(responseCode = "200", description = "Success", useReturnTypeSchema = true, headers = @Header(name = "Authorization", required = true), content = @Content(schema = @Schema(implementation = CreateProjectResponse.class), mediaType = APPLICATION_JSON_VALUE, examples = @ExampleObject(value = "{\\"success\\":true,\\"error\\":null,\\"message\\":{\\"projectId\\":1}"))),
...
})
@ApiCommonResponse
ResponseEntity<Long> createProject(
@Valid @RequestBody CreateMemberDto request
) {
...
}