Swagger detail settings

GEONNY·2024년 8월 13일
0

Building-API

목록 보기
21/28
post-thumbnail

application.yml 으로 설정 가능한 옵션은 Link 에서 알아 보았습니다. 이제 보다 디테일한 설정 방법을 알아보겠습니다.

📌Swagger 상단 상세 설정

📍@OpenAPIDefinition

Swagger 상단 부분을 상세 설정하는 방법을 알아보겠습니다.config.swagger.SwaggerConfig 파일을 생성합니다. @OpenAPIDefinition 을 사용하여 Swagger 상단에 정보를 커스텀 할 수 있습니다.

@OpenAPIDefinition(
        info = @Info(title = "GEONLEE API Documentation"
                , version = "v1.0.0"
                , description = """
                - Springboot 3.3.1 기반 JPA 활용 API
                """
                , termsOfService = "https://velog.io/@geonlee/posts"
                , license = @License(name = "Apache License Version 2.0"
                                   , url = "https://www.apache.org/licenses/LICENSE-2.0")
                , contact = @Contact(name = "GEONLEE"
                                   , email = "geonlee@email.com")
        ),
        servers = {
                @Server(url = "/my-api", description = "API 구축 테스트 서버")
        },
        externalDocs = @ExternalDocumentation(description = "Swagger reference"
        	, url = "https://springdoc.org/properties.html")
)
@Configuration
public class SwaggerConfig {}

🎈@Info

OpenAPI 문서의 제목, 설명, 버전 등을 설정할 수 있는 메타데이터를 제공합니다.

🎈@Servers

API가 배포된 서버의 URL과 관련 정보를 정의합니다.
여러 서버를 지정할 수 있으며, 서버의 URL과 설명을 설정할 수 있습니다.

🎈externalDocs

API에 대한 외부 문서의 URL과 설명을 설정할 수 있습니다. 추가적인 문서나 참조 자료를 연결할 때 유용합니다.

기타 설정할 수 있는 다른 옵션들은 추후 해당 기능을 추가하면서 알아보겠습니다.

📌Operation 상세 설정

📍@Tag

@Tag 를 활용하여 Operation을 그룹화 할 수 있습니다. 클라이언트가 쉽게 Operation 을 찾을 수 있게 도와줍니다.
Controller class 상단에 위치하며 class 내 모든 method 가 해당 tag의 하위로 설정됩니다.
entity.member.MemberController

