[생각정리] Swagger (Feat. SpringDoc)

jeyong·2024년 1월 24일
0

공부 / 생각 정리  

목록 보기
9/121


스프링에 대해서 여러가지 공부를 하던중, 정리 해놓으면 좋을 것 같은 주제를 정리하려고 한다.
이번에 다를 주제는 Swagger이다. 해당 주제를 공부하게 된 이유는 굉장히 간단하다. 연구실 과제를 진행중에 외주를 맡긴 스프링 서버의 API 명세서를 보게되었다. 해당 명세서는 Swagger를 이용해서 작성되어있었다. 굉장히 편리해보였고 한번 사용해보기로 하였다.
그래서 공부를 한뒤 프로젝트에 적용하였다. 그리고 해당 내용을 기록하려고한다.
게시글 섹션이 왜 공부정리가 아닌 생각정리냐면 공부 정리라고 하기에는 굉장히 간단한 내용을 다루었고 사실 공부를 정리하기 위한 목적이 아닌 Swagger 사용에 대한 개발자들의 의견이 다르다는 것을 알게되었고 그것에 대한 나의 생각을 정리하고자 기록하는 것이기 때문이다.

1. Swagger란?

1-1. Swagger란?

사실 Swagger에 대해서 설명할 것도 없다. Swagger에 대해서 간단히 설명하자면, 아래와 같다.

  • Swagger를 사용하면 API 문서를 생성할 때, 개발자가 문서를 작성하지 않아도 되므로 개발 시간을 단축할 수 있다.
  • Swagger UI를 이용하면 API를 쉽게 테스트할 수 있으며, API 호출 시 전달해야 할 파라미터를 확인할 수 있다.
  • Swagger를 사용하면 API 버전 관리가 용이해지고 다양한 API 문서를 통합할 수 있다.

1-2. 사용 방식

@RestController
@RequiredArgsConstructor
public class MemberController {

    private final MemberService memberService;

    @Operation(summary = "사용자 정보 조회", description = "사용자 정보를 조회한다.")
    @GetMapping("/api/members/{id}")
    public ResponseEntity<Response> read(@Parameter(description = "사용자 id") @PathVariable(name = "id")Long id) {
        return ResponseEntity
                .status(HttpStatus.OK)
                .body(Response.success(memberService.read(id)));
    }

    @Operation(summary = "사용자 정보 삭제", description = "사용자 정보를 삭제한다.")
    @DeleteMapping("/api/members/{id}")
    public ResponseEntity<Response> delete(@Parameter(description = "사용자 id") @PathVariable(name = "id")Long id) {
        memberService.delete(id);
        return ResponseEntity
                .status(HttpStatus.OK)
                .body(Response.success());
    }
}

controller 코드에 몇 가지 어노테이션을 추가해주면, Swagger가 어노테이션을 이용해 API 정보를 읽는다.

1-3. SpringDoc vs SpringFox

Swagger은 Java, Spring 전용 프레임워크가 아니다. 정확히는 OAS (OpenAPI Specification)를 위한 프레임워크라, JS, Python 등등 언어별로 전부 사용할 수 있다.
따라서 Spring 환경에서 Swagger를 사용하려면 Swagger UI의 설정, Swagger 어노테이션으로 API 메타데이터를 읽는 과정 등을 직접 구현해줘야 하는데, 이런 작업을 대신해주는 라이브러리, SpringDoc 또는 SpringFox를 사용한다.

SpringDoc 과 SpringfFx 중에 무엇을 사용해야하냐고 묻는다면 정말 간단하다. SpringDoc이다. 왜냐하면 springfox은 Spring Boot 3부터 지원을 하지 않기 때문이다!

2. 환경 설정

2-1. build.gradle

