상세 컨텐츠

본문 제목

Swagger 사용하고 사랑 받는 Server 되어 보자!

Server

by 가든잉 2024. 1. 18. 18:06

본문

안녕하세요 고잉고잉 Server 팀 윤정원입니다.

 

DO SOPT 앱잼 마지막 과제에 Swagger 사용하기가 있어서.. 공부 하면서 정리할 겸 아티클을 작성해보려 합니다.

Swagger를 사용해보니 생각보다 너무 간단하고 쉽게 적용 가능하며, API 테스트를 하는 부분도 너무 편리해져서 앞으로 프로젝트마다 사용할 것 같습니다. 그러니 처음 써보시는 분들도 제 아티클을 참고해서 편하게 API 테스트를 하는, 사랑받는 서버가 되어보세요 :)

 

Swagger 란?

- 웹 서비스 API에 대한 문서를 작성하고 관리하기 위한 오픈 소스 프레임워크이다.

- OpenAPI Sepecification을 사용하여 API에 대한 표준을 정의하고 문서를 생성한다.

 

즉, 개발한 Rest API를 편리하게 문서화해주고, 이를 통해 관리 및 호출해 테스트할 수 있도록 도와주는 프레임 워크이다.

 

Swagger 장점

- Code와 함께 API 명세서를 관리할 수 있다.

- 다양한 프레임 워크에서 지원한다.

- 일관되고 깔끔한 UI를 가지고 있다.

- API Test가 가능하다.

 

Swagger 단점

- Code가 길어져서 한눈에 쉽게 보기 힘들다.

- API 명세서를 확인하려면 기능적으로 API가 다 구현되어야 확인이 가능하다.

     -> 따라서 Swagger를 사용하더라도, 문서로 미리 API 명세서를 작업해두는 것이 좋다.

 

SpringBoot에서 Swagger 사용하기

build.gradle에서 Swagger 관련 dependency 추가

Swagger를 사용하는 대표적 라이브러리로는 Springfox Swagger, Springdoc가 있다.

Springfox Swagger는 Spring 프레임워크를 사용하는 프로젝트에서 Swagger를 이용해서 API 문서를 쉽게 사용할 수 있도록 도와주는 라이브러리이다. 하지만 2020년 7월에 3.0.0 버전을 마지막으로 이후 업데이트가 중단되었다.

Springdoc 또한 Springfox Swagger와 유사하며, Springfox가 업데이트를 중단한 사이 처음 나온 라이브러리이다. 

그래서 되도록이면 Springdoc로 Swagger를 사용하는 것이 좋다.

(SpringBoot 3.0.0 이상 부터는 Springfox가 아닌 Springdoc 사용이 필수라고 한다.)

 

우리 프로젝트는 Springboot 3.1.7을 사용하고 있으며, Swagger v3을 사용하려 한다.

따라서 build.gradle에 아래의 코드를 작성해줬다.

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

https://springdoc.org/을 통해 확인해보면 현재 springdoc-openapi는 v2.3.0까지 나왔으며, 해당 라이브러리는 OpenAPI 3, Spring-boot v3, Swagger-ui, OAuth2 등의 라이브러리를 지원하고 있다.

(내가 2.0.2를 쓴 이유는 딱히 없다. 버전을 확인하고 자유롭게 프로젝트에 맞는 버전을 사용하면 될 것 같다!)

 

SwaggerConfig 설정하기

의존성 관리가 다 됐다면 이제 SwaggerConfig를 작성해주면 된다.

@SecurityScheme(
        name = "Authorization",
        type = SecuritySchemeType.HTTP,
        in = SecuritySchemeIn.HEADER,
        bearerFormat = "JWT",
        scheme = "Bearer"
)
@Configuration
public class SwaggerConfig {
    @Bean
    public OpenAPI api() {
        Info info = new Info()
                .title("Doorip API Docs")
                .version("v1.0")
                .description("Doorip 서비스 API 명세서 입니다.");
        return new OpenAPI()
                .info(info);
    }
}

 

- 우리가 만든 SwaggerConfig라는 클래스와 메소드를 @Configuration과 @Bean을 통해 수동으로 스프링 컨테이너에 Bean으로 등록해준다.

첫번째 사진은 OpenAPI에 대한 디자인이다. 이런 디자인을 기반으로 실제 코드가 작성되있음을 볼 수 있다.

 

- OpenAPI는 Swagger 라이브러리의 소스 코드로, OpenAPI 객체에는 다양한 클래스들과 멤버 변수들이 있다. 

     - Info는 해당 OpenAPI에 대한 기본 정보(제목, 버전, 설명 등)인 메타데이터가 포함된 객체이다.

          - title, description, version, contact, license등 Info class를 다시 뜯어보면 나오는 속성들을 각각 설정해줄 수 있다.

 

