본문 바로가기

개발/Java

Swagger 적용해보기

배경


지난번에 살짝 swagger에 대해 언급하긴 했었는데 제가 했던 프로젝트에 직접 적용해 보았습니다. 시간이 좀 걸리더라도, 다른 기능들을 좀 더 개발을 하는 것보다도 이 작업을 택했던 이유는 앞으로 저는 협업이 훨씬 많아질 것이기 때문이죠. 이전에 회사를 다닐 때에도 나 혼자 일을 잘하는 것도 좋지만, 서로 협업 할 때는 내가 나아가는 방향과 진척도를 상대에게 얼마나 잘 전달하느냐에 따라서 능률이 정말 많이 차이난다는 것을 느꼈습니다.

내용


swagger 선택 이유는 다음과 같습니다.

1. 프로젝트를 모르는 개발자가 쉽게 이해할 수 있도록 한다.

2. UI를 통해 이해가 쉽다.

3. 직접 API를 날려볼 수 있다.

장점만 있는건 아닙니다. 단점으로는

- 소스 코드가 매우 지저분해집니다. custom을 통해 특정 controller나 dto에 명시를 하게 되는데 이때 애노테이션을 통해 작성을 길게 해야 하는 경우가 있으므로 코드 보기가 난잡해집니다..

이런 이유들이 있지만, 그래도 단점보다는 앞으로 협업을 생각해서 swagger를 통해 API 문서를 작성해 보겠습니다.

1. gradle.build 설정

Swagger를 spring boot에 시작하기 전에 준비해야 될 것이 있습니다. spring boot버전에 맞는 gradle을 build해야겠죠?

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.2.11'
    id 'io.spring.dependency-management' version '1.1.6'
}

아래는 dependencies에 추가될 내용입니다.

    // swagger 세팅
    implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0")

최신버전에는 import 된 것들이 jakarta로만 사용 가능하니 참고해야 될 것 같습니다.

spring 버전과 swagger 버전을 꼭 확인하셔서 항상 build를 진행하는 습관이 필요합니다. 가령 spring boot 버전은 모르는데 특정 dependenies만 같다고해서 똑같이 적용이 되지는 않습니다.

2. config 설정

다음으로 Config 설정입니다. 저는 프로젝트에 Security를 활용한 jwt를 적용했기 때문에 jwt에 대한 설정도 필요합니다.

@Configuration
public class OpenApiConfig {

    private static final String API_NAME = "Harmony";

    private static final String API_VERSION = "1.0.0";

    private static final String API_DESCRIPTION = "Harmony 프로젝트 API";

    @Bean
    public OpenAPI OpenAPIConfig() {

        // jwt 사용 추가
        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()
                .components(new Components())
                .info(new Info()
                        .title(API_NAME)
                        .description(API_DESCRIPTION)
                        .version(API_VERSION))
                .addSecurityItem(securityRequirement)
                .components(components);
    }
}

또한 Security에서는 권한이 없으면 API 주소를 막아버리기 때문에 이에 대한 Security config 설정도 작성해 주어야 합니다.

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    //... 의존성 주입들..

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        // CSRF 설정
        http.csrf((csrf) -> csrf.disable());

        // 기본 설정인 Session 방식은 사용하지 않고 JWT 방식을 사용하기 위한 설정
        http.sessionManagement((sessionManagement) ->
                sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        );

        http.authorizeHttpRequests((authorizeHttpRequests) ->
                authorizeHttpRequests
                        .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll() // resources 접근 허용 설정
                        .requestMatchers("/").permitAll() 
                        .requestMatchers("/swagger-ui", "/swagger-ui/**", "/swagger-resources/**", "/v3/api-docs/**", "/webjars/**", "/swagger-ui.html").permitAll()    // 이 부분 추가 필요
                        .anyRequest().authenticated();

        // 필터 관리
        http.addFilterBefore(jwtAuthorizationFilter(), JwtAuthenticationFilter.class);
        http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);


        return http.build();
    }
}

이렇게 되면 일단 기본적으로 App을 구동시킨 뒤에 접속을 해보시면 swagger에서 자동으로 API들을 작성해 둔 것들을 보여줍니다. (ex. localhost:8080/swagger-ui/index.html#/)

당연히 request와 response 내용이 맞지 않는 것도 있기 때문에 하나하나 직접 애노테이션을 달며 작성해 주는 것이 필요합니다. 귀찮고 눈이 아프지만 swagger ui를 통해 처음 App을 이해하는 사람들을 생각해보며 작업을 해줍시다.

###3. 추가 변동 사항
만약에 custom filter를 적용하고 있다면 어떻게 될까요? custom filter가 security보다 먼저 적용되면 이후 doFilter로 인해 security로 자동으로 흘러들어가 권한 문제가 다시 발생할 수 있습니다.(위에서 설정을 해놔도)

그래서 따로 사용한 Filter (예를들어 OncePerRequestFilter의 경우 shouldNotFilter를 활용하여 해당 Filter를 거치지 않게 해야 swagger가 정상적으로 동작되었습니다.)

@Order(Ordered.HIGHEST_PRECEDENCE)
public class ReqResLoggingFilter extends OncePerRequestFilter {

// filter 내용....

@Override
    protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
        String[] excludePath = {"/swagger-ui/",
                "/swagger-ui/**",
                "/v3/api-docs/**",
                "/swagger-resources/**",
                "/webjars/**"};
        String path = request.getRequestURI();
//        boolean shouldNotFilter = Arrays.stream(excludePath).anyMatch(path::startsWith);
//        logger.info("shouldNotFilter: {} for path: {}", shouldNotFilter, path);
        return Arrays.stream(excludePath).anyMatch(path::startsWith);
    }
}

