Skip to content

feat(S15P11A705-53): 공통 응답 봉투(ApiResponse) 적용 - #26

Merged
minyongP merged 10 commits into
devfrom
feat/S15P11A705-53-api-response-envelope
Jul 27, 2026
Merged

feat(S15P11A705-53): 공통 응답 봉투(ApiResponse) 적용#26
minyongP merged 10 commits into
devfrom
feat/S15P11A705-53-api-response-envelope

Conversation

@minyongP

Copy link
Copy Markdown
Contributor

요약

성공·오류 응답을 같은 봉투로 통일합니다. 클라이언트는 success 하나로 분기합니다. 도메인 컨트롤러는 DTO를 그대로 반환하고 ResponseBodyAdvice가 감쌉니다.

{ "success": true,  "data": { } }
{ "success": false, "error": { "code": "", "message": "", "fieldErrors": [], "traceId": "" } }

Jira: S15P11A705-53 — https://ssafy.atlassian.net/browse/S15P11A705-53
함께 머지 필요: 명세 PR Team-PinLog/docs#11 (§1.6 봉투 정의, §1.5 오류+traceId, §11.0 타입)

⚠️ 프론트엔드 영향 (Breaking)

res.itemsres.data.items, res.coderes.error.code. FE와 동시 배포가 필요합니다.

변경 사항

  • global/response/ApiResponse<T> — record {success, data, error} + ok(T)/fail(ErrorResponse). @JsonInclude(NON_NULL)로 성공에 error 키·오류에 data 키가 나오지 않음. 성공 쪽에 message 필드 없음.
  • global/web/ApiResponseBodyAdvice — 세 조건 모두 만족할 때만 감쌈: ① 컨트롤러가 com.pinlog.pinlogback.domain 하위 ② 반환형이 이미 ApiResponse가 아님 ③ Jackson 컨버터. body == null이면 그대로 통과(204 유지), 이미 봉투면 이중 감싸기 없음.
  • GlobalExceptionHandler — 4분기를 ApiResponse.fail(...)로. 상태 코드·ErrorCode 선택·로깅 레벨은 불변.
  • ErrorResponse구조 변경 없이 재사용(error 본문).
  • 문서: api-conventions.md·error-handling.md 갱신, BD-03(결정), BI-03(구현 리포트), WORKLOG.

설계: URL 패턴이 아니라 컨트롤러 패키지로 판정

actuator(org.springframework.boot.actuate.*)·springdoc(org.springdoc.*)은 도메인 패키지가 아니므로 자동으로 제외됩니다. URL 제외 목록을 관리하는 방식과 달리 default-deny라, 새 서드파티 핸들러가 들어와도 감싸지지 않습니다. 감쌌다면 배포 헬스체크와 Swagger UI가 깨집니다.

테스트 / 검증

  • ./gradlew clean check --no-daemon BUILD SUCCESSFUL (25 tests, 0 failures)
  • 회귀 감시 통과DeploymentContractTests(actuator health가 봉투 없이 {"status":"UP"}, 미매핑 URL 404) · OpenApiDocsTests(/v3/api-docs 200 + 유효 OpenAPI)
  • Advice: DTO 감싸기 / 이중 감싸기 없음 / ResponseEntity<Void>·직접 호출로 null 분기 실증(분기를 깨면 실패함을 확인)
  • 오류 3종이 {success:false, error:{…}}, error.traceId 존재, 상태 코드 불변, 500이 내부 정보 미노출
  • 봉투 JSON 단정은 운영 Jackson 3 매퍼(tools.jackson)로 검증

리뷰 중 잡아낸 것 (기록용)

  • 플랜이 지정한 AbstractJackson2HttpMessageConverter(Jackson 2 이름)는 이 프로젝트에서 절대 매칭되지 않아 Advice가 영구 무동작이 될 수 있었습니다. 실제 상위 타입 AbstractJacksonHttpMessageConverter(Jackson 3)로 정정했습니다.
  • BD-03이 "OpenAPI 스키마가 깊어진다"고 잘못 기록 → 감싸기는 런타임이라 springdoc 스키마에는 봉투가 반영되지 않고 실제 응답과 어긋납니다. 사실대로 정정했습니다.

