BackEnd๐ŸŒฑ/Spring

ํ˜‘์—…์„ ์œ„ํ•ด Swagger ์ข€ ๋” ์ž˜ ์‚ฌ์šฉํ•ด๋ณด๊ธฐ

dkswnkk 2024. 5. 15. 22:03

๋ชฉ์ฐจ

  1. API ๊ทธ๋ฃนํ™”
  2. API ๋ฒ„์ „ ๊ด€๋ฆฌ ์‹œ @Deprecated ํ™œ์šฉํ•˜๊ธฐ
  3. ๋ช…์„ธ๋งŒ ๋จผ์ € ์ „๋‹ฌํ•˜๊ธฐ
  4. Authorize์— jwt ๋„ฃ์„ ๋•Œ prefix์— Bearer ์ƒ๋žต์‹œํ‚ค๊ธฐ
  5. ๋ธŒ๋ผ์šฐ์ € ์ƒˆ๋กœ๊ณ ์นจ ํ›„์—๋„ ์ธ์ฆ์ •๋ณด ์œ ์ง€์‹œํ‚ค๊ธฐ
  6. ๊ธฐํƒ€ ์ž์ž˜ํ•œ ์ƒ์„ธ ์„ค์ •๋“ค

 

 

1. API ๊ทธ๋ฃนํ™”

Swagger์—์„œ API๋ฅผ ๊ทธ๋ฃนํ™”ํ•˜๋ฉด ์—ฌ๋Ÿฌ ์—”๋“œํฌ์ธํŠธ๋ฅผ ๋…ผ๋ฆฌ์ ์ธ ๊ทธ๋ฃน์œผ๋กœ ๋ฌถ์–ด ๊ด€๋ฆฌํ•˜๊ณ  ๋ฌธ์„œ๋ฅผ ์ฒด๊ณ„์ ์œผ๋กœ ์ •๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์„ค์ • ํ›„์—๋Š” ์œ„ ์ด๋ฏธ์ง€์ฒ˜๋Ÿผ ๊ด€๋ จ๋œ ์—”๋“œํฌ์ธํŠธ๋งŒ ๋ณผ ์ˆ˜ ์žˆ์–ด ์›ํ•˜๋Š” API๋ฅผ ์‰ฝ๊ฒŒ ์ฐพ๊ณ  ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. Swagger์—์„œ API๋ฅผ ๊ทธ๋ฃนํ™”ํ•˜๋Š” ๋ฐฉ๋ฒ•์€ ์–ด๋–ค ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๋Š๋ƒ์— ๋”ฐ๋ผ ๋‹ค๋ฅด๊ธฐ ๋•Œ๋ฌธ์— ํ™•์ธํ•ด์„œ ์ ์šฉํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.

springfox-swagger2๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ

@Configuration
@EnableSwagger2
public class SwaggerConfig {

    @Bean
    public Docket appApi() {
        return new Docket(DocumentationType.SWAGGER_2)
                .groupName("APP API")
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.example.app"))
                .paths(PathSelectors.any())
                .build();
    }

    @Bean
    public Docket totalApi() {
        return new Docket(DocumentationType.SWAGGER_2)
                .groupName("TOTAL API")
                .select()
                .apis(RequestHandlerSelectors.any())
                .paths(PathSelectors.any())
                .build();
    }
}

springdoc-openapi๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ

@Configuration
public class SwaggerConfig {

    @Bean
    public GroupedOpenApi appApi() {
        return GroupedOpenApi.builder()
                             .group("APP API")
                             .pathsToMatch("/app/**")
                             .build();
    }

    @Bean
    public GroupedOpenApi totalApi() {
        return GroupedOpenApi.builder()
                             .group("TOTAL API")
                             .pathsToMatch("/**")
                             .build();
    }
}

 

 

2. API ๋ฒ„์ „ ๊ด€๋ฆฌ ์‹œ @Deprecated ํ™œ์šฉํ•˜๊ธฐ