이 외에도 OpenAPI에 Security, Paths, Components 등을 해당 Config를 통해 설정할 수 있다.

(Security는 API 보안에 대한 정보를 나타내는 SecurityRequirement 객체의 리스트, Components는 재사용 가능한 구성 요소를 정의하는 Components 객체 등.. 과 같이 다양한 역할을 하는 요소들을 설정할 수 있다.)

OpenAPI Design과 속성에 대한 사용법, 예시는 https://swagger.io/blog/api-design/openapi-driven-api-design/를 통해 확인해 프로젝트에 맞춰 자율적으로 사용하면 좋을 것 이다. (공식문서 외에도 'OAS 3.0' 이라는 키워드로 검색을 하면 관련된 Design을 찾아볼 수 있다.)

 

나는 이번 프로젝트에서는 Info에서 title, version, description 만 설정해뒀다.

해당 설정들은 실제 Swagger 연결 후 페이지에서 아래 사진과 같이 윗줄에 표시된다.

Info 설정한 Swagger-ui

 

 

프로젝트에서 Session, Token, JWT와 같은 인가 방식을 사용한다면, SwaggerConfig에서 Security를 추가로 설정해줘야 한다.

Security에서 이 관련된 부분을 추가하는 방법은 다양하다.

 

SecurityRequirement securityRequirement = new SecurityRequirement().addList("JWT");
SecurityScheme securityScheme = new SecurityScheme().type(SecurityScheme.Type.HTTP)
				.in(SecurityScheme.In.HEADER).name("Authorization").scheme("bearer");
Components components = new Components().addSecuritySchemes("JWT", securityScheme);

return new OpenAPI().components(components).info(info).addSecurityItem(securityRequirement);

 

먼저, 위와 같이 OpenAPI의 Design에 맞춰 SecurityRequirement, SecurityScheme, Components의 구조를 이해하고 사용하는 인가 방식의 name, type, scheme, in 과 같은 설정을 해줄 수 있다. 많은 레퍼런스들에서 이러한 방식을 사용했기에 예시로 이 코드도 같이 넣어놨다.

 

 

해당 설정을 하면 Swagger-ui에서 오른쪽 상단위에 Authorize 버튼이 생긴다. 

이곳에 토큰 값과 같은 인가 코드를 넣어주면 모든 API Test시 @SecurityRequirement가 있는 API에 한해서 요청시에 이 값이 Header에 들어가게 된다. (이 기능이 매우매우 편리하다. Postman으로 Test 할 때는 API 마다 Header에 값을 넣어줬다면, 이제는 더 이상 그럴 필요가 없다!!!!)

 

@SecurityScheme(
        name = "Authorization",
        type = SecuritySchemeType.HTTP,
        in = SecuritySchemeIn.HEADER,
        bearerFormat = "JWT",
        scheme = "Bearer"
)

나는 개인적으로 어노테이션을 사용하는 것이 더 깔끔하다고 생각해서 보안 체계를 정의해주는 @SecurityScheme의 속성들로 나머지 값들을 설정해주었다. 어노테이션 안에 속성들을 지정해주면 OpenAPI Design에 맞춘 많은 객체 생성 과정을 생략할 수 있게 된다.

 

이 외에도 다양한 방법으로 인가 방식을 지정해줄 수 있을 것이다. 많은 레퍼런스가 있었지만 내 생각엔 어노테이션이 가장 깔끔하다고 생각되었다.

 

추가로 필터링해서 그룹화를 하려면 SwaggerConfig에서 설정 할 수 있다. 하지만 난 그룹화를 하진 않았다.

 

SecurityConfig에서 Whitelist 추가하기

"/swagger-ui/**", "/v3/api-docs/**"

 

특정 리소스, 경로, 기능에 대한 접근을 허용하는 whitelist에 위의 두 개의 url을 추가해줘야 한다.

이 부분은 기존의 SecurityConfig whitelist 처리를 해놓은 곳에 해당 url을 추가해주면 된다.

 

이후 남은 Swagger 설정

마지막으로 Controller에서 각각의 API 마다 Swagger 설정을 해주면 된다.

 

@Tag(name = "여행 TODO 관련 API")
@SecurityRequirement(name = "Authorization")
@RequiredArgsConstructor
@RequestMapping("/api/trips")
@Controller
public class TripApiController{
    private final TripService tripService;
    private final TripDetailService tripDetailService;


