๐Ÿ“‘ Swagger โ€“ API ๋ฌธ์„œ ์ž๋™ํ™”ํ•˜๊ธฐ

๊น€๊ณต์˜ยท2024๋…„ 5์›” 27์ผ

how-to

๋ชฉ๋ก ๋ณด๊ธฐ
1/12
post-thumbnail

ํ˜ผ์ž ๊ฐœ๋ฐœํ•  ๋•Œ์—๋Š” ๊ทธ๋ƒฅ ๋งŒ๋“ค๋ฉด ๋˜์ง€๋งŒ, ํ˜‘์—… ์‹œ์—๋Š” API ๋ฌธ์„œ๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๋Š” ๊ฒƒ์ด ๊ฝค๋‚˜ ์‹ ๊ฒฝ ์“ฐ์ด๋Š” ์ผ์ด๋‹ค. ๊ณต์œ  ํด๋ผ์šฐ๋“œ๋‚˜ ํ”Œ๋žซํผ(notion, confluence ๋“ฑ)์„ ํ™œ์šฉํ•˜๋Š” ๊ฒƒ๋„ ์ข‹์€ ๋ฐฉ๋ฒ•์ด์ง€๋งŒ ๋” ์‰ฝ๊ฒŒ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋Š” ๋ฐฉ๋ฒ•์ด ์žˆ๋‹ค! ๋ฌผ๋ก  ๋‚œ ๊ณต์œ  ๋ฌธ์„œ์— ์ž‘์„ฑํ•ด๋‘๊ณ , Swagger๋„ ์‚ฌ์šฉํ•œ๋‹ค. ๊ฐ๊ฐ์˜ ๋ฐฉ์‹์—๋Š” ์žฅ๋‹จ์ ์ด ์žˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

๐Ÿค” Swagger?

REST web service๋ฅผ ์„ค๊ณ„, ๋นŒ๋“œ, ๋ฌธ์„œํ™” ๋“ฑ์˜ ์ผ์„ ๋„์™€์ฃผ๋Š” ๋Œ€ํ˜• ๋„๊ตฌ ์ƒํƒœ๊ณ„์˜ ์ง€์›์„ ๋ฐ›๋Š” ์˜คํ”ˆ์†Œ์Šค ์†Œํ”„ํŠธ์›จ์–ด ํ”„๋ ˆ์ž„ ์›Œํฌ

  • API ์ •๋ณด ์ž๋™ ๊ฐฑ์‹ 
  • API๋ฅผ ํ†ตํ•ด ํŒŒ๋ผ๋ฏธํ„ฐ, ์‘๋‹ต, ์ •๋ณด, ์˜ˆ์‹œ ๋“ฑ spec ์ •๋ณด ์ „๋‹ฌ์— ์šฉ์ด
  • ์‹ค์ œ ์‚ฌ์šฉ ๋˜๋Š” ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ํ…Œ์ŠคํŠธ๋ฅผ ํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋Šฅ ์ง€์›
  • Java์— ์ข…์†๋œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ์•„๋‹ˆ๊ธฐ ๋•Œ๋ฌธ์— ๋‹ค๋ฅธ ์–ธ์–ด๋กœ ๊ฐœ๋ฐœ๋œ ์›น ํ”„๋ ˆ์ž„์›Œํฌ์—์„œ ๋™์ผํ•˜๊ฒŒ ๊ตฌ์„ฑ ๊ฐ€๋Šฅ
  • FE์™€ BE ๊ฐ„์˜ ๊ฐœ๋ฐœ ์ง„ํ–‰ ์ƒํ™ฉ ๋ฐ ํŽธ๋ฆฌํ•œ API ์„ธ๋ถ€ ์‚ฌํ•ญ ๊ณต์œ  ๊ฐ€๋Šฅ

