SpringDoc + Stoplight Elements API 문서 자동화 도입#62
Conversation
- 컨트롤러에 @Tag/@Operation/@apiresponse 부착 및 성공·에러 예시 JSON 명시 - 위시리스트 등록 API 의 400/409 응답을 ProblemDetail 스키마로 문서화 - DTO 필드별 @Schema(description, example, nullable, format) 부착으로 클라이언트 가이드 강화
- Swagger UI 없이 /v3/api-docs 엔드포인트만 제공하는 webmvc-api 모듈 사용 - OpenApiConfig: API 제목, 설명, 연락처, 라이선스, 서버 설정 - application.yml: springdoc api-docs 경로 및 기본 미디어 타입 설정
- /v3/api-docs 를 읽어 Stoplight Elements UI로 렌더링 - sidebar 레이아웃, hash 라우터 적용
|
No description provided. |
|
Warning Rate limit exceeded
Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 48 minutes and 29 seconds. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
WalkthroughSpringDoc OpenAPI 라이브러리를 Gradle 의존성에 추가하고, OpenAPI 설정 클래스를 작성하여 Changes
Estimated code review effort🎯 2 (Simple) | ⏱️ ~12 minutes Assessment against linked issues
리뷰 의견✨ 좋은 점OpenAPI 자동화 도입이 깔끔하게 진행되었습니다! 의존성 추가부터 설정, 어노테이션 적용까지 일관성 있게 구성되었네요. 💡 제안 및 개선 사항
📚 참고 자료🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (3)
src/main/kotlin/com/depromeet/team3/wishlist/controller/WishlistController.kt (1)
28-62:@Operation/@ApiResponses구성 자체는 깔끔합니다 👍성공 예시(JSON)에 실제 응답 모양 그대로 넣어주신 덕분에 Stoplight에서 그대로 "Try it" 가능해 보여요. 이런 식으로 컨트롤러에 응답 컨트랙트가 못 박혀 있으면 프론트와의 합의 비용이 확 줄어듭니다.
다만 이 long-form
examples가 컨트롤러마다 반복되면 금세 노이즈가 됩니다. 컨트롤러 4~5개 넘어가기 전에 공통 에러 응답 예시(Bad Request,Conflict등)를OpenApiConfig에서Components.examples로 등록해놓고@ApiResponse(content =@content(examples =@ExampleObject(ref = "#/components/examples/InvalidInput")))형태로 참조하는 패턴을 권장합니다. 지금 PR에서 당장 바꿀 필요는 없어요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/kotlin/com/depromeet/team3/wishlist/controller/WishlistController.kt` around lines 28 - 62, The controller currently embeds long ExampleObject JSON directly in WishlistController's `@Operation/`@ApiResponses which will create noise when repeated; extract common response examples (e.g., Bad Request, Conflict, and the success payload example if reused) into OpenApiConfig's Components.examples and update the annotations in WishlistController to reference them via ExampleObject(ref = "#/components/examples/YourExampleName") inside `@ApiResponse`(content = `@Content`(...)) so the controller keeps a compact annotation while reusing shared examples across controllers.src/main/resources/application.yml (1)
31-34: 운영 환경에서/v3/api-docs·/docs/index.html노출 정책을 정해두면 좋겠어요.지금 설정대로면 모든 프로파일(특히 prod)에서
/v3/api-docs가 인증 없이 그대로 열립니다. 아직 인증/시큐리티가 들어가기 전이라 당장 코드 수정이 필요한 건 아니지만, 내부 API 스펙(엔드포인트, 파라미터 시맨틱, 에러 카테고리 등)은 공격자에게 공짜 공격면이 됩니다. 한 번 외부에 인덱싱되면 회수도 어려워요.운영 적용 전에 아래 중 한 가지를 정해두시는 걸 권장합니다.
- prod 프로파일에서 비활성화:
springdoc.api-docs.enabled: false,swagger-ui.enabled: false(또는 정적/docs/**도 차단)- 또는 SecurityFilterChain에서
/v3/api-docs/**,/docs/**을 사내망/베이직 인증 뒤로 격리참고: SpringDoc – Disabling the springdoc-openapi endpoints.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/resources/application.yml` around lines 31 - 34, The springdoc endpoints are currently exposed unconditionally via the springdoc.api-docs config; for prod disable or protect them: add a prod-specific setting to application.yml setting springdoc.api-docs.enabled: false and swagger-ui.enabled: false (or block static /docs/**) to disable exposure in production, or update your SecurityFilterChain (the security configuration bean/method that registers HTTP security rules) to require internal-network or basic auth for the "/v3/api-docs/**" and "/docs/**" patterns; implement one of these two options and ensure the change targets the springdoc/api-docs config and the SecurityFilterChain route matchers so prod does not serve API docs publicly.src/main/kotlin/com/depromeet/team3/guest/controller/GuestController.kt (1)
28-44: 문서는 일관됩니다. 한 가지만 짚고 갈게요.
issueGuestId()가 새 식별자를 발급(=리소스 생성)하는데@ResponseStatus가 없어 기본200 OK로 응답합니다. 문서의responseCode = "200"은 실제 동작과 정확히 맞아 떨어지므로 본 PR 범위 내에서는 OK 입니다 ✅다만 시맨틱적으로는
201 Created가 더 정확합니다. 현재 클라이언트가 200을 가정하고 짠 코드가 없다면, 별도 PR에서@ResponseStatus(HttpStatus.CREATED)+ 문서도 201로 맞추는 걸 권장드려요. 지금 PR에서 같이 바꾸진 마시고요(범위가 커집니다).참고: RFC 9110 §15.3.2 – 201 Created
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/kotlin/com/depromeet/team3/guest/controller/GuestController.kt` around lines 28 - 44, The endpoint GuestController.issueGuestId issues a new resource but currently returns 200; to make it semantically correct, add `@ResponseStatus`(HttpStatus.CREATED) to the issueGuestId method and update the `@ApiResponse`(responseCode = "200", ...) to responseCode = "201" (adjust the description/example as needed) so the OpenAPI docs and runtime behavior both reflect 201 Created; locate these changes on the issueGuestId method and the surrounding ApiResponse/Content schema declarations referencing GuestResponse.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@src/main/kotlin/com/depromeet/team3/wishlist/controller/dto/WishlistRegisterRequest.kt`:
- Around line 6-22: The request body DTO lacks Bean Validation and the
controller isn't triggering validation: update the WishlistRegisterRequest data
class to add validation annotations on the url field (e.g., `@NotBlank` and a
`@Pattern` or URL-style constraint) and any necessary constraints on guestId, and
then add `@Valid` to the parameter in WishlistController.register(...) so Spring
will run validation before reaching ProductLink.parse(); ensure you use the
field-targeted annotations (e.g., `@field`:NotBlank) on url inside
WishlistRegisterRequest and keep the existing Schema metadata.
In
`@src/main/kotlin/com/depromeet/team3/wishlist/controller/WishlistController.kt`:
- Around line 87-110: Add a new duplicate/conflict category to the ErrorCategory
enum (e.g., DUPLICATE_RESOURCE or CONFLICT) and update the
WishAlreadyExistsException to use that category instead of
ErrorCategory.INVALID_INPUT; then update the OpenAPI example for the 409
response so its "category" and "detail" match the new ErrorCategory text and the
actual message produced by WishAlreadyExistsException. Ensure you modify the
ErrorCategory enum declaration and the WishAlreadyExistsException class
(overridden httpStatus/category properties) and the ApiResponse ExampleObject
for the 409 case so code, runtime response, and docs are consistent.
In `@src/main/resources/static/docs/index.html`:
- Around line 7-8: Pin the Stoplight Elements CDN to a hash-compatible release
and add SRI/crossorigin: replace the current unversioned script and stylesheet
tags with explicit v8.5.2 URLs (so router="hash" continues to work), add the
corresponding integrity attribute values and set crossorigin="anonymous" for
both <script> and <link>, and consider a local/static fallback if CDN fails; if
you instead want to use v9+, change router="hash" to router="history" and ensure
server-side routing is configured before switching.
---
Nitpick comments:
In `@src/main/kotlin/com/depromeet/team3/guest/controller/GuestController.kt`:
- Around line 28-44: The endpoint GuestController.issueGuestId issues a new
resource but currently returns 200; to make it semantically correct, add
`@ResponseStatus`(HttpStatus.CREATED) to the issueGuestId method and update the
`@ApiResponse`(responseCode = "200", ...) to responseCode = "201" (adjust the
description/example as needed) so the OpenAPI docs and runtime behavior both
reflect 201 Created; locate these changes on the issueGuestId method and the
surrounding ApiResponse/Content schema declarations referencing GuestResponse.
In
`@src/main/kotlin/com/depromeet/team3/wishlist/controller/WishlistController.kt`:
- Around line 28-62: The controller currently embeds long ExampleObject JSON
directly in WishlistController's `@Operation/`@ApiResponses which will create
noise when repeated; extract common response examples (e.g., Bad Request,
Conflict, and the success payload example if reused) into OpenApiConfig's
Components.examples and update the annotations in WishlistController to
reference them via ExampleObject(ref = "#/components/examples/YourExampleName")
inside `@ApiResponse`(content = `@Content`(...)) so the controller keeps a compact
annotation while reusing shared examples across controllers.
In `@src/main/resources/application.yml`:
- Around line 31-34: The springdoc endpoints are currently exposed
unconditionally via the springdoc.api-docs config; for prod disable or protect
them: add a prod-specific setting to application.yml setting
springdoc.api-docs.enabled: false and swagger-ui.enabled: false (or block static
/docs/**) to disable exposure in production, or update your SecurityFilterChain
(the security configuration bean/method that registers HTTP security rules) to
require internal-network or basic auth for the "/v3/api-docs/**" and "/docs/**"
patterns; implement one of these two options and ensure the change targets the
springdoc/api-docs config and the SecurityFilterChain route matchers so prod
does not serve API docs publicly.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro
Run ID: 58539509-097c-4ecb-83b1-6e7ebb5bbff9
📒 Files selected for processing (9)
build.gradle.ktssrc/main/kotlin/com/depromeet/team3/common/config/OpenApiConfig.ktsrc/main/kotlin/com/depromeet/team3/guest/controller/GuestController.ktsrc/main/kotlin/com/depromeet/team3/guest/controller/dto/GuestResponse.ktsrc/main/kotlin/com/depromeet/team3/wishlist/controller/WishlistController.ktsrc/main/kotlin/com/depromeet/team3/wishlist/controller/dto/WishlistRegisterRequest.ktsrc/main/kotlin/com/depromeet/team3/wishlist/controller/dto/WishlistRegisterResponse.ktsrc/main/resources/application.ymlsrc/main/resources/static/docs/index.html
| @Schema(description = "위시리스트 등록 요청") | ||
| data class WishlistRegisterRequest( | ||
| @field:Schema( | ||
| description = "등록할 상품 페이지 URL", | ||
| example = "https://www.example-shop.com/products/12345", | ||
| requiredMode = Schema.RequiredMode.REQUIRED, | ||
| ) | ||
| val url: String, | ||
|
|
||
| @field:Schema( | ||
| description = "게스트 식별자 (게스트 발급 API로 받은 UUID)", | ||
| example = "8f1a3c2b-9d44-4e2a-9b12-1a2b3c4d5e6f", | ||
| format = "uuid", | ||
| requiredMode = Schema.RequiredMode.REQUIRED, | ||
| ) | ||
| val guestId: UUID, | ||
| ) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# 컨트롤러에서 `@Valid` 사용 여부 + 기존 검증 어노테이션 사용 패턴 확인
rg -nP -C2 '@Valid\b|jakarta\.validation' --type=kotlinRepository: depromeet/18th-team3-server
Length of output: 53
🏁 Script executed:
# WishlistRegisterRequest.kt 파일 위치와 내용 확인
fd -t f WishlistRegisterRequest.ktRepository: depromeet/18th-team3-server
Length of output: 158
🏁 Script executed:
# Wishlist 관련 컨트롤러 찾기
fd -t f -i wishlist.*controller.ktRepository: depromeet/18th-team3-server
Length of output: 149
🏁 Script executed:
# 프로젝트 전체 검증 패턴 확인 (다른 DTO들의 예시)
rg -t kotlin '@field:(NotBlank|NotNull|Pattern|Valid)' -c | head -20Repository: depromeet/18th-team3-server
Length of output: 53
🏁 Script executed:
cat -n src/main/kotlin/com/depromeet/team3/wishlist/controller/dto/WishlistRegisterRequest.ktRepository: depromeet/18th-team3-server
Length of output: 870
🏁 Script executed:
cat -n src/main/kotlin/com/depromeet/team3/wishlist/controller/WishlistController.ktRepository: depromeet/18th-team3-server
Length of output: 5996
🏁 Script executed:
# 프로젝트의 일반적인 요청 DTO 패턴 확인 (다른 DTO 예시)
fd -t f -path '*/controller/dto/*.kt' | head -5 | xargs -I {} sh -c 'echo "=== {} ===" && head -30 {}'Repository: depromeet/18th-team3-server
Length of output: 240
🏁 Script executed:
# WishlistService.register 메서드에서 실제 검증 로직 확인
fd -t f WishlistService.kt | xargs -I {} sh -c 'echo "=== {} ===" && cat -n {}'Repository: depromeet/18th-team3-server
Length of output: 2060
🏁 Script executed:
# 다른 컨트롤러 DTO 패턴 비교 (2-3개 샘플)
find src/main/kotlin -path '*/controller/dto/*.kt' -type f | head -3 | xargs -I {} sh -c 'echo "=== {} ===" && cat -n {}'Repository: depromeet/18th-team3-server
Length of output: 3423
컨트롤러에 @Valid 붙이고, DTO에 검증 어노테이션을 추가해야 합니다.
현재 WishlistController.register()에서 @RequestBody 아래에 @Valid가 없어서, 컨트롤러 레벨에서 자동 검증이 작동하지 않습니다. Swagger 문서상으로는 requiredMode = REQUIRED + format = "uuid"라고 선언되어 있는데, 실제로는:
url이 빈 문자열이면 그대로 서비스까지 흘러가ProductLink.parse()에서 예외 발생 (컨트롤러 검증층 부재)- 문서와 실제 동작의 불일치 — 프론트가 Swagger를 믿고 호출했는데, 문서에 없는 500이나 예상 밖의 에러가 떨어질 수 있음
Spring Boot 표준 패턴과 API 안정성을 고려하면:
import jakarta.validation.constraints.NotBlank
import jakarta.validation.constraints.Pattern
data class WishlistRegisterRequest(
`@field`:Schema(/* ... */)
`@field`:NotBlank(message = "URL은 필수입니다.")
`@field`:Pattern(
regexp = "^https?://.+",
message = "지원하지 않는 URL 형식입니다."
)
val url: String,
`@field`:Schema(/* ... */)
val guestId: UUID,
)그리고 컨트롤러에서:
fun register(
`@Valid`
`@RequestBody`
request: WishlistRegisterRequest
): WishlistRegisterResponse프로젝트 전체에서 Bean Validation을 쓰지 않는 이유가 있다면 (서비스 레이어 검증 정책 등), 그 전략을 문서화하고 일관되게 유지하세요. 하지만 현재 상태는 Swagger 문서와 실제 구현이 어긋나 있으므로 개선이 필요합니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@src/main/kotlin/com/depromeet/team3/wishlist/controller/dto/WishlistRegisterRequest.kt`
around lines 6 - 22, The request body DTO lacks Bean Validation and the
controller isn't triggering validation: update the WishlistRegisterRequest data
class to add validation annotations on the url field (e.g., `@NotBlank` and a
`@Pattern` or URL-style constraint) and any necessary constraints on guestId, and
then add `@Valid` to the parameter in WishlistController.register(...) so Spring
will run validation before reaching ProductLink.parse(); ensure you use the
field-targeted annotations (e.g., `@field`:NotBlank) on url inside
WishlistRegisterRequest and keep the existing Schema metadata.
| ApiResponse( | ||
| responseCode = "409", | ||
| description = "이미 위시리스트에 등록된 상품", | ||
| content = [ | ||
| Content( | ||
| mediaType = MediaType.APPLICATION_PROBLEM_JSON_VALUE, | ||
| schema = Schema(implementation = ProblemDetail::class), | ||
| examples = [ | ||
| ExampleObject( | ||
| name = "중복 등록", | ||
| value = """ | ||
| { | ||
| "type": "about:blank", | ||
| "title": "Conflict", | ||
| "status": 409, | ||
| "detail": "입력 오류 — 요청을 수정하여 재시도", | ||
| "category": "INVALID_INPUT" | ||
| } | ||
| """, | ||
| ), | ||
| ], | ||
| ), | ||
| ], | ||
| ), |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# 실제 예외 핸들러에서 409와 함께 어떤 category 값을 쓰는지 확인
rg -nP -C3 '\bProblemDetail\b|category\s*='
rg -nP -C3 'register\s*\(' --type=kotlinRepository: depromeet/18th-team3-server
Length of output: 12010
🏁 Script executed:
#!/bin/bash
# WishAlreadyExistsException과 ErrorCategory 정의 확인
rg -A5 'class WishAlreadyExistsException'
rg -A10 'enum class ErrorCategory'
rg -nC3 'HttpMappable'Repository: depromeet/18th-team3-server
Length of output: 10286
🏁 Script executed:
#!/bin/bash
# ErrorCategory 전체 파일과 WishAlreadyExistsException 사용처 확인
cat src/main/kotlin/com/depromeet/team3/common/exception/ErrorCategory.kt
echo "---"
rg 'WishAlreadyExistsException' -C2Repository: depromeet/18th-team3-server
Length of output: 2764
409 응답의 category와 detail 메시지가 실제 예외 정의와 어긋나고 있습니다.
문제: OpenAPI 예시를 수정하는 것만으로는 부족합니다. 실제로 WishAlreadyExistsException이 ErrorCategory.INVALID_INPUT으로 설정되어 있어서, 클라이언트가 받는 응답은 항상 "입력 오류 — 요청을 수정하여 재시도"가 나옵니다.
WishAlreadyExistsException(라인 15):category = ErrorCategory.INVALID_INPUT→ 409(중복)에 400(검증 오류) 카테고리를 쓰는 것은 의미가 어긋남- 클라이언트가 "재시도"를 보고 무한 재시도 로직을 짤 수 있음
- 또한
ErrorCategoryenum에DUPLICATE또는CONFLICT같은 카테고리가 없음
해결 방안:
1단계: ErrorCategory enum에 새로운 카테고리 추가
enum class ErrorCategory(val description: String) {
INVALID_INPUT("입력 오류 — 요청을 수정하여 재시도"),
DUPLICATE_RESOURCE("이미 등록된 리소스입니다."), // 추가
// ... 나머지
}2단계: WishAlreadyExistsException에서 해당 카테고리 사용
class WishAlreadyExistsException(
guestId: UUID,
link: ProductLink,
) : BaseException("이미 위시리스트에 등록된 상품입니다. guestId=$guestId link=$link"), HttpMappable {
override val httpStatus: HttpStatus = HttpStatus.CONFLICT
override val category: ErrorCategory = ErrorCategory.DUPLICATE_RESOURCE // 변경
}3단계: OpenAPI 예시도 함께 수정
♻️ 문서 예시 수정
value = """
{
"type": "about:blank",
"title": "Conflict",
"status": 409,
- "detail": "입력 오류 — 요청을 수정하여 재시도",
- "category": "INVALID_INPUT"
+ "detail": "이미 등록된 리소스입니다.",
+ "category": "DUPLICATE_RESOURCE"
}
""",이렇게 하면 실제 응답과 문서가 일치하고, 클라이언트가 상태 코드와 카테고리로 명확하게 분기 처리할 수 있습니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@src/main/kotlin/com/depromeet/team3/wishlist/controller/WishlistController.kt`
around lines 87 - 110, Add a new duplicate/conflict category to the
ErrorCategory enum (e.g., DUPLICATE_RESOURCE or CONFLICT) and update the
WishAlreadyExistsException to use that category instead of
ErrorCategory.INVALID_INPUT; then update the OpenAPI example for the 409
response so its "category" and "detail" match the new ErrorCategory text and the
actual message produced by WishAlreadyExistsException. Ensure you modify the
ErrorCategory enum declaration and the WishAlreadyExistsException class
(overridden httpStatus/category properties) and the ApiResponse ExampleObject
for the 409 case so code, runtime response, and docs are consistent.
- v9.x에서 React Router v6 업그레이드로 hash 라우팅 회귀 버그 발생 - v8.5.2로 고정: hash router 정상 동작이 확인된 마지막 v8 버전 - integrity + crossorigin 속성으로 SRI 적용
- API 버전(v1)은 빌드 버전(0.0.1-SNAPSHOT)과 별개의 public 계약 버전 - project.version은 Gradle 컨텍스트 전용이라 Spring Bean에서 참조 불가
Situation
Task
Action
springdoc-openapi-starter-webmvc-api:3.0.3의존성 추가-webmvc-ui(Swagger UI 번들) 대신-webmvc-api(API 엔드포인트만) 사용 — Stoplight Elements가 UI 역할을 하므로 Swagger UI 불필요OpenApiConfig: API 제목, 설명, 연락처 등 메타데이터 설정GuestController,WishlistController의@Tag,@Operation,@ApiResponse어노테이션은 @m-a-king 의chore/swagger-openapi브랜치 작업을 cherry-pick/docs/index.html: Stoplight Elements CDN 로드 후/v3/api-docs를 읽어 렌더링Result
/docs/index.html문서 자동 반영@RestController만 있으면 기본 문서 생성, 어노테이션으로 설명 보강 가능연관 이슈