	 @Operation(
            summary = "여행 TODO 생성 API",
            responses = {
                    @ApiResponse(
                            responseCode = "201",
                            description = "요청이 성공했습니다."),
                    @ApiResponse(
                            responseCode = "400",
                            description = "잘못된 요청입니다.",
                            content = @Content),
                    @ApiResponse(
                            responseCode = "401",
                            description = "액세스 토큰의 형식이 올바르지 않습니다. Bearer 타입을 확인해 주세요.",
                            content = @Content)})
    @PostMapping
    public ResponseEntity<BaseResponse<?>> createTrip(@Parameter(hidden = true)
                                                      @UserId final Long userId,
                                                      @RequestBody final TripCreateRequest request) {
        final TripCreateResponse response = tripService.createTripAndParticipant(userId, request);
        return ApiResponseUtil.success(SuccessMessage.CREATED, response);
    }

    @Operation(
            summary = "여행 대시보드 전체 조회 API",
            responses = {
                    @ApiResponse(
                            responseCode = "200",
                            description = "요청이 성공했습니다."),
                    @ApiResponse(
                            responseCode = "400",
                            description = "유효하지 않은 요청 파라미터 값입니다.",
                            content = @Content),
                    @ApiResponse(
                            responseCode = "401",
                            description = "액세스 토큰의 형식이 올바르지 않습니다. Bearer 타입을 확인해 주세요.",
                            content = @Content),
                    @ApiResponse(
                            responseCode = "500",
                            description = "서버 내부 오류입니다.",
                            content = @Content)})
    @GetMapping
    public ResponseEntity<BaseResponse<?>> getTrips(@Parameter(hidden = true)
                                                    @UserId final Long userId,
                                                    @RequestParam final String progress) {
        final TripGetResponse response = tripService.getTrips(userId, progress);
        return ApiResponseUtil.success(SuccessMessage.OK, response);
    }
}

 

Tag 설정한 Swagger-ui

- @Tag : API 그룹 설정을 하는 어노테이션이다.

     - name 속성에 태그의 이름을 작성하면 된다.

     - description 속성에 태그에 대한 설명을 작성하면 된다.

 

- @SecurityRequirement : Swagger API를 테스팅할 때 인증 정보를 보낼 수 있게 설정해주는 어노테이션이다.

보통 SecurityRequirement 어노테이션은 Controller 단에 적어둔다.

하나의 Controller에 인증 정보가 필요 없는 API 목록이 생긴다면, Controller단이 아닌, API마다 어노테이션을 달아서 사용해야한다.

 

- @Operation : API 상세 정보 설정을 하는 어노테이션이다.

     - summary 속성은 API에 대한 간략 설명을 작성하면 된다.

     - description 속성은 API에 대한 상세 설명을 작성하면 된다.

     - reponses 속성은 API Response 리스트를 작성하면 된다.

     - parameters 속성은 API Parmeter 리스트를 작성하면 된다.

이 외에도 tags, security.. 등 다양한 설정을 할 수 있는 속성들이 있다.

 

responses 에서는 ApiResponse 어노테이션을 통해 나올 수 있는 응답코드와 설명을 작성해주면 된다.

 

- @ApiResponse : API Response 설정을 하는 어노테이션이다.

     - responseCode 속성은 http 상태 코드에 대해 작성하면 된다,

     - description 속성은 response에 대한 설명을 작성하면 된다.

     - content 속성은 Response payload 구조를 작성하면 된다.

          - schema는 payload에서 이용하는 Schema를 의미하며, hidden은 Schema를 숨길 수 있고, implementation은 Schema 대상 클래스를 지정할 수 있다.

 

이 외에도 @Schema를 통해 DTO에서 각각의 필드에 이름이나 설명, 예시와 같은 다양한 설정을 할 수 있다.

또한, @Parameter를 통해 요청 Parameter에 관한 설정을 할 수도 있다.

 

이와 같이 지정해줄 수 있는 무수히 많은 속성과 방식이 있기에.. 각자 취향과 목적에 맞게 키워드를 찾아서 사용하면 좋을 것 같다.

 

Operation 설정한 Swagger-ui & ApiResponse 설정한 Swagger-ui

실제 Swagger-ui화면은 이렇게 나오니 참고해서 Operation와 ApiResponse를 작성해주면 될 것이다.   

 

 

Swagger 설정 분리

하지만 위의 방법처럼 작성하면 Code가 길어져서 한눈에 보기 어렵다.

이러한 단점을 보완하기 위해서 Swagger 설정을 interface로 빼서 관리해 가독성을 높일 수 있다.

 

실제 Controller에서는 우리가 만든 Swagger 설정 interface를 implements해서 해당 api들을 @Override 해주면 된다.

나는 '도메인Api' 로 인터페이스를 생성하고 해당 Controller에서 implements해서 사용했다. 사용한 코드 예시는 다음과 같다.

