Skip to content

API 응답 포맷 ApiResponse로 통일#67

Merged
1o18z merged 5 commits intodevfrom
feat/25-api-response
Apr 30, 2026
Merged

API 응답 포맷 ApiResponse로 통일#67
1o18z merged 5 commits intodevfrom
feat/25-api-response

Conversation

@1o18z
Copy link
Copy Markdown
Collaborator

@1o18z 1o18z commented Apr 28, 2026

Situation

  • 기존 컨트롤러마다 응답 구조가 달랐고, 예외 응답은 Spring ProblemDetail 포맷을 사용해 성공/실패 응답을 클라이언트가 일관되게 처리하기 어려운 상황이었음
  • 새로운 기능(토너먼트 등) 추가 시에도 응답 포맷이 제각각 늘어날 우려가 있었음

Task

  • 모든 API 응답을 ApiResponse<T> 단일 포맷으로 통일
  • ProblemDetail 기반 예외 응답 제거 및 ApiResponse.fail() 형태로 일관화

Action

  • ApiResponse<T> 클래스 도입: status, data, detail, code 필드 구성, ok() / created() / fail() 팩토리 메서드 제공
  • PageResponse 클래스 추가 (페이지네이션 응답용)
  • GlobalExceptionHandler를 ProblemDetail 방식에서 ApiResponse.fail() 방식으로 전환
  • GuestController, WishlistController에 ApiResponse 적용
  • GuestControllerTest의 JSONPath를 $.guestId$.data.guestId로 수정 (ApiResponse 래핑 반영)

Result

  • 성공/실패 모든 응답이 { status, data, detail, code } 포맷으로 통일되어 클라이언트 파싱 일관성 확보
  • 신규 컨트롤러는 ApiResponse.ok(data) / ApiResponse.created(data, detail) 만 사용하면 포맷이 자동으로 맞춰짐

연관 이슈

Summary by CodeRabbit

릴리스 노트

  • Refactor

    • API 응답 구조를 표준화된 형식으로 통일하였습니다.
    • 예외 처리 응답 형식을 일관된 API 응답 포맷으로 변경하였습니다.
  • New Features

    • 페이지네이션 메타데이터 지원이 추가되었습니다.
  • Tests

    • API 응답 구조 변경에 따른 테스트를 업데이트하였습니다.

1o18z added 3 commits April 28, 2026 16:32
- ApiResponse, PageResponse 클래스 추가
- 기존 ProblemDetail 기반 예외 응답 제거
- 모든 예외 응답을 ApiResponse.fail 형태로 일관화
- ApiResponse 도입으로 응답이 data 필드로 래핑됨에 따라 $.guestId → $.data.guestId 로 수정
@github-actions
Copy link
Copy Markdown

No description provided.

@1o18z 1o18z changed the base branch from main to dev April 28, 2026 10:49
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 28, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

🗂️ Base branches to auto review (2)
  • main
  • develop

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 6621e9e1-5b16-4de1-b85d-a12c5ed071d9

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

공통 API 응답 래퍼 ApiResponse<T>를 도입하여 성공/실패 응답을 표준화했습니다. 예외 처리, 게스트/위시리스트 엔드포인트를 개편하고 페이지네이션 응답 구조를 추가했습니다.

Changes

Cohort / File(s) Summary
공통 응답 래퍼 도입
src/main/kotlin/com/depromeet/team3/common/response/ApiResponse.kt, src/main/kotlin/com/depromeet/team3/common/response/PageResponse.kt
새로운 ApiResponse<T> 클래스와 PageResponse 클래스를 도입하여 표준화된 응답 구조를 정의. ok(), created(), fail() 팩토리 메서드로 성공/실패 응답을 통일.
글로벌 예외 처리 개편
src/main/kotlin/com/depromeet/team3/common/exception/GlobalExceptionHandler.kt
ProblemDetail 기반 응답에서 ResponseEntity<ApiResponse<Nothing>>로 변경. 모든 예외 핸들러가 ApiResponse.fail()을 통해 통일된 형식의 오류 응답 반환.
엔드포인트 응답 래핑
src/main/kotlin/com/depromeet/team3/guest/controller/GuestController.kt, src/main/kotlin/com/depromeet/team3/wishlist/controller/WishlistController.kt
기존 응답을 ApiResponse<T>로 래핑. Swagger 문서 스키마 및 예제를 신규 응답 구조에 맞게 업데이트.
테스트 조정
src/test/kotlin/com/depromeet/team3/guest/controller/GuestControllerTest.kt
응답 어설션을 조정하여 래핑된 구조($.data.guestId)를 검증하도록 수정.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Assessment against linked issues

