2023 글로벌미디어학부 졸업 작품 프로젝트 Dandi 에서 api 문서를 위해 Swagger를 사용하기로 결정했습니다.
보통 Swagger와 많이 비교되는 것이 Restdocs입니다. 과거 우테코 프로젝트에서는 Restdocs를 사용했습니다.
장점
단점
장점
단점
이번 프로젝트는 아래와 같은 이유로 Swagger를 채택했습니다.
Springdoc-openapi과 Springfox는 Swagger를 통한 API 문서 구성을 도와주는 library입니다.
따라서, 둘 중에 하나를 선택해야 했기 때문에 둘의 차이를 알아보았습니다.
https://springdoc.org/#migrating-from-springfox
Springdoc의 공식문서 Differentiation to Springfox project 에 아래와 같은 내용이 있습니다.
springfox
to support OAS 3. springfox
covers for the moment only swagger 2 integration with Spring Boot. The latest release date is June 2018. So, in terms of maintenance there is a big lack of support lately.springfox
, is that we integrate new features not covered by springfox
:swagger-annotations
and swagger-ui
only official libraries.spring-webflux
with annotated and functional style.Springdoc 문서에 아래와 같이 Springfox와의 차이점을 설명합니다.
작성일 기준 8일 전을 기준으로, Springfox보다 Springdoc을 추천하는 근거들이 있습니다.
Springdoc이 Springfox보다 좋은 점은 꾸준히 관리되고 업데이트 된다는 점
입니다. 공식 문서 FAQ도 상당히 잘 되었습니다. SpringBoot 3가 출시되었습니다. Springfox는 SpringBoot 3에 대한 support를 하지 않고 할지도 명확하지 않다고 합니다. 물론 저는 2.7.7 버전을 사용하고 있지만, 추후에 3 점대로 올릴 수도 있다는 가능성도 열어두었습니다.
따라서, Springdoc-openapi
을 선택했습니다.
implementation group: 'org.springdoc', name: 'springdoc-openapi-ui', version: '1.6.14'
For spring-boot v3 support, make sure you use springdoc-openapi v2
저는 SpringBoot 2.7.7을 사용하기 때문에 v1 중에 공식 문서에 나와있는 1.6.14를 사용했습니다.
공식 문서에 친절하게 아래의 endpoints를 제시해줍니다.
http://serverName:managementPort/actuator/openapi
http://serverName:managementPort/actuator/swagger-ui
springdoc:
version: v0.0.1
packagesToScan: dandi.dandi
pathsToMatch: /api
swagger-ui:
path: /api-docs
api-docs:
enabled: true
groups:
enabled: false
version은 제가 @Value로 받아쓰기 위한 값입니다.
springdoc.swagger-ui.path 경로로 접근하면 /swagger-ui/index.html
로 Redirect 됩니다.
springdoc.api-docs.groups.enabled는 Default가 true인데, 일단 false로 지정해주었습니다. 추후에 api들이 많아지면 grouping 하는 설정을 진행할 때 true로 활성화 할 예정입니다. 바로 아래 링크를 참고하시면 groups에 대해 이해하실 수 있을 겁니다.
https://happy-jjang-a.tistory.com/165
다른 설정들은 부가적인 설명 없이 이해할 수 있을 겁니다. 궁금한 점이 있으면 아래 링크의 springdoc-openapi core properties
와 swagger-ui properties
를 참조하시면 좋을 것 같습니다.
https://springdoc.org/#springdoc-openapi-core-properties
springdoc.cache.disabled (Default는 false)
To disable the springdoc-openapi cache of the calculated OpenAPI.
springdoc.show-actuator (Default는 false)
To display the actuator endpoints.
springdoc.api-docs.enabled=false
springdoc.api-docs를 false로 설정하고 swagger-ui에 접근했을 때, 404가 응답되는 것을 겪었는데요.
공식 문서를 보니 아래와 같은 설명이 있었습니다.
With this property, all the
springdoc-openapi
auto-configuration beans are disabled:
아래와 같이 /v3/api-docs로 접근할 수 있는 값에 대한 허용 여부인지 알고 false로 두었는데요. false로 두면 springdoc-openapi bean들이 활성화되지 않습니다. 문서를 잘 읽는 필요성을 또 한번 느꼈습니다.
{
"openapi":"3.0.1",
"info": {
"title":"Dandi API",
"description":"Global Media 2023 Graduation Exhibition Team Dandi",
"termsOfService":"Team Dandi","version":"v0.0.1"
},
"servers":[{
"url":"http://localhost:8080",
"description":"Generated server url"
}],
"paths":{},
"components":{}
}
@Configuration
public class OpenApiConfig {
@Bean
public OpenAPI openAPI(@Value("${springdoc.open-api-version}") String openApiVersion) {
Info info = new Info()
.title("Dandi API")
.version(openApiVersion)
.description("Global Media 2023 Graduation Exhibition Team Dandi")
.termsOfService("Team Dandi");
return new OpenAPI()
.components(new Components())
.info(info);
}
}
대부분의 값들을 yml에서 설정해서 Bean에 주입하기 때문에 크게 설정할 값이 많지 않았습니다.
Swagger 2에서 Swagger 3로 업데이트 되면서 아래와 같은 변경이 있습니다.
@Api
→ @Tag
@ApiIgnore
→ @Parameter(hidden = true)
or @Operation(hidden = true)
or @Hidden
@ApiImplicitParam
→ @Parameter
@ApiImplicitParams
→ @Parameters
@ApiModel
→ @Schema
@ApiModelProperty(hidden = true)
→ @Schema(accessMode = READ_ONLY)
@ApiModelProperty
→ @Schema
@ApiOperation(value = "foo", notes = "bar")
→ @Operation(summary = "foo", description = "bar")
@ApiParam
→ @Parameter
@ApiResponse(code = 404, message = "foo")
→ @ApiResponse(responseCode = "404", description = "foo")
@Tag(name = "인증")
@RestController
public class AuthController {
private final AuthService authService;
public AuthController(AuthService authService) {
this.authService = authService;
}
@Operation(summary = "Apple ID로 로그인/회원가입")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "로그인", headers = @Header(name = AUTHORIZATION, description = "Access Token")),
@ApiResponse(responseCode = "201", description = "회원가입 후 로그인", headers = @Header(name = AUTHORIZATION, description = "Access Token")),
@ApiResponse(responseCode = "401", description = "유효하지 않은 Apple Id Token")
})
@PostMapping("/login/oauth/apple")
public ResponseEntity<Void> login(@Parameter(description = "Apple IdToken") @RequestBody LoginRequest loginRequest) {
LoginResponse loginResponse = authService.getAccessToken(loginRequest);
if (loginResponse.isNewUser()) {
return ResponseEntity.status(HttpStatus.CREATED)
.header(HttpHeaders.AUTHORIZATION, loginResponse.getToken())
.build();
}
return ResponseEntity.ok()
.header(HttpHeaders.AUTHORIZATION, loginResponse.getToken())
.build();
}
}
@Tag, @Operation, @ApiResponses, @ApiResponse, @Parameter 만 보시면 됩니다!
어노테이션들에 대한 부가 설명은 하지 않겠습니다.
public class LoginRequest {
@Schema(example = "1g2u5h1jh12j12jk23h")
private String idToken;
public LoginRequest() {
}
public String getIdToken() {
return idToken;
}
}
위에서 설정한 방식에서는 프로덕션 코드와 문서를 위한 코드, 두 개의 관심사가 한 곳에 함께 있다는 문제
가 있습니다. Controller에 메서드가 많아지면 가독성이 더 떨어질 것으로 예상합니다.(물론 Controller를 분리하는 방법도 있겠습니다만..)
따라서, 아래와 같이 추상화 기술을 이용해서 문서를 위한 코드와 프로덕션 코드를 분리
했습니다.
다만, Controller에만 적용했습니다. DTO는 api가 많아짐에 따라 Request, Response로 2배씩 늘어날텐데, Controller와 같이 분리를 한다면 클래스들이 너무 많아질 것이라고 생각했습니다. 또한, DTO는 필드 생성자 getter만 존재하기 때문에 가독성을 크게 해치지 않을 것이라고 예상했습니다.
@Tag(name = "인증")
public interface AuthControllerDocs {
@Operation(summary = "Apple ID로 로그인/회원가입")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "로그인", headers = @Header(name = AUTHORIZATION, description = "Access Token")),
@ApiResponse(responseCode = "201", description = "회원가입 후 로그인", headers = @Header(name = AUTHORIZATION, description = "Access Token")),
@ApiResponse(responseCode = "401", description = "유효하지 않은 Apple Id Token")
})
ResponseEntity<Void> login(@Parameter(description = "사용자 id") LoginRequest loginRequest);
}
@RestController
public class AuthController implements AuthControllerDocs {
private final AuthService authService;
public AuthController(AuthService authService) {
this.authService = authService;
}
@PostMapping("/login/oauth/apple")
public ResponseEntity<Void> login(@RequestBody LoginRequest loginRequest) {
LoginResponse loginResponse = authService.getAccessToken(loginRequest);
if (loginResponse.isNewUser()) {
return ResponseEntity.status(HttpStatus.CREATED)
.header(HttpHeaders.AUTHORIZATION, loginResponse.getToken())
.build();
}
return ResponseEntity.ok()
.header(HttpHeaders.AUTHORIZATION, loginResponse.getToken())
.build();
}
}
개발을 하다보면 api가 많아질텐데요.
다음엔 api가 많아짐에 따라, group을 적용해야한다면 적용하는 포스팅으로 돌아오겠습니다~