Swagger๊ฐ€ ์ง€์›ํ•˜๋Š” ๊ธฐ๋Šฅ

  • API Design
    • Swagger-editor๋ฅผ ํ†ตํ•ด API๋ฅผ ๋ฌธ์„œํ™”ํ•˜๊ณ  ๋น ๋ฅด๊ฒŒ ๋ช…์„ธ ์ž‘์„ฑ ๊ฐ€๋Šฅ
  • API Development
    • Swagger-codepen์„ ํ†ตํ•ด ์ž‘์„ฑ๋œ ๋ฌธ์„œ์˜ SDK๋ฅผ ์ƒ์„ฑ โ†’ ๋นŒ๋“œ ํ”„๋กœ์„ธ์Šค ๊ฐ„์†Œํ™”
  • API Documentation
  • API Testing
    • Swagger-Inspector๋ฅผ ํ†ตํ•ด API ์‹œ๊ฐํ™” ๋ฐ ๋น ๋ฅธ ํ…Œ์ŠคํŒ… ๊ฐ€๋Šฅ
  • Standardize
    • Swagger-hub๋ฅผ ํ†ตํ•ด ๊ฐœ์ธ, ํŒ€์›๋“ค์ด API ์ •๋ณด๋ฅผ ๊ณต์œ 

๐Ÿงญ ํ™˜๊ฒฝ ์„ค์ •

springfox-boot-starter ์‚ฌ์šฉ ์‹œ Spring Boot 2.5.x ๋ฒ„์ „ ์ดํ›„๋ถ€ํ„ฐ๋Š” Spring๊ณผ Swagger ๊ฐ„์˜ ๋ฒ„์ „ ํ˜ธํ™˜์ด ๋˜์ง€ ์•Š์•„ ์˜ค๋ฅ˜ ๋ฐœ์ƒ โ†’ Spring Boot 2.4.x ๋ฒ„์ „์œผ๋กœ ๋‚ฎ์ถฐ์„œ ํ•ด๊ฒฐ ํ•„์š”
์ฐธ๊ณ  : Spring Boot 2.6.2์— Swagger ์ ์šฉ ์‹œ ์˜ค๋ฅ˜, SpringBoot 2.6์—์„œ swagger 3.0 ์‚ฌ์šฉ

์ด ๊ธ€์—์„œ๋Š” springfox ๋Œ€์‹  springdoc์„ ์‚ฌ์šฉํ•œ๋‹ค.

Gradle

dependencies {
	...
	implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0'
	...
}

Maven


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

application.yml

springdoc:
	api-docs:
		path: /api-docs
		group:
			enable: true
	swagger-ui:
		path: /swagger-ui-custom.html
		enabled: true
		groups-order: ASC
		tags-sorter: alpha
		operations-sorter: alpha
		display-request-duration: true
		doc-expansion: none
	cache:
		disabled: true
	model-and-view-allowed: true

SwaggerConfig.java


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.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import io.swagger.v3.oas.models.servers.Server;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SwaggerConfig {

	@Bean
    public OperationCustomizer operationCustomizer() {
        return (operation, handlerMethod) -> {
            this.addResponseBodyWrapperSchemaExample(operation);
            return operation;
        };
    }

    private void addResponseBodyWrapperSchemaExample(Operation operation) {
        final Content content = operation.getResponses().get("200").getContent();
        if (content != null) {
            content.forEach((mediaTypeKey, mediaType) -> {
                Schema<?> originalSchema = mediaType.getSchema();
                Schema<?> wrapperSchema = wrapSchema(originalSchema);
                mediaType.setSchema(wrapperSchema);
            });
        }
    }

    private Schema<?> wrapSchema(Schema<?> originalSchema) {
        Schema<?> wrapperSchema = new Schema<>();
        wrapperSchema.addProperties("status", new Schema<>().type("integer").example(200));
        wrapperSchema.addProperties("message", new Schema<>().type("string"));
        wrapperSchema.addProperties("data", originalSchema);
        return wrapperSchema;
    }

    @Bean
    public OpenAPI openAPI() {
        Info info = new Info()
                .version("v1.0.0")
                .title("API")
                .description("");

        SecurityScheme auth = new SecurityScheme()
                .type(SecurityScheme.Type.APIKEY).in(SecurityScheme.In.COOKIE).name("JSESSIONID");
        SecurityRequirement securityRequirement = new SecurityRequirement().addList("sessionAuth"); // ํ—ค๋”์— ํ† ํฐ ํฌํ•จ

        // When Using JWT, Activate Below Code
//  	String jwt = "JWT";
//  	SecurityRequirement securityRequirement = new SecurityRequirement().addList(jwt); // ํ—ค๋”์— ํ† ํฐ ํฌํ•จ
//  	Components components = new Components().addSecuritySchemes(jwt, new SecurityScheme()
//   		.name(jwt)
//   		.type(SecurityScheme.Type.HTTP)
//   		.scheme("Bearer")
//   		.bearerFormat("JWT")
//  	);

        return new OpenAPI()
                .addServersItem(new Server().url("/"))
                .info(info)
                .addSecurityItem(securityRequirement)
                .components(new Components().addSecuritySchemes("sessionAuth", auth))
                ;

    }

}

