
조금 더 예쁘게 보였으면 하는 마음에, 조금 더 쉽게 작성했으면 하는 마음에 찾기 시작했던 스웨거 설정들입니다.
새로 알아낸 방법이 있을 때마다 업데이트를 진행해보겠습니다.
모든 내용은 Kotlin + Spring Boot를 기반으로 합니다.

하나의 서버에서 여러 서비스에 대한 명세를 내려주는 경우가 있습니다. 저의 경우 유저들이 사용하는 APP과 어드민 서비스를 제공하는 Web API를 제공했는데, 페이지를 나눠서 제공하면 각 클라이언트 개발자들이 훨씬 보기 좋게 제공할 수 있습니다.
@Configuration
class SwaggerConfig {
@Bean
fun appApi(): GroupedOpenApi =
GroupedOpenApi
.builder()
.group("App API")
.pathsToExclude("/admin/**")
.build()
@Bean
fun adminApi(): GroupedOpenApi =
GroupedOpenApi
.builder()
.group("Admin API")
.pathsToMatch("/admin/**") // 어드민 API만 포함
.build()
}
GroupedOpenApi를 Bean으로 등록하면 되는데, group을 결정짓는 방식은 여러가지가 있습니다. 경로(path) 기반, 패키지(package) 기반 등 여러가지 옵션 중 선택할 수 있습니다.
흔히 Get 메서드에서 파라미터로 다양한 인자값을 전달합니다. 파라미터가 많아지다보면 Controller Parameter에 @RequestParam 어노테이션이 떡칠 되어 있습니다. 거기에 Valid 어노테이션까지 붙는다면 어지러워지죠.
@Operation(summary = "RequestParam")
@GetMapping("/v1/request-param")
fun requestParam(
@RequestParam(required = true) year: Int,
@Min(value = 1L, message = "월은 1월부터 12월까지입니다.")
@Max(value = 12L, message = "월은 1월부터 12월까지입니다.")
@RequestParam(required = false) month: Int
): ResponseEntity<SuccessResponse<SchedulePageApiResponseDto>>

그래서 @RequestBody로 요청 객체를 처리하듯, @RequestParam도 동일하게 처리할 수 있습니다. @ParameterObject를 이용하면 됩니다.
@Operation(summary = "ParameterObject")
@GetMapping("/v1/parameter-object")
fun parameterObject(
@Valid @ParameterObject request: SchedulePageApiRequestDto
): ResponseEntity<SuccessResponse<SchedulePageApiResponseDto>>
data class SchedulePageApiRequestDto(
@field:Schema(description = "년도", required = true)
val year: Int,
@field:Schema(description = "월", required = false)
@field:Min(value = 1L, message = "월은 1월부터 12월까지입니다.")
@field:Max(value = 12L, message = "월은 1월부터 12월까지입니다.")
val month: Int
)

@ParameterObject를 이용하면 @Valid나 @Schema 어노테이션도 모두 객체 안으로 뺄 수 있습니다. 무엇보다 Controller에서 Service로 인자를 전달할 때 request 통으로 전달할 수 있으므로, 굉장히 편하고 좋습니다.
파라미터나 바디 형태의 데이터를 요청 시에 전달 받을 때, Primitive Type이 아닌 객체 형태로 데이터를 받을 수 있습니다.
class Version
@JsonCreator
constructor(
@field:Schema(name = "version", description = "버전 정보 (x.y.z)", required = true)
val value: String
) {
@JsonIgnore
val major: Int
@JsonIgnore
val minor: Int
@JsonIgnore
val patch: Int
init {
val splited = value.split(".")
if (splited.size != 3) {
throw BusinessException(ConfigError.WRONG_VERSION_FORMAT)
}
major = splited[0].toInt()
minor = splited[1].toInt()
patch = splited[2].toInt()
}
}
@GetMapping("/v1/operations/force-update")
fun getForceUpdateInfo(
@ParameterObject request: ForceUpdateInquiryRequest
): ResponseEntity<SuccessResponse<ForceUpdateApiResponseDto>>
data class ForceUpdateInquiryRequest(
@field:Schema(name = "version", description = "현재 클라이언트 버전", example = "1.2.3", required = true)
val version: Version,
@field:Schema(description = "클라이언트 플랫폼", required = true)
val platform: ClientPlatform
)
@PutMapping("/admin/v1/operations/minimum-support-versions")
fun updateMinSupportVersion(
@RequestBody request: AdminMinSupportVersionUpdateRequest
): ResponseEntity<Unit>
data class AdminMinSupportVersionUpdateRequest(
@Schema(description = "플랫폼")
val platform: ClientPlatform,
@Schema(description = "최소 지원 버전", example = "1.2.3", required = true)
val version: Version
)

객체 형태의 데이터임에도 불구하고 단순한 String인 것처럼 노출됩니다. 물론 실제 요청에서도 String 형태로 보내면 Version으로 잘 초기화 됩니다.
서버에서 204(No Content)를 내려주는 것을 표시하기 위해 ApiResponse를 다음과 같이 설정할 때가 있습니다. 불필요하게 Example Value 영역이 생기면서 공간을 많이 잡아먹게 됩니다.
@ApiResponse(responseCode = "204")

ApiResponse에 텅 빈 content를 넣게 되면 Example Value를 포함하여 아무것도 남기지 않을 수 있습니다.
@ApiResponse(
responseCode = "204",
content = [Content()]
)

API를 묶는 단위인 Tag의 높이가 달라 불-편한 경우가 있습니다. description을 지정하지 않은 경우에 이런 상황이 발생합니다.

TMI라면, description이 있는 경우 위 아래로 margin이 생기면서 Tag의 name(제목)보다 더 큰 영역을 차지하게 됩니다.
그래서 저는 별다른 설명할 게 없는 경우 description에 언더스코어(_)를 넣어둡니다. 티도 잘 안 나고 좋습니다.

공통 에러 처리는 어떻게 하면 좋을까요?
공통 에러 처리는 공식문서에 따라 OpenApiCustomizer를 통해 API 명세서를 커스터마이징 할 수 있습니다.
해당 내용은 너무 길어서 제 블로그의 다른 글에 설명을 기입해두었습니다.