ํŒ€ ๋‚ด์˜ ๋ฒ„์ „ ๊ด€๋ฆฌ ์ „๋žต์— ๋”ฐ๋ผ ๋‹ค๋ฅด๊ฒ ์ง€๋งŒ API ๋ฒ„์ „์„ ๊ด€๋ฆฌํ•  ๋•Œ ์ผ๋ฐ˜์ ์œผ๋กœ @Deprecated ์–ด๋…ธํ…Œ์ด์…˜์„ ํ™œ์šฉํ•˜๋Š” ๊ฒƒ์€ ๊ฝค๋‚˜ ์œ ์šฉํ•ฉ๋‹ˆ๋‹ค.

@Tag(name = "[APP] ํ…Œ์ŠคํŠธ")
@RestController
@RequestMapping("/app")
public class TestController {

    @Deprecated
    @GetMapping(Version.V1 + "/test")
    public void testV1() {
    }

    @GetMapping(Version.V2 + "/test")
    public void testV2() {
    }
}

class Version {
    public static final String V1 = "/v1";
    public static final String V2 = "/v2";
}

์œ„์˜ ์˜ˆ์ œ์—์„œ๋Š” /v1/app/test ์—”๋“œํฌ์ธํŠธ๊ฐ€ ์ด์ œ /v2/app/test๋กœ ๋ณ€๊ฒฝ๋˜์–ด์•ผ ํ•œ๋‹ค๋Š” ๊ฐ€์ • ํ•˜์— V1 ๋ฒ„์ „์˜ API์— @Deprecated ์–ด๋…ธํ…Œ์ด์…˜์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด Swagger UI์ƒ์—์„œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์ด ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.

@Deprecated ์–ด๋…ธํ…Œ์ด์…˜์ด ๋ถ™์€ ์—”๋“œํฌ์ธํŠธ๋Š” ๋ธ”๋ผ์ธ๋“œ ์ฒ˜๋ฆฌ๋˜์ง€๋งŒ, ์—ฌ์ „ํžˆ Swagger ์ƒ์—์„œ API ํ…Œ์ŠคํŠธ๋Š” ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด ๋ช…์„ธ๋ฅผ ์ „๋‹ฌ๋ฐ›์€ ๊ฐœ๋ฐœ์ž์—๊ฒŒ V1์€ ๊ณง ์‚ฌ๋ผ์งˆ ๊ฒƒ์ด๋ผ๋Š” ๊ฒƒ์„ ์•Œ๋ฆฌ๊ณ , ๋” ์ด์ƒ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋„๋ก ์œ ๋„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

@Deprecated ์–ด๋…ธํ…Œ์ด์…˜์˜ ์†์„ฑ

์ถ”๊ฐ€๋กœ @Deprecated ์–ด๋…ธํ…Œ์ด์…˜์—๋Š” since์™€ forRemoval์ด๋ผ๋Š” ์˜ต์…˜์ด ์กด์žฌํ•ฉ๋‹ˆ๋‹ค.

@Deprecated(since = "v1.1.0", forRemoval = true)
@GetMapping(Version.V1 + "/test")
public void testV1() {

}

์ด ์˜ต์…˜๋“ค์€ ์ฝ”๋“œ์˜ ๋™์ž‘์— ์ง‘์ ์ ์œผ๋กœ ์˜ํ–ฅ์„ ๋ฏธ์น˜์ง€๋Š” ์•Š์ง€๋งŒ, ์ด ์˜ต์…˜๋“ค์„ ํ†ตํ•ด ์ฝ”๋“œ๋ฅผ ๋ณด๋Š” ๊ฐœ๋ฐœ์ž๋“ค์—๊ฒŒ ์œ ์šฉํ•œ ์ •๋ณด๋ฅผ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ผ๋ฐ˜์ ์œผ๋กœ since์—๋Š” @Deprecated๊ฐ€ ๋ถ™์—ฌ์ง€๊ฒŒ ๋œ ์Šคํ”„๋ฆฐํŠธ ํ˜น์€ ์ œํ’ˆ์˜ ๋ฒ„์ „์„ ๋ช…์‹œํ•˜๊ณ , forRemoval์ด true์ธ ๊ฒฝ์šฐ์—๋Š” ํ•ด๋‹น API๊ฐ€ ๋‹ค์Œ ๋ฒ„์ „์—์„œ ๊ณง๋ฐ”๋กœ ์ œ๊ฑฐ๋  ์ˆ˜ ์žˆ์Œ์„ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค.