dependencies {
	// Spring Boot Starter Dependencies
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa' // JPA
	implementation 'org.springframework.boot:spring-boot-starter-security' // Security
	implementation 'org.springframework.boot:spring-boot-starter-web' // Web

	// Lombok - Simplify Java code
	compileOnly 'org.projectlombok:lombok'
	annotationProcessor 'org.projectlombok:lombok'

	// Database
	runtimeOnly 'com.h2database:h2' // In-memory database

	// JWT - JSON Web Token
	implementation 'io.jsonwebtoken:jjwt:0.9.1'

	// XML and Validation
	implementation 'javax.xml.bind:jaxb-api:2.3.1' // XML Binding
	implementation 'org.springframework.boot:spring-boot-starter-validation' // Validation

	// Test Dependencies
	testImplementation 'org.springframework.boot:spring-boot-starter-test' // Spring Boot Test
	testImplementation 'org.springframework.security:spring-security-test' // Spring Security Test
	testImplementation 'org.projectlombok:lombok:1.18.28' // Lombok for testing

	// SpringDoc - OpenAPI 3
	implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0' // SpringDoc for OpenAPI UI

	// QueryDSL
	implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta' // QueryDSL for JPA
	annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta" // QueryDSL Annotation Processor
	annotationProcessor "jakarta.annotation:jakarta.annotation-api" // Jakarta Annotations
	annotationProcessor "jakarta.persistence:jakarta.persistence-api" // Jakarta Persistence API
}

SpringDoc - OpenAPI 3 주석 처리되어있는 설정을 참고하여 의존성을 설치해준다.

2-2. application.yml

spring:
  springdoc:
    api-docs:
      enabled: true
    swagger-ui:
      path: /swagger-ui/index.html
      groups-order: DESC
      doc-expansion: none
      tags-sorter: alpha
      operationsSorter: method
      display-request-duration: true
  • path: swagger-ui를 접속할 path 설정
  • groupd-order: group 정렬 방법 설정
    • ASC : 오름차순
    • DESC : 내림차순
  • doc-expansion: swagger 각 태그별 api 리스트 펼치기/접기
    • list(default) : tag만 펼치기
    • full : tag와 operation 모두 펼치기
    • none : 모두 접기
  • tags-sorter: tag 정렬 방법
    • alpha : 알파벳순
  • operations-sorter: api 정렬 방법
    • alpha : 알파벳순
  • display-request-duration : "Try it out"을 실행 후 api 실행 시간 표시(milliseconds)
    • false(default) : 실행시간 미표시
    • true : 실행시간 표시

2-3. SwaggerConfig

@Configuration
public class SwaggerConfig {
    @Bean
    public GroupedOpenApi SignApi() {
        return GroupedOpenApi.builder()
                .group("sign")
                .pathsToMatch("/api/sign-up","/api/sign-in","/api/refresh-token")
                .build();
    }

    @Bean
    public GroupedOpenApi MemberApi() {
        return GroupedOpenApi.builder()
                .group("member")
                .pathsToMatch("/api/members/**")
                .build();
    }

    @Bean
    public GroupedOpenApi CategoryApi() {
        return GroupedOpenApi.builder()
                .group("category")
                .pathsToMatch("/api/categories/**")
                .build();
    }

    @Bean
    public GroupedOpenApi PostApi() {
        return GroupedOpenApi.builder()
                .group("post")
                .pathsToMatch("/api/posts/**")
                .build();
    }

    @Bean
    public GroupedOpenApi CommentApi() {
        return GroupedOpenApi.builder()
                .group("comment")
                .pathsToMatch("/api/comments/**")
                .build();
    }

    @Bean
    public GroupedOpenApi MessageApi() {
        return GroupedOpenApi.builder()
                .group("message")
                .pathsToMatch("/api/messages/**")
                .build();
    }

    @Bean
    public OpenAPI customOpenAPI() {
        return new OpenAPI()
                .info(new Info()
                        .title("O2O Automatic Store System Demo")
                        .version("1.0")
                        .description("O2O Automatic Store System Demo REST API Documentation"))
                .addSecurityItem(new SecurityRequirement().addList("bearerAuth"))
                .components(new io.swagger.v3.oas.models.Components()
                        .addSecuritySchemes("bearerAuth", new SecurityScheme()
                                .name("bearerAuth")
                                .type(SecurityScheme.Type.HTTP)
                                .scheme("bearer")
                                .bearerFormat("JWT")
                        ));
    }
}