Objective Addressed Explanation
API 공통 응답 포맷 도입 - ApiResponse<T> 래퍼 클래스 구성 [#25]
성공/실패 응답 표준화 - ok(), created(), fail() 팩토리 메서드 제공 [#25]
에러 원인을 응답 본문에서 직접 확인 가능하도록 구성 [#25] fail() 메서드에서 ErrorCategory.description 또는 전달된 detail을 사용하나, ErrorCategory enum에 description 속성이 실제로 정의되어 있는지 확인 필요

Out-of-scope changes

Code Change Explanation
WishlistController.register() 반환 타입 변경 시 정확한 detail 값 지정 미흡 (src/main/kotlin/com/depromeet/team3/wishlist/controller/WishlistController.kt) 예시: ApiResponse.created(data=..., detail=null) 처리되는 경우 기본 메시지 사용 로직이 있는지 확인 필요. 비즈니스 의도가 명확하지 않음

💡 리뷰 의견

잘된 점

깔끔하게 진행된 표준화입니다. 팩토리 메서드 패턴으로 응답 생성 로직을 일관되게 제어하는 접근이 좋습니다.

개선 제안

1. ErrorCategory와 detail 처리 명확화

fun <T> fail(category: ErrorCategory, status: HttpStatus, detail: String? = null): ApiResponse<T>
  • detail이 null일 때 ErrorCategory.description을 사용한다고 했는데, ErrorCategory enum 정의를 확인해야 합니다.
  • 각 ErrorCategory가 의미 있는 설명 메시지를 가지도록 설계되었는지 확인하세요.

2. PageResponse 활용 지점 검토

  • PageResponse가 정의되었으나, 현재 PR에서는 사용되지 않고 있습니다.
  • 향후 페이지네이션 엔드포인트에서 ApiResponse<List<T>>와 함께 pageResponse 필드를 채워 사용할 계획인지 확인하는 것을 권장합니다.

3. 예외 핸들러 category 매핑 검토
GlobalExceptionHandler에서:

handleBaseException(e: BaseException):ApiResponse.fail(e.errorCategory, e.httpStatus, ...)
  • BaseExceptionerrorCategoryhttpStatus를 올바르게 제공하는지 확인하세요.
  • 특히 handleIllegalArgument()handleUnexpected()에서 사용된 ErrorCategory가 의미론적으로 적절한지 재검토 권장합니다.

4. Swagger 문서 일관성

  • WishlistControllerGuestController의 Swagger 예제가 실제 응답 구조와 일치하는지 재차 확인하세요.
  • 특히 code 필드가 어떤 규칙으로 채워지는지 문서화하면 좋겠습니다 (예: "COMMON_SUCCESS", "CREATED", status.name 등).

학습 포인트

Generic 팩토리 메서드 패턴을 효과적으로 사용했습니다. 이 패턴은 유연성과 타입 안전성을 동시에 제공하므로, 향후 응답 처리가 더 복잡해지면 이 구조를 확장하는 방식을 생각해보세요.

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning 제목은 Conventional Commits 형식을 따르지 않습니다. 필수 요소인 type(feat, fix, chore 등)과 콜론이 누락되었습니다. 제목을 'feat: API 응답 포맷 ApiResponse로 통일' 또는 'refactor: API 응답 포맷 ApiResponse로 통일' 형식으로 수정하세요. type은 변경의 성질을 반영하는 것이 좋습니다.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/25-api-response

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/test/kotlin/com/depromeet/team3/guest/controller/GuestControllerTest.kt (1)

34-57: ⚠️ Potential issue | 🟡 Minor

래퍼 전체를 한 번은 검증해 주세요.

지금은 data.guestId만 확인해서 status/detail/code가 깨져도 테스트가 통과합니다. 공통 응답 포맷 통일이 이번 PR의 핵심이니, 최소 한 케이스는 wrapper 필드까지 검증해야 회귀를 더 잘 잡을 수 있습니다.

수정 예시
         mockMvc.perform(post("/api/v1/guests"))
             .andExpect(status().isOk)
             .andExpect(jsonPath("$.data.guestId").value(expectedUuid.toString()))
+            .andExpect(jsonPath("$.detail").value("요청이 정상적으로 처리되었습니다."))
+            .andExpect(jsonPath("$.code").value("COMMON_SUCCESS"))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/kotlin/com/depromeet/team3/guest/controller/GuestControllerTest.kt`
around lines 34 - 57, Update the GuestControllerTest to assert the common
response wrapper fields in at least one test (e.g., inside the test functions
`POST api v1 guests 는 발급된 UUID 를 guestId 필드로 반환한다` or `POST api v1 guests 는 매
요청마다 GuestService 를 호출해 새 UUID 를 받아온다` in class GuestControllerTest): in
addition to verifying jsonPath("$.data.guestId"), also assert the wrapper fields
like jsonPath("$.status"), jsonPath("$.code") and jsonPath("$.detail") (or their
actual names used by your API) to ensure the response format is validated; keep
the existing guestId assertions and add these extra jsonPath checks to fail if
wrapper fields are incorrect.
src/main/kotlin/com/depromeet/team3/guest/controller/GuestController.kt (1)

29-52: ⚠️ Potential issue | 🟡 Minor

Swagger 문서와 실제 응답 구조가 불일치합니다.

schema = Schema(implementation = GuestResponse::class)로 설정하면 OpenAPI 문서는 GuestResponse만 보여주지만, 실제 응답은 ApiResponse<GuestResponse> 래퍼입니다. 결과적으로 status, detail, code 필드가 생성된 API 문서에서 빠지게 됩니다.

또한 예제의 detail 값이 "성공"이지만, ApiResponse.ok()의 실제 기본값은 "요청이 정상적으로 처리되었습니다."입니다. 클라이언트가 예제를 보고 구현했을 때 실제 응답과 달라 혼동할 수 있습니다.

수정 방안:

`@SwaggerApiResponse`(
    responseCode = "200",
    description = "게스트 ID 발급 성공",
    content = [
        Content(
            schema = Schema(implementation = ApiResponse::class),
            examples = [
                ExampleObject(
                    name = "발급 성공",
                    value = """
                        {
                          "status": 200,
                          "data": { "guestId": "8f1a3c2b-9d44-4e2a-9b12-1a2b3c4d5e6f" },
                          "detail": "요청이 정상적으로 처리되었습니다.",
                          "code": "COMMON_SUCCESS"
                        }
                    """,
                ),
            ],
        ),
    ],
)

schemaApiResponse::class로 수정하고, 예제의 detail을 실제 기본값으로 맞춰주세요.

🤖 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 29 - 52, The Swagger annotation on GuestController.issueGuestId
currently declares Schema(implementation = GuestResponse::class) but the
endpoint actually returns ApiResponse<GuestResponse> via
ApiResponse.ok(GuestResponse(...)), so update the `@SwaggerApiResponse` to use
Schema(implementation = ApiResponse::class) and adjust the example JSON to match
ApiResponse.ok()'s real default detail ("요청이 정상적으로 처리되었습니다.") and shape
(including status, detail, code, and data with guestId) so docs match the
runtime response.
🤖 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/common/exception/GlobalExceptionHandler.kt`:
- Around line 25-30: In GlobalExceptionHandler.handleIllegalArgument, include
the exception message (e.message) in the 400 response body so clients get the
validation detail; update the ApiResponse produced in handleIllegalArgument
(currently ApiResponse.fail(ErrorCategory.INVALID_INPUT,
HttpStatus.BAD_REQUEST)) to pass the exception detail (e.message) into the
ApiResponse payload (use the existing overload or add a detail field) so
ErrorCategory.INVALID_INPUT and the HTTP 400 status are preserved while the
response.detail contains e.message.

In `@src/main/kotlin/com/depromeet/team3/common/response/ApiResponse.kt`:
- Around line 17-43: The ApiResponse creators currently bind the response "code"
to HTTP status names, preventing domain-specific codes like
WISH_CREATED/INVALID_INPUT; update the factory methods (created(), fail(), and
ok() as needed) to accept an explicit code:String parameter (or provide separate
successCode:String / errorCode:String parameters) and set ApiResponse.code to
that value instead of using status.name; ensure created() defaults remain (e.g.,
default code "CREATED" or require caller-specified code) and adjust fail() to
accept either an error code parameter or derive it from ErrorCategory so
controllers can pass WISH_DUPLICATED/INVALID_INPUT.

---

Outside diff comments:
In `@src/main/kotlin/com/depromeet/team3/guest/controller/GuestController.kt`:
- Around line 29-52: The Swagger annotation on GuestController.issueGuestId
currently declares Schema(implementation = GuestResponse::class) but the
endpoint actually returns ApiResponse<GuestResponse> via
ApiResponse.ok(GuestResponse(...)), so update the `@SwaggerApiResponse` to use
Schema(implementation = ApiResponse::class) and adjust the example JSON to match
ApiResponse.ok()'s real default detail ("요청이 정상적으로 처리되었습니다.") and shape
(including status, detail, code, and data with guestId) so docs match the
runtime response.

In `@src/test/kotlin/com/depromeet/team3/guest/controller/GuestControllerTest.kt`:
- Around line 34-57: Update the GuestControllerTest to assert the common
response wrapper fields in at least one test (e.g., inside the test functions
`POST api v1 guests 는 발급된 UUID 를 guestId 필드로 반환한다` or `POST api v1 guests 는 매
요청마다 GuestService 를 호출해 새 UUID 를 받아온다` in class GuestControllerTest): in
addition to verifying jsonPath("$.data.guestId"), also assert the wrapper fields
like jsonPath("$.status"), jsonPath("$.code") and jsonPath("$.detail") (or their
actual names used by your API) to ensure the response format is validated; keep
the existing guestId assertions and add these extra jsonPath checks to fail if
wrapper fields are incorrect.
🪄 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: 5ba88699-aad0-4a58-a28c-f8d6e806aa7d

📥 Commits

Reviewing files that changed from the base of the PR and between 99dc358 and 732ae53.

📒 Files selected for processing (6)
  • src/main/kotlin/com/depromeet/team3/common/exception/GlobalExceptionHandler.kt
  • src/main/kotlin/com/depromeet/team3/common/response/ApiResponse.kt
  • src/main/kotlin/com/depromeet/team3/common/response/PageResponse.kt
  • src/main/kotlin/com/depromeet/team3/guest/controller/GuestController.kt
  • src/main/kotlin/com/depromeet/team3/wishlist/controller/WishlistController.kt
  • src/test/kotlin/com/depromeet/team3/guest/controller/GuestControllerTest.kt

Comment on lines 25 to +30
@ExceptionHandler(IllegalArgumentException::class)
fun handleIllegalArgument(e: IllegalArgumentException): ProblemDetail {
fun handleIllegalArgument(e: IllegalArgumentException): ResponseEntity<ApiResponse<Nothing>> {
log.warn("[IllegalArgumentException] {}", e.message)
return ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, e.message ?: "잘못된 요청입니다.").apply {
setProperty("category", ErrorCategory.INVALID_INPUT)
}
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(ApiResponse.fail(ErrorCategory.INVALID_INPUT, HttpStatus.BAD_REQUEST))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

400 응답에 예외 메시지를 전달하세요.

지금은 IllegalArgumentException의 실제 사유를 버리고 항상 같은 입력 오류만 내려서, 어떤 필드/값이 잘못됐는지 클라이언트가 알 수 없습니다. 공통 포맷을 쓰더라도 detail에는 e.message를 실어야 디버깅과 UI 메시지 매핑이 가능합니다.

수정 예시
-            .body(ApiResponse.fail(ErrorCategory.INVALID_INPUT, HttpStatus.BAD_REQUEST))
+            .body(ApiResponse.fail(ErrorCategory.INVALID_INPUT, HttpStatus.BAD_REQUEST, e.message))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/kotlin/com/depromeet/team3/common/exception/GlobalExceptionHandler.kt`
around lines 25 - 30, In GlobalExceptionHandler.handleIllegalArgument, include
the exception message (e.message) in the 400 response body so clients get the
validation detail; update the ApiResponse produced in handleIllegalArgument
(currently ApiResponse.fail(ErrorCategory.INVALID_INPUT,
HttpStatus.BAD_REQUEST)) to pass the exception detail (e.message) into the
ApiResponse payload (use the existing overload or add a detail field) so
ErrorCategory.INVALID_INPUT and the HTTP 400 status are preserved while the
response.detail contains e.message.

Comment on lines +17 to +43
fun <T> ok(data: T? = null): ApiResponse<T> = ApiResponse(
status = HttpStatus.OK.value(),
data = data,
detail = "요청이 정상적으로 처리되었습니다.",
code = "COMMON_SUCCESS",
)

fun <T> created(
data: T,
detail: String?,
): ApiResponse<T> = ApiResponse(
status = HttpStatus.CREATED.value(),
data = data,
detail = detail ?: "생성되었습니다.",
code = "CREATED",
)

fun <T> fail(
category: ErrorCategory,
status: HttpStatus,
detail: String? = null,
): ApiResponse<T> = ApiResponse(
status = status.value(),
data = null,
detail = detail ?: category.description,
code = status.name,
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

응답 code를 HTTP 상태명에 묶지 마세요.

지금 created()/fail()CREATED, BAD_REQUEST, CONFLICT 같은 상태명만 내려서, PR 설명과 컨트롤러 예제에서 기대하는 WISH_CREATED, INVALID_INPUT, WISH_DUPLICATED를 표현할 수 없습니다. 이 구조로는 클라이언트가 code만 보고 성공/실패를 세분화할 수 없으니, code를 매개변수로 받거나 성공/오류용 코드 체계를 분리해 주세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/kotlin/com/depromeet/team3/common/response/ApiResponse.kt` around
lines 17 - 43, The ApiResponse creators currently bind the response "code" to
HTTP status names, preventing domain-specific codes like
WISH_CREATED/INVALID_INPUT; update the factory methods (created(), fail(), and
ok() as needed) to accept an explicit code:String parameter (or provide separate
successCode:String / errorCode:String parameters) and set ApiResponse.code to
that value instead of using status.name; ensure created() defaults remain (e.g.,
default code "CREATED" or require caller-specified code) and adjust fail() to
accept either an error code parameter or derive it from ErrorCategory so
controllers can pass WISH_DUPLICATED/INVALID_INPUT.

Copy link
Copy Markdown
Collaborator

@m-a-king m-a-king left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

간단한 수정만 남겼어~ 수고했습니당


@ExceptionHandler(BaseException::class)
fun handleBaseException(e: BaseException): ProblemDetail {
fun handleBaseException(e: BaseException): ResponseEntity<ApiResponse<Nothing>> {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing 멋있다

Comment on lines +17 to +32
fun <T> ok(data: T? = null): ApiResponse<T> = ApiResponse(
status = HttpStatus.OK.value(),
data = data,
detail = "요청이 정상적으로 처리되었습니다.",
code = "COMMON_SUCCESS",
)

fun <T> created(
data: T,
detail: String?,
): ApiResponse<T> = ApiResponse(
status = HttpStatus.CREATED.value(),
data = data,
detail = detail ?: "생성되었습니다.",
code = "CREATED",
)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

common success 랑 created 를 일관적으로 가져가면 좋겠당!

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

좋아~~~ 수정수정

import io.swagger.v3.oas.annotations.media.ExampleObject
import io.swagger.v3.oas.annotations.media.Schema
import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.responses.ApiResponse as SwaggerApiResponse
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분은 왜 수정된거지?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요거 스웨거의 ApiResponse랑 우리꺼 응답래퍼 ApiResponse랑 이름이 똑같아가지고
스웨거꺼는 SwaggerApiResponse로 쓰도록 수정했어

@1o18z 1o18z merged commit 8d74641 into dev Apr 30, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

API 공통 응답 포맷 도입

3 participants