미결 / 후속 (티켓 필요)

  1. springdoc 커스터마이저 — 첫 도메인 엔드포인트와 함께 OperationCustomizer/ModelConverter를 넣어야 문서와 실제 응답이 일치합니다(현재는 도메인 엔드포인트가 없어 영향 0).
  2. 인증 401/403 봉투 — Security의 entry point·handler와 Boot /error 폴백은 @RestControllerAdvice를 거치지 않으므로 인증 PR에서 직접 ApiResponse.fail(...)을 만들어야 합니다(error-handling.md에 명기).
  3. framework 4xx가 500으로 수렴 — malformed JSON·405·415가 여전히 500입니다. 기존 티켓 S15P11A705-40에서 처리.
  4. error.impact(409 DELETE_CONFIRMATION_REQUIRED) — 명세에는 있으나 미구현. Record 삭제 티켓에서 ErrorResponse 확장점과 함께 도입.
  5. 사소: supports()의 패키지 판정이 startsWithdomainsupport 같은 형제 패키지도 매칭(현재 그런 패키지 없음), ErrorResponse@JsonInclude(NON_NULL) 없음(async 경로에서 traceId: null 가능).

minyongP and others added 7 commits July 27, 2026 13:01
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… mapper

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
api-conventions.md wrongly described list pagination as page/size/sort
while the response used cursors, contradicting spec §1.4 (cursor+size
only, no sort). BD-03 claimed the envelope deepens the OpenAPI schema,
but ApiResponseBodyAdvice wraps at runtime while springdoc introspects
declared controller types with no OperationCustomizer/ModelConverter,
so the schema actually diverges from the real response instead.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@minyongP minyongP self-assigned this Jul 27, 2026
…-response-envelope

# Conflicts:
#	docs/backend/WORKLOG.md
#	docs/backend/decisions/README.md
#	docs/backend/implements/README.md
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@minyongP

Copy link
Copy Markdown
Contributor Author

추가: OpenAPI 스키마 정합 (Task 5, 5297b1d)

리뷰에서 지적된 문서/런타임 불일치를 이 PR 안에서 닫았습니다. ApiResponseBodyAdvice는 런타임에 감싸는데 springdoc은 선언된 반환 타입을 문서화하므로, 그대로 두면 /v3/api-docs가 래퍼 없는 스키마를 내보내 FE 코드 생성이 {success, data} 없는 타입을 만듭니다.

  • global/config/ApiResponseOpenApiCustomizer (springdoc OperationCustomizer) 추가 — Advice와 같은 판정(com.pinlog.pinlogback.domain 패키지 + 이미 ApiResponse가 아님)으로 대상 operation의 2xx 응답 스키마만 감쌉니다. 원래 스키마가 $ref$refdata에 그대로 넣어 스키마 중복 정의를 만들지 않고, 204처럼 content가 없는 응답은 건드리지 않습니다.
  • 검증: @SpringBootTest에 테스트 전용 컨트롤러를 @Import해 springdoc이 문서화하게 만든 뒤 실제 /api/core/v3/api-docs 응답의 200 스키마가 success/data를 갖는지 단정합니다(커스터마이저 없으면 실패함을 RED로 확인).
  • 범위 밖: 오류 응답 스키마 문서화. BD-03에 기록했습니다.

./gradlew clean check --no-daemon BUILD SUCCESSFULOpenApiDocsTests 2/2, DeploymentContractTests 4/4 유지(커스터마이저가 문서 생성을 깨뜨리지 않음).

dev 통합

origin/dev(#25 member 머지분)를 머지해 충돌을 해소했습니다(7e28e13). 충돌은 문서 3건(WORKLOG.md, decisions/README.md, implements/README.md)뿐이었고 양쪽 항목을 모두 살렸습니다 — BD-02/BI-02(−41)와 BD-03/BI-03(−53)이 나란히 등재됩니다. 통합 후 전체 게이트를 다시 돌려 통과 확인했습니다.

남은 잠재 격차 (후속)

커스터마이저는 Advice의 세 번째 조건(Jackson 컨버터)을 미러링하지 않습니다. 지금은 해당 엔드포인트가 없어 무해하지만, String을 반환하는 도메인 엔드포인트가 생기면 런타임은 감싸지 않는데 문서는 감싸진 것으로 보일 수 있습니다.

"봉투"는 envelope의 직역으로 어색해 문서·주석 전반에서 envelope로 바꿨다.
계약·동작 변경은 없다.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@minyongP
minyongP merged commit af76261 into dev Jul 27, 2026
1 check passed
@minyongP
minyongP deleted the feat/S15P11A705-53-api-response-envelope branch July 27, 2026 05:59
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.

2 participants