๐Ÿš€ How to use

Controller

@RestController
@RequiredArgsConstructor
@RequestMapping("example")
@Tag(name = "์˜ˆ์‹œ ์ปจํŠธ๋กค๋Ÿฌ")
public class ExampleController {

    private final ExampleService exampleService;

    @GetMapping("{exampleId}")
    @Operation(summary = "์˜ˆ์‹œ ์กฐํšŒ", description = "์˜ˆ์‹œ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.")
    @ApiResponse(responseCode = "200", description = "์„ฑ๊ณต", content = @Content(schema = @Schema(implementation = ExampleResponseDto.class)))
    public Response<?> exampleMethod(@RequestBody ExampleRequestDto exampleRequestDto, @PathVariable Long exampleId) {
        return Response.ok(exampleService.exampleMethod(exampleRequestDto), "์„ฑ๊ณต์ ์œผ๋กœ ์˜ˆ์‹œ๋ฅผ ๋ฐ˜ํ™˜ํ•˜์˜€์Šต๋‹ˆ๋‹ค.");
    }
}

DTO

DTO class์—์„œ๋Š” request๋ฅผ ์œ„ํ•œ example value๋ฅผ ์„ค์ •ํ•  ์ˆ˜ ์žˆ๋‹ค. ๋ฏธ๋ฆฌ ์ž‘์„ฑํ•ด์„œ ์ „๋‹ฌํ•ด๋‘๋ฉด ๋™์ž‘ ํ…Œ์ŠคํŠธ ์‹œ ํŽธํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๊ถŒ์žฅํ•œ๋‹ค.

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class ExampleRequestDto {

    @Schema(description = "์˜ˆ์‹œ ID", example = "1")
    private Long exampleId;

    @Schema(description = "์˜ˆ์‹œ ์ œ๋ชฉ", example = "title")
    private String exampleTitle;
}

DTO ํด๋ž˜์Šค๋Š” ๋ฌผ๋ก  DTO ํด๋ž˜์Šค์—์„œ ์‚ฌ์šฉํ•˜๋Š” ๋‹ค๋ฅธ ํด๋ž˜์Šค ๋‚ด๋ถ€์—๋„ @Schema annotation์„ ํ™œ์šฉํ•˜๋ฉด ๋™์ผํ•˜๊ฒŒ ์„ค์ •ํ•  ์ˆ˜ ์žˆ๋‹ค.

Swagger annotation

annotationdescription
@Apiํด๋ž˜์Šค๋ฅผ ์Šค์›จ๊ฑฐ์˜ ๋ฆฌ์†Œ์Šค๋กœ ํ‘œ์‹œ
โ€ข ํ•ด๋‹น ํด๋ž˜์Šค๊ฐ€ Swagger ๋ฆฌ์†Œ์Šค๋ผ๋Š” ๊ฒƒ์„ ๋ช…์‹œ
โ€ข ํด๋ž˜์Šค ์œ„์— ์„ ์–ธ
@ApiOperationํŠน์ • ๊ฒฝ๋กœ์˜ operation HTTP method ์„ค๋ช…
โ€ข ํ•œ ๊ฐœ์˜ Operation์„ ์„ ์–ธ
โ€ข ๋ฉ”์†Œ๋“œ ์œ„์— ์„ ์–ธ
@ApiParamoperation parameter์— ๋ฉ”ํƒ€ ๋ฐ์ดํ„ฐ ์„ค๋ช…
โ€ข ํŒŒ๋ผ๋ฏธํ„ฐ์— ๋Œ€ํ•œ ์ •๋ณด ๋ช…์‹œ
โ€ข method ์•ˆ์—, ํŒŒ๋ผ๋ฏธํ„ฐ ์œ„์— ์„ ์–ธ
@ApiResponseoperation์˜ ์‘๋‹ต ์ง€์ •
@ApiModelProperty๋ชจ๋ธ์˜ ์†์„ฑ ๋ฐ์ดํ„ฐ๋ฅผ ์„ค๋ช…
โ€ข Dto class ์•ˆ์˜ property ์œ„์— ์„ ์–ธ
@ApiImplicitParammethod ๋‹จ์œ„์˜ operation parameter ์„ค๋ช…
โ€ข ํŒŒ๋ผ๋ฏธํ„ฐ์— ๋Œ€ํ•œ ์ •๋ณด ๋ช…์‹œ
โ€ข method ์œ„์— ์„ ์–ธ
@ApiImplicitParamsmethod ๋‹จ์œ„์˜ operation parameter ์—ฌ๋Ÿฌ ๊ฐœ ์„ค๋ช…
โ€ข ํŒŒ๋ผ๋ฏธํ„ฐ์— ๋Œ€ํ•œ ์ •๋ณด ๋ช…์‹œ
โ€ข method ์œ„์— ์„ ์–ธ