๋ณดํ†ต forRemoval๊ฐ€ false์ผ ๊ฒฝ์šฐ์—๋Š” @Deprecated๋ฅผ ๋ถ™์ธ ๊ฐœ๋ฐœ์ž๊ฐ€ ๋‹ค์Œ ๋ฒ„์ „์—์„œ ์ง์ ‘ ์ฝ”๋“œ๋ฅผ ์ œ๊ฑฐํ•˜๊ฑฐ๋‚˜ ํ•ด๋‹น ํžˆ์Šคํ† ๋ฆฌ๋ฅผ ์•„๋Š” ๊ฐœ๋ฐœ์ž๊ฐ€ ์‚ญ์ œํ•ฉ๋‹ˆ๋‹ค. true์ผ ๊ฒฝ์šฐ์—๋Š” '์ด ์ฝ”๋“œ๋ฅผ ์ง€์›Œ๋„ ๋ ๊นŒ?'๋ผ๋Š” ๊ฑฑ์ • ์—†์ด ํ•ด๋‹น ์ฝ”๋“œ๋ฅผ ๋ณธ ๋ˆ„๊ตฌ๋‚˜ ์•ˆ์‹ฌํ•˜๊ณ  ์ œ๊ฑฐํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฒƒ์„ ์˜๋ฏธํ•˜๋ฉฐ ์ด๋ฅผ ํ†ตํ•ด ๋ ˆ๊ฑฐ์‹œ๊ฐ€ ๋œ ์ฝ”๋“œ๋ฅผ ๊ณง๋ฐ”๋กœ ์ œ๊ฑฐํ•˜๋„๋ก ์œ ๋„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

 

3. ๋ช…์„ธ๋งŒ ๋จผ์ € ์ „๋‹ฌํ•˜๊ธฐ

API ๋กœ์ง์ด ์ „๋ถ€ ์™„์„ฑ๋œ ํ›„ ๋ช…์„ธ๋ฅผ ์ „๋‹ฌํ•˜๋ฉด ํ”„๋ก ํŠธ ๊ฐœ๋ฐœ์ž๋“ค์ด ๊ธฐ๋‹ค๋ฆฌ๋Š” ์‹œ๊ฐ„์ด ๊ธธ์–ด์ ธ ๋ฆฌ์†Œ์Šค๋ฅผ ๋‚ญ๋น„ํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ์ด๋Ÿด ๋•Œ๋Š” ์•„๋ž˜์™€ ๊ฐ™์ด ๋นˆ ๊ฐ์ฒด๋งŒ ๋จผ์ € ์ „๋‹ฌํ•˜์—ฌ ์–ด๋–ค Response๋ฅผ ๋„˜๊ฒจ์ค„์ง€ ๋ช…์„ธ๋งŒ ๋จผ์ € ์ œ๊ณตํ•˜๊ณ , ์ดํ›„์— ๋กœ์ง์„ ๊ฐœ๋ฐœํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ์ข€ ๋” ์›ํ™œํ•˜๊ฒŒ ํ˜‘์—…์„ ์ง„ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

@GetMapping("/test")
public ResponseDto test() {
    // TODO ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ์ž‘์„ฑ
    return new ResponseDto();
}

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ํ”„๋ก ํŠธ ๊ฐœ๋ฐœ์ž๋Š” ๋ช…์„ธ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์ž‘์—…์„ ์‹œ์ž‘ํ•  ์ˆ˜ ์žˆ๊ณ , ๋ฐฑ์—”๋“œ ๊ฐœ๋ฐœ์ž๋Š” ๋กœ์ง์„ ์ฐจํ›„์— ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์–ด ๋˜ ๋‹ค๋ฅธ ๋ช…์„ธ๋ฅผ ์ž‘์—…ํ•˜๊ฑฐ๋‚˜ ๋‹ค๋ฅธ ์ž‘์—…์„ ๋จผ์ € ์ง„ํ–‰ํ•˜๋Š” ๋“ฑ ์–‘์ชฝ ๋ชจ๋‘ ์ž‘์—…์„ ๋ณ‘ํ–‰ํ•˜์—ฌ ํšจ์œจ์ ์œผ๋กœ ์ง„ํ–‰ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

 

 