2-4. SecurityConfig

@Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        return web -> web.ignoring().requestMatchers("/swagger-ui/**","/v3/api-docs/**");
    }

3. 프로젝트에 적용

3-1. Controller

@RestController
@RequiredArgsConstructor
public class MemberController {

    private final MemberService memberService;

    @Operation(summary = "사용자 정보 조회", description = "사용자 정보를 조회한다.")
    @GetMapping("/api/members/{id}")
    public ResponseEntity<Response> read(@Parameter(description = "사용자 id") @PathVariable(name = "id")Long id) {
        return ResponseEntity
                .status(HttpStatus.OK)
                .body(Response.success(memberService.read(id)));
    }

    @Operation(summary = "사용자 정보 삭제", description = "사용자 정보를 삭제한다.")
    @DeleteMapping("/api/members/{id}")
    public ResponseEntity<Response> delete(@Parameter(description = "사용자 id") @PathVariable(name = "id")Long id) {
        memberService.delete(id);
        return ResponseEntity
                .status(HttpStatus.OK)
                .body(Response.success());
    }
}
  • @Operation를 이용해 해당 API가 어떤 리소스를 나타내는지 간략한 설명을 추가할 수 있다.
  • @Parameter 어노테이션을 이용해 각 필드에 대한 설명을 추가할 수 있다.

Request Body

@Schema(description = "게시글 생성 요청")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PostCreateRequest {

    @Schema(description = "게시글 제목", example = "my title", required = true)
    @NotBlank(message = "{postCreateRequest.title.notBlank}")
    private String title;

    @Schema(description = "게시글 본문", example = "my content", required = true)
    @NotBlank(message = "{postCreateRequest.content.notBlank}")
    private String content;

    @Schema(description = "가격", example = "50000", required = true)
    @NotNull(message = "{postCreateRequest.price.notNull")
    @PositiveOrZero(message = "{postCreateRequest.price.positiveOrZero}")
    private Long price;

    @Schema(hidden = true)
    @Null
    private Long memberId;

    @Schema(description = "카테고리 아이디", example = "3", required = true)
    @NotNull(message = "{postCreateRequest.categoryId.notNull}")
    @PositiveOrZero(message = "{postCreateRequest.categoryId.positiveOrZero}")
    private Long categoryId;

    @Schema(description = "이미지", type = "array", format = "binary")
    private List<MultipartFile> images = new ArrayList<>();
}
  • class 상단에 @Schema 어노테이션을 이용해, 해당 클래스가 어떤 클래스인지 설명을 적어줄 수 있다.
  • @Schema 어노테이션을 이용해 각 필드에 대한 설명, 예시, 필수값 여부를 추가해줄 수 있다.

4. 마무리

Swagger를 프로젝트에 직접 적용시켜보며, API 명세서 작성에 대한 고민을 내려 놓을 수 있게 되었고 정말 도움이 될 것 같다.
프론트엔드, 백엔드 개발자 간 소통 돕는 Swagger의 댓글을 보면 알겠지만, Swagger에 대한 개발자들의 의견이 다르다. 직접 사용해보면서 느낀 나의 생각을 감히 말하자면 나도 댓글을 작성한 개발자분들과 같이, 과한 어노테이션의 사용은 소스코드 작성에 있어서 방해를 한다고 생각한다. 하지만 정말 필요한 어노테이션만을 이용해서 작성한다면, 생각보다 소스코드의 가독성을 해치지 않는다. 그래서 나는 @Tag 과 @ApiResponses 등 적용하지 않은 어노테이션도 많다.
결론적으로 나는 Swagger에 적용에 있어서도 그렇고 여러가지 어노테이션을 사용하기 전에 있어서도 그렇고 프로젝트를 같이 진행하는 팀원들과 충분한 상의를 해보고 적용하는 것이 좋다고 생각한다. 항상 생각정리의 마무리는 중립의 의견을 내고 끝내는 것 같다.

profile
노를 젓다 보면 언젠가는 물이 들어오겠지.

0개의 댓글