@Tag(name = "회원 관리", description = "회원에 대한 조회/추가/수정/삭제 기능")
public class MemberController {

📍@Operation

@Operation 을 활용하여 개별 Operation 의 동작, 입력 및 출력, 보안 요구 사항 등을 상세히 기술할 수 있습니다. 이를 통해 API 문서가 더욱 구체적이고 명확해지며, Swagger UI에서도 이를 직관적으로 표현할 수 있습니다.
Method 상단에 위치하며 해당 Operation 을 설명합니다.
entity.member.MemberController.getMemberById

@GetMapping(value = "/members/{memberId}", produces = MediaType.APPLICATION_JSON_VALUE)
@Operation(summary = "회원 ID 로 회원 조회", description = """
         ### MemberId 유효성 목록
         - 영문, 숫자만 조회
         - 사용자의 경우 최소 5, 최대 30자까지 전달 가능, User group 의 경우에만 체크
        """,
        parameters = {
                @Parameter(name = "memberId"
                		 , description = "Member 를 조회하기 위한 ID를 입력", required = true)
        }, operationId = "API-001-01")
public ResponseEntity<ItemResponse<MemberSearchResponse>> getMemberById(
            @PathVariable("memberId")
            @Pattern(regexp = "^[zA-Z0-9]+$")
            @Length(min = 5, max = 30, groups = MemberValidationGroup.User.class) String memberId) {
    return ResponseEntity.ok()
            .body(ItemResponse.<MemberSearchResponse>builder()
                    .status(messageConfig.getCode(NormalCode.SEARCH_SUCCESS))
                    .message(messageConfig.getMessage(NormalCode.SEARCH_SUCCESS))
                    .item(memberService.getMemberById(memberId))
                    .build());
}


description은 Markdown 도 지원하니 원하는 스타일로 작성하시면 됩니다. 클라이언트에게 문의사항이 오지 않도록 최대한 상세히 기술합니다.

📍@Schma

@Schma 를 활용하여 Request/Response 객체의 설명을 추가할 수 있습니다. name 이나 title 속성의 경우 해당 객체의 이름을 바꾸니 용도에 따라 활용하세요. example 의 경우 클라이언트가 어떤값이 전달되는지 확인할 수 있으므로, 꼭 실제 전달될 데이터와 같은 형식으로 작성해야 합니다.
common.response.ItemResponse

@Builder
@Schema(description = "공통 단일 객체 응답 Record")
public record ItemResponse<T>(
        @Schema(description = "응답 상태", example = "OK")
        String status,
        @Schema(description = "응답 상태 메시지"
        	  , example = "데이터를 조회/추가/수정/삭제 하는데 성공하였습니다.")
        String message,
        @Schema(description = "단일 응답 객체")
        T item
) {
}

domamin.member.record.MemberSearchResponse

@Builder
@Schema(description = "회원 조회 응답 Record")
public record MemberSearchResponse(
        @Schema(description = "회원 ID", example = "member01")
        String memberId,
        @Schema(description = "회원 명", example = "회원1")
        String memberName,
        @Schema(description = "사용 여부[Y/N]", example = "N")
        String useYn,
        @Schema(description = "생성일시", example = "2024-08-12 00:00:00")
        @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
        LocalDateTime createDate,
        @Schema(description = "수정일시", example = "2024-08-12 00:00:00")
        @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
        LocalDateTime updateDate
) {
}

@JsonFormat을 사용할 경우 Schema 객체의 타입은 string, format은 date-time 으로 인식되지만, example의 경우 실제 타입인 LocalDateTime 으로 인식됩니다. 제대로된 example data 를 보여주기위해선 response 객체의 field type 을 String 으로 변경하고, mapper 쪽에서 변경 처리를 해줍니다.
domain.member.record.MemberSearchResponse

@Schema(description = "생성일시", example = "2024-08-12 00:00:00")
String createDate, //LocalDateTime -> String 으로 변경
@Schema(description = "수정일시", example = "2024-08-12 00:00:00")
String updateDate //LocalDateTime -> String 으로 변경

common.converter.Converter

public class Converter {
    public static String localDateTimeToFormattedString(LocalDateTime localDateTime) {
        if (ObjectUtils.isEmpty(localDateTime)) {
            return null;
        }
        return localDateTime.format(
        	DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss", Locale.KOREA));
    }
}

domain.member.MemberMapper

@Mapper(componentModel = "spring", imports = Converter.class) // Add import Converter
public interface MemberMapper extends GenericMapper<MemberSearchResponse, Member> {
    @Mappings({
            @Mapping(target = "memberId", source = "memberId", qualifiedByName = "toUpperCase"),
            //Converter.localDateTimeToFormattedString 호출
            @Mapping(target = "createDate"
               , expression = "java(Converter.localDateTimeToFormattedString(entity.getCreateDate()))"),
            @Mapping(target = "updateDate"
               , expression = "java(Converter.localDateTimeToFormattedString(entity.getUpdateDate()))")
    })
    MemberSearchResponse toRecord(Member entity);