4. Authorize์— jwt ๋„ฃ์„ ๋•Œ prefix์— Bearer ์ƒ๋žต์‹œํ‚ค๊ธฐ

Swagger UI์—์„œ Authorization์— JWT ํ† ํฐ์„ ๋„ฃ์„ ๋•Œ๋Š” 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9~'์™€ ๊ฐ™์ด 'Bearer '๋ฅผ prefix์— ์ˆ˜๋™์œผ๋กœ ๋ช…์‹œํ•ด์•ผ๋งŒ ํ•ฉ๋‹ˆ๋‹ค. ์ด๋Ÿฐ ๋ฒˆ๊ฑฐ๋กœ์šด ๊ณผ์ •์„ Swagger ์„ค์ •์„ ํ†ตํ•ด ์ƒ๋žตํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    @Bean
    public OpenAPI openAPI() {
        return new OpenAPI()
            .components(new Components()
                            .addSecuritySchemes("Authorization",
                                                new SecurityScheme().type(SecurityScheme.Type.HTTP)
                                                                    .scheme("bearer")
                                                                    .bearerFormat("JWT")
                                                                    .in(SecurityScheme.In.HEADER)
                                                                    .description("JWT ํ† ํฐ ์ •๋ณด")
                            )
            );
    }

์œ„์™€ ๊ฐ™์ด SecurityScheme ์„ค์ •์„ ํ†ตํ•ด ์‚ฌ์šฉ์ž๊ฐ€ Swagger ์ƒ์—์„œ 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9~' ํ˜•์‹์œผ๋กœ๋งŒ ์ž…๋ ฅํ•ด๋„ ์ž๋™์œผ๋กœ 'Bearer '๊ฐ€ prefix์— ์ถ”๊ฐ€๋˜๋„๋ก ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์„ค์ • ํ›„ ์‹ค์ œ๋กœ Swagger์—์„œ API๋ฅผ ์š”์ฒญํ•œ ๋’ค curl ๋ช…๋ น์–ด๋ฅผ ํ™•์ธํ•ด ๋ณด๋ฉด -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9~"์™€ ๊ฐ™์ด Bearer๊ฐ€ ์ž๋™์œผ๋กœ ๋ถ™๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

 

5. ๋ธŒ๋ผ์šฐ์ € ์ƒˆ๋กœ๊ณ ์นจ ํ›„์—๋„ ์ธ์ฆ์ •๋ณด ์œ ์ง€์‹œํ‚ค๊ธฐ

Swagger๋Š” ์ƒˆ๋กœ๊ณ ์นจํ•˜๊ฑฐ๋‚˜ ๋ธŒ๋ผ์šฐ์ €๋ฅผ ์ข…๋ฃŒํ•œ ํ›„ ๋‹ค์‹œ ์—ด๋ฉด ์ธ์ฆ ์ •๋ณด๊ฐ€ ์ดˆ๊ธฐํ™”๋˜์–ด ๋งค๋ฒˆ ํ† ํฐ์„ ๋‹ค์‹œ ์ž…๋ ฅํ•ด์•ผ ํ•˜๋Š” ๋ฒˆ๊ฑฐ๋กœ์›€์ด ์žˆ์Šต๋‹ˆ๋‹ค.

์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด 2020๋…„ 3์›” 29์ผ PR #5939์ด ์˜ฌ๋ผ์™”๋Š”๋ฐ, ์ด PR ๋•๋ถ„์— ๊ฐ„๋‹จํ•œ ์„ค์ • ์ถ”๊ฐ€๋งŒ์œผ๋กœ ์ธ์ฆ ์ •๋ณด๋ฅผ ์œ ์ง€์‹œํ‚ฌ ์ˆ˜ ์žˆ๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์„ค์ • ๋ฐฉ๋ฒ•์€ ๊ฐ„๋‹จํ•œ๋ฐ yml์— persist-authorization์„ true๋กœ ์„ค์ •ํ•˜๋ฉด ๋ธŒ๋ผ์šฐ์ €๋ฅผ ์ƒˆ๋กœ๊ณ ์นจํ•˜๊ฑฐ๋‚˜ ์žฌ์‹œ์ž‘ํ•ด๋„ ์ธ์ฆ ์ •๋ณด๊ฐ€ ์œ ์ง€๋ฉ๋‹ˆ๋‹ค.