@Tag(name = "여행 관련 API")
@SecurityRequirement(name = "Authorization")
public interface TripApi {
    @Operation(
            summary = "여행 생성 API",
            responses = {
                    @ApiResponse(
                            responseCode = "201",
                            description = "요청이 성공했습니다."),
                    @ApiResponse(
                            responseCode = "400",
                            description = "유효하지 않은 날짜 타입입니다.",
                            content = @Content),
                    @ApiResponse(
                            responseCode = "401",
                            description = "액세스 토큰의 형식이 올바르지 않습니다. Bearer 타입을 확인해 주세요.",
                            content = @Content),
                    @ApiResponse(
                            responseCode = "404",
                            description = "존재하지 않는 회원입니다.",
                            content = @Content),
                    @ApiResponse(
                            responseCode = "405",
                            description = "잘못된 HTTP method 요청입니다.",
                            content = @Content),
                    @ApiResponse(
                            responseCode = "500",
                            description = "서버 내부 오류입니다.",
                            content = @Content)})
    ResponseEntity<BaseResponse<?>> createTrip(@Parameter(hidden = true)
                                               @UserId final Long userId,
                                               @RequestBody final TripCreateRequest request);

    @Operation(
            summary = "여행 대시보드 전체 조회 API",
            responses = {
                    @ApiResponse(
                            responseCode = "200",
                            description = "요청이 성공했습니다."),
                    @ApiResponse(
                            responseCode = "400",
                            description = "유효하지 않은 요청 파라미터 값입니다.",
                            content = @Content),
                    @ApiResponse(
                            responseCode = "401",
                            description = "액세스 토큰의 형식이 올바르지 않습니다. Bearer 타입을 확인해 주세요.",
                            content = @Content),
                    @ApiResponse(
                            responseCode = "500",
                            description = "서버 내부 오류입니다.",
                            content = @Content)})
    ResponseEntity<BaseResponse<?>> getTrips(@Parameter(hidden = true)
                                             @UserId final Long userId,
                                             @Parameter(name = "progress", description = "complete/incomplete")
                                             @RequestParam final String progress);

 

@Tag(name = "여행 TODO 관련 API")
@SecurityRequirement(name = "Authorization")
@RequiredArgsConstructor
@RequestMapping("/api/trips")
@Controller
public class TripApiController implements TripApi{
    private final TripService tripService;
    private final TripDetailService tripDetailService;

    @PostMapping
    @Override
    public ResponseEntity<BaseResponse<?>> createTrip(@UserId final Long userId,
                                                      @RequestBody final TripCreateRequest request) {
        final TripCreateResponse response = tripService.createTripAndParticipant(userId, request);
        return ApiResponseUtil.success(SuccessMessage.CREATED, response);
    }

    @GetMapping
    @Override
    public ResponseEntity<BaseResponse<?>> getTrips(@UserId final Long userId,
                                                    @RequestParam final String progress) {
        final TripGetResponse response = tripService.getTrips(userId, progress);
        return ApiResponseUtil.success(SuccessMessage.OK, response);
    }
}

 

 

해당 Swagger 설정이 되면 ip주소 or localhost의 ' /swagger-ui/index.html' 에서 Swagger-ui를 확인할 수 있다.

 

 

관련 오류

Swagger-ui 실행 방법은 하나의 API를 선택해 Try it out을 클릭 후 해당 API에 맞는 request를 작성하고 Execute를 누르면 실행 된다. 이때 Swagger-ui 페이지에 실제 실행된 Curl이 보여진다. 여기서 요청한 값들을 잘 전달했는지 확인할 수 있다.

처음에 Bearer 타입이 맞지 않는 토큰이라는 오류가 전달 되었다.  확인해보니 애초에 -H 'Authorization: Bearer 토큰 값'  인 해당 Curl 명령어가 날라가지 않았다.

 

알고보니 @SecurityScheme의 name 을 잘못 설정해두어서 연결이 되지 않았다. name은 각각의 HTTP Authorization header에 무엇을 사용할지에 따라 Authorization이나 jwt 를 설정해주면 될 것이다.

 

이와 비슷한 오류들은 Curl을 확인해서 어느 부분에서 오류가 나는지 확인해보면 좋을 것 같다.

 

추가로 우리는 http로 해서 CORS에러는 뜨지 않았지만, https 배포시 해당 url에 관한 CORS를 허용해주면 된다고 한다.

 

 

결론?

Swagger는 사람마다 다양하게 사용하는 경우가 많아서 아직 나도 모르는 설정이나 속성들이 많을 것이다. 그래도 최대한 기본적인 설정들은 꼼꼼하게 작성해 놨으니 이 글을 참고한다면 크게 어렵진 않을 것 같다 하하하😊

이 외는 알아서 본인 프로젝트에 알맞는 Swagger 설정을 해서 사용하면 될 것 같다. 

 

모두들.. Swagger 쓰고 사랑받는 Server가 되어보세요❤️

 

저는 Swagger 안써도 사랑받는 서버긴 합니다 ㅋㅋ 본인 피셜 사랑둥이 ㅋㅋ