    @Named("toUpperCase")
    default String toUpperCase(String text) {
        return StringUtils.isEmpty(text) ? null : text.toUpperCase();
    }
}

📍@ApiResponse

@ApiResponse 는 바로 위에서 본 Operation의 Responses 설정하는 Annotation입니다. 아무 설정도 안할 경우 정상 응답에 대한 결과만 표출 됩니다.
예를들어, getMemberById method 에는 EntityNotFoundException 이 예외처리 되어 있습니다. 이를 Response에 추가하기 위해 Controller 에 @ApiResponse 를 추가합니다.
domain.member.MemberServiceImpl

@Override
public MemberSearchResponse getMemberById(String memberId) {
    Member memberEntity = memberRepository.findById(memberId)
            .orElseThrow(() -> new EntityNotFoundException(
            	"회원 ID 가 존재하지 않습니다. -> " + memberId)
            );
    return memberMapper.toRecord(memberEntity);
}

domain.member.MemberController

@ApiResponses({
    @ApiResponse(responseCode = "OK", description = "정상 응답", useReturnTypeSchema = true),
    @ApiResponse(responseCode = "ERR_DT_01", description = "데이터가 존재하지 않음"
            , content = @Content(schema = @Schema(implementation = ErrorResponse.class)
            , examples = @ExampleObject(value = """
                    {
                        "status": "ERR_DT_01",
                        "message": "데이터가 존재하지 않습니다."
                    }
            """)
        )
    )    
})
public ResponseEntity<ItemResponse<MemberSearchResponse>> getMemberById(
//이하 생략

이번 프로젝트의 경우 Exception 이 발생하더라도 모든 HTTP Status Code는 200으로 전달할 것이기 때문에 responseCode 에는 status code를 입력했습니다. HTTP Status Code를 전달하고 싶다면 200, 400 과 같이 입력해주시면 됩니다. responseCode 는 중복될 수 없으니 참고 바랍니다.
useReturnTypeSchema 는 설정된 schema type을 그대로 리턴하는 옵션입니다

모든 응답에 대해서 공통되는 Response 를 매번 이렇게 추가하는 것은 비효율 적일 수 있습니다. 다음에 OperationCustomizer 를 활용하여 공통 Response 를 추가하는 방법을 알아보겠습니다.

📌Grouping

GroupedOpenApi 객체를 Spring Bean에 등록해서 API Operation들을 grouping 할 수 있습니다.
config.swagger.SwaggerConfig

@Configuration
public class SwaggerConfig {

        @Bean
        public GroupedOpenApi version1APi() {
            return GroupedOpenApi.builder()
                    .group("v1.0")
                    .pathsToMatch("/v1/**")
                    .build();
            }

        @Bean
        public GroupedOpenApi version2APi() {
            return GroupedOpenApi.builder()
                    .group("v2.0")
                    .pathsToMatch("/v2/**")
                    .build();
}

이렇게 설정할 경우 URI 가 v1 으로 시작하는 operation과 v2 로 시작하는 operation으로 그룹화 됩니다. 그룹화를 할 경우 Swagger UI 상단에 'Select a definition' 콤보박스가 추가되고, 선택에 따라 해당 group 의 operation이 표출됩니다.

현재 진행중인 Operation 들은 모두 v1에 속하도록 MemberController 상단에 RequestMapping을 추가합니다.

@RestController
@RequiredArgsConstructor
@Validated({MemberValidationGroup.User.class})
@Tag(name = "회원 관리", description = "회원에 대한 조회/추가/수정/삭제 기능")
@RequestMapping("v1")
public class MemberController {
//이하 생략

모든 MemberController의 Operation 이 v1 group 에 속하게 됩니다.

📌마무리

Swagger는 단순한 API 문서화를 넘어서, API 설계와 개발 프로세스 전반에 걸쳐 협업과 효율성을 극대화하는 강력한 도구입니다. 잘 작성된 Swagger 문서는 명확한 커뮤니케이션을 가능하게 하고, 개발자, 테스트팀, 그리고 최종 사용자가 모두 동일한 이해를 공유할 수 있도록 도와줍니다. 그러니 최대한 친절하고 구체적으로 작성하도록 합시다!😉

profile
Back-end developer

0개의 댓글