#springdoc swagger
springdoc:
  swagger-ui:
      persist-authorization: true # ๋ธŒ๋ผ์šฐ์ €๋ฅผ ์ƒˆ๋กœ๊ณ ์นจ ํ•˜๋”๋ผ๋„ ์ธ์ฆ์ •๋ณด ์œ ์ง€

์ƒˆ๋กœ๊ณ ์นจ ํ›„์—๋„ ์ธ์ฆ์ •๋ณด๊ฐ€ ๋‚จ์•„์žˆ๋Š” ๋ชจ์Šต

๋” ์ž์„ธํ•œ ๊ด€๋ จ ์ •๋ณด๋Š” configuration.md์—์„œ ํ™•์ธ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

 

 

6. ๊ธฐํƒ€ ์ž์ž˜ํ•œ ์ƒ์„ธ ์„ค์ •๋“ค

์ฝ”๋“œ์— Swagger ์–ด๋…ธํ…Œ์ด์…˜์„ ๋ถ™์—ฌ API์— ๋Œ€ํ•ด ๋” ์ƒ์„ธํ•œ ์ •๋ณด๋ฅผ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์ด ๋ถ€๋ถ„์€ ์šด์˜ ์ฝ”๋“œ์— Swagger ์–ด๋…ธํ…Œ์ด์…˜์ด ๋งŽ์ด ์นจํˆฌ๋œ๋‹ค๋Š” ์  ๋•Œ๋ฌธ์— Swagger์˜ ๋‹จ์ ์œผ๋กœ ๋ถˆ๋ฆฌ๊ธฐ๋„ ํ•ฉ๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ํ•ญ์ƒ ๋ชจ๋“  ์–ด๋…ธํ…Œ์ด์…˜์„ ์ ์šฉํ•˜๊ธฐ๋ณด๋‹ค๋Š” ๊ฐ API ๋ณ„๋กœ ํ•„์š”ํ•œ ๋ถ€๋ถ„๋งŒ ์„ ํƒ์ ์œผ๋กœ ์ ์šฉํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.(์ €๋Š” ๋ณดํ†ต @Tag, @Schema, @Operation๊นŒ์ง€๋งŒ ์ ์šฉํ•˜๊ณ  ํ•„์š”์— ๋”ฐ๋ผ @ApiResponse๊นŒ์ง€ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.)

  • @Tag: API ๊ทธ๋ฃน ์ •์˜
  • @Schema: ๋ฐ์ดํ„ฐ ๋ชจ๋ธ์˜ ์Šคํ‚ค๋งˆ ์ •์˜
  • @Operation: ํŠน์ • ์—”๋“œํฌ์ธํŠธ์˜ ์ž‘์—… ์„ค๋ช…
  • @ApiResponse: ํŠน์ • ์—”๋“œํฌ์ธํŠธ์˜ ์‘๋‹ต ์„ค๋ช…
  • @Parameter ๋ฉ”์„œ๋“œ ๋งค๊ฐœ๋ณ€์ˆ˜ ์„ค๋ช…

์ถ”๊ฐ€๋กœ ํ”„๋กœํผํ‹ฐ ์„ค์ •์„ ํ†ตํ•ด Swagger ๋‚ด API๋“ค์˜ ์ •๋ ฌ ์ˆœ์„œ, ๊ธฐ๋ณธ์œผ๋กœ ์ „๋ถ€ ํŽผ์น˜๊ธฐ/์ ‘๊ธฐ ๋“ฑ์˜ ์„ค์ •์„ ํ†ตํ•ด ๊ฐ€๋…์„ฑ๋„ ํ–ฅ์ƒ์‹œํ‚ฌ ์ˆ˜ ์žˆ์œผ๋‹ˆ ์ด ๋ถ€๋ถ„๋„ ์ƒํ™ฉ์— ๋งž๊ฒŒ ์ ์šฉํ•˜๋ฉด ๋„์›€์ด ๋ฉ๋‹ˆ๋‹ค.