SwaggerConfig class method

methoddescription
useDefaultResponseMessages()false : Swagger์—์„œ ์ œ๊ณตํ•˜๋Š” ์‘๋‹ต ์ฝ”๋“œ (200, 401, 403, 404)์— ๋Œ€ํ•œ ๊ธฐ๋ณธ ๋ฉ”์„ธ์ง€ ์ œ๊ฑฐ
groupName()Docket Bean์ด ํ•œ ๊ฐœ์ผ ๊ฒฝ์šฐ ์ƒ๋žต ๊ฐ€๋Šฅ,
๋‘˜ ์ด์ƒ์ผ ๊ฒฝ์šฐ ์ถฉ๋Œ ๋ฐฉ์ง€๋ฅผ ์œ„ํ•ด ์„ค์ •
select()โ€ข ApiSelectorBuilder๋ฅผ ์ƒ์„ฑ
โ€ข apis()์™€ paths()๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์คŒ
apis()โ€ข api spec์ด ์ž‘์„ฑ๋˜์–ด ์žˆ๋Š” ํŒจํ‚ค์ง€ ์ง€์ •
โ€ข ์ปจํŠธ๋กค๋Ÿฌ๊ฐ€ ์กด์žฌํ•˜๋Š” ํŒจํ‚ค์ง€๋ฅผ base package๋กœ ์ง€์ •
-> ํ•ด๋‹น ํŒจํ‚ค์ง€์— ์กด์žฌํ•˜๋Š” API ๋ฌธ์„œํ™”
โ€ข RequestHandlerSelectors.any() : ๋ชจ๋“  ํŒจํ‚ค์ง€๋ฅผ base package๋กœ ์ง€์ •ํ•ด API ๋ฌธ์„œํ™”
paths()โ€ข apis()๋กœ ์„ ํƒ๋˜์–ด์ง„ API ์ค‘ ํŠน์ • path ์กฐ๊ฑด์— ๋งž๋Š” API๋“ค์„ ๋‹ค์‹œ ํ•„ํ„ฐ๋ง -> ๋ฌธ์„œํ™”
โ€ข PathSelectors.any()๋กœ ์„ค์ •ํ•˜๋ฉด ํŒจํ‚ค์ง€ ์•ˆ์— ๋ชจ๋“  API๋ฅผ ํ•œ ๋ฒˆ์— ๋ณผ ์ˆ˜ ์žˆ์Œ
apiInfo()์ œ๋ชฉ, ์„ค๋ช… ๋“ฑ ๋ฌธ์„œ์— ๋Œ€ํ•œ ์ •๋ณด๋“ค์„ ์„ค์ •ํ•˜๊ธฐ ์œ„ํ•ด ํ˜ธ์ถœ

Execute

  1. application์„ ์‹คํ–‰ํ•œ๋‹ค.
  2. ์•„๋ž˜ url ์ค‘ ํ•˜๋‚˜์— ์ ‘์†ํ•œ๋‹ค.
  3. Swagger-UI ํ™”๋ฉด์„ ๋ณผ ์ˆ˜ ์žˆ๋‹ค.
  4. ์‚ฌ์šฉ์ž API๋ฅผ ํด๋ฆญํ•˜๋ฉด ์ƒ์„ธ API ๋ชฉ๋ก์„ ๋ณผ ์ˆ˜ ์žˆ๋‹ค.

Reference : Swagger ๊ณต์‹ ๋ฌธ์„œ, springdoc-openapi repository, springdoc.org, Baeldung OpenAPI 3.0 ๋ฌธ์„œ

profile
๋‚˜๋Š”์•ผ ๋งํ•˜๋Š” ๊ฐœ๋ฐœ(๊ฐ)์ž

0๊ฐœ์˜ ๋Œ“๊ธ€