###4. 사용할 곳에 custom으로 적용하기
기본적으로 swagger가 모든 controller를 돌면서 api에 대해 자동으로 작성해 주지만 모든 결과가 다 만족스럽지는 않습니다. api들을 보면서 좀더 알아보기 쉽게 수정해줘야 하는 부분이 있죠.. 예를들어

@RestController
@RequiredArgsConstructor
@RequestMapping("/api")
public class OrderController {

    private final OrderService orderService;
    private final SuccessResponseHandler successResponseHandler;

    // user 이상 사용 가능.
    @Operation(summary = "주문 생성", description = "주문을 생성할 때 사용하는 API")
    @ApiResponses(value = {
            @ApiResponse(responseCode = "201", description = "주문 요청이 성공적으로 접수된 경우"),
            @ApiResponse(responseCode = "400", description = "주문 요청에 실패했을 경우", content = @Content(schema = @Schema(implementation = RestApiException.class), mediaType = "application/json")),
    })
    @Parameter(name = "OrderRequestDto", description = "주문 입력 사항", schema = @Schema(implementation = OrderRequestDto.class))
    @PreAuthorize("hasAuthority('ROLE_USER') or hasAuthority('ROLE_OWNER')  or hasAuthority('ROLE_MANAGER') or hasAuthority('ROLE_MASTER')")
    @PostMapping("/orders")
    public ResponseEntity<ApiResponseDto<OrderResponseDto>> createOrder(@RequestBody @Valid OrderRequestDto orderRequestDto,
                                                                       @AuthenticationPrincipal UserDetailsImpl userDetails) {
        OrderResponseDto orderResponseDto = orderService.createOrder(orderRequestDto, userDetails.getUser());

        return successResponseHandler.handleSuccess(HttpStatus.CREATED, "주문이 성공적으로 접수되었습니다.", orderResponseDto);
    }
}
  • 특정 Api에 대해 @Operation, @ApiResponse, @Parameter 등과 같은 것을 사용해 좀더 구체적으로 작성할 수 있습니다.

###5. 스키마 적용
우리는 보통 응답을 주고 받을 때 dto를 사용하게 됩니다. 이때 dto에서 @schema를 활용해 dto에 구체적인 값들을 명시해 swagger-ui에서 테스트시 유용한 결과들을 얻을 수 있습니다.(db의 테스트 데이터를 조회할 수 있게 특정 id나 파라미터를 예시로 설정해놓는 행위입니다.)

@Schema(description = "주문 성공 응답 Dto")
@Getter
@NoArgsConstructor
@Builder
@AllArgsConstructor
public class OrderResponseDto {

    @Schema(description = "주문 ID", example = "fd7e91c0-8a1c-4706-9eb3-0b0ce4d5184b")
    @JsonProperty("order_id")
    private UUID orderId;

    @Schema(description = "가게 이름", example = "교촌치킨")
    @JsonProperty("store_name")
    private String storeName;

    @Schema(description = "총 주문 금액", example = "124000")
    @JsonProperty("total_amount")
    private int totalAmount;

    @Schema(description = "주문 메뉴 리스트")
    @JsonProperty("order_menu_list")
    private List<OrderMenuListResponseDto> orderMenuList = new ArrayList<>();

    @Schema(description = "주문 일자", example = "2024-11-17T19:07:04.9538123")
    @JsonProperty("order_date")
    private LocalDateTime createdAt;

    @Schema(description = "주문 타입", example = "DELIVERY")
    @JsonProperty("order_type")
    private OrderTypeEnum orderType;

    @Schema(description = "주문 상태", example = "PENDING")
    @JsonProperty("order_status")
    private OrderStatusEnum orderStatus;

    public OrderResponseDto(Order order) {
        this.orderId = order.getOrderId();
        this.storeName = order.getStore().getStoreName();
        this.totalAmount = order.getTotalAmount();
        this.orderMenuList = order.getOrderMenuList().stream()
                .map(OrderMenuListResponseDto::new)
                .toList();
        this.orderType = order.getOrderType();
        this.orderStatus = order.getOrderStatus();
        this.createdAt = order.getCreatedAt();
    }
}
  • 위는 예시입니다. 저런식으로 class와 필드 값들에 대해 @Schema를 적용해 값을 설정해줄 수 있습니다.

결과


 

 

요런식으로 App을 실행 시킨 뒤 /swagger-ui/index.html 을 적용하게 되면 custom한 내용들을 알기 쉽게 볼 수 있습니다.

 

참고사항

jwt를 사용시 권한이 필요한 경우가 있습니다. 이때는 login 후 받아온 jwt값을 우측 상단의 Authorize에 넣어주시면 됩니다. 이때  "Bearer" 부분을 제외하고 넣어주셔야 제대로 인증이 완료됩니다.

 

 

 

이후에는 프로젝트에는 Rest Docs를 적용해볼 예정입니다.

 

 

다음 글에는 logging을 활용한 부분에 대해 적어보겠습니다.

'개발 > Java' 카테고리의 다른 글

Thread Pool  (0) 2024.11.22
static method(정적 메서드)  (1) 2024.11.21
Request, Response Logging  (1) 2024.11.20
Java의 제네릭 이용  (3) 2024.11.14
Swagger를 이용한 Spring Boot API 명세작업(1)  (1) 2024.11.13