OpenAPI 계약 드리프트 게이트
컨텍스트
2026년 5월, Lumie는 OpenAPI를 문서 전용 출력으로 처리하는 것을 중단하고 이를 백엔드 모놀리스와 Next.js 프런트엔드 간에 공유되는 빌드 계약으로 처리하기 시작했습니다.
출시는 두 가지 계층으로 이루어졌습니다.
- 백엔드
f20fa4cd및f7178524: 게시된 사양을 정확한 코드 생성으로 만든 다음 이를OpenApiSnapshotTest및 커밋된app/src/test/resources/openapi/api-docs.json뒤에 고정합니다. - 프런트엔드
51614d3및90c2265: orval을 추가하고,openapi.json에서 엔터티 범위 클라이언트를 생성하고, 컨트롤러 현실에서 벗어나 있던 손으로 작성한 쿼리 래퍼를 교체합니다.
23e3cb67, 84312d2d, f28469be, 3c9988f1, bbab0360 및 4497db03와 같은 최신 커밋은 새로운 아키텍처가 아닙니다. 이는 의도된 유지 관리 형태입니다. 계약 변경 사항은 자동으로 진행되는 대신 명시적으로 스냅샷을 업데이트합니다.
무엇이 깨졌거나 변경되었는지
원래 문제는 버그 하나가 아니었습니다. 두 저장소가 동일한 API를 처리하는 방식이 일치하 지 않았습니다.
- springdoc의 원시 출력은 몇 가지 주요 위치, 특히 페이징 가능한 쿼리 매개변수 및 응답 필드 요구사항에서 orval에 대해 충분히 정확하지 않았습니다.
- 백엔드에는 커밋된 생산자 측 골든 파일이 없으므로 사양 드리프트가 관련 없는 컨트롤러 또는 DTO 변경 사항 내에 숨겨질 수 있습니다.
- 프런트엔드에는 여러 엔터티에 걸쳐 감사하는 데 비용이 많이 드는 직접 작성한 쿼리 래퍼가 있었습니다.
백엔드는 게이트를 추가하기 전에 게시된 모양을 수정했습니다. OpenApiPageableFlattenCustomizer는 Pageable를 평면화하고 OpenApiResponseRequiredCustomizer는 응답 필드를 현재 키 필수로 표시하므로 생성된 TypeScript가 Java 레코드 응답과 일치합니다.
진단 및 결정 사다리
- 먼저 사양을 신뢰할 수 있게 만드세요. 부정확한 사양의 스냅샷 게이트는 잘못된 계약만 동결시킵니다.
- 그런 다음 라이브
/v3/api-docs를 가져와 정규화하고 커밋된 스냅샷과 비교하는 생산자 측 테스트를 추가합니다. - CI 매직 내부에 숨겨진 대신 프런트엔드 계약 핸드오프를 명시적으로 유지하세요. 백엔드는
snapshotOpenApi를 소유합니다. 프런트엔드는npx orval --config orval.config.ts를 소유합니다. - 생성된 모든 요청을 하나의 공유 변경자를 통해 라우팅하여 인증 쿠키, 테넌트 헤더, 업로드, 다운로드 및 재시도 동작이 생성된 코드에서 다시 구현되는 대신 중앙 집중화되도록 합니다.
생산자 게이트는 의도적으로 단순합니다.
// lumie-backend/app/src/test/java/com/lumie/app/OpenApiSnapshotTest.java
final String raw = mockMvc.perform(get("/v3/api-docs"))
.andExpect(status().isOk())
.andReturn().getResponse().getContentAsString(StandardCharsets.UTF_8);
final String current = normalize(raw);
해결
현재 분할 게이트에는 네 개의 내구성 있는 부분이 있습니다.
lumie-backend/app/src/test/java/com/lumie/app/OpenApiSnapshotTest.java는 라이브/v3/api-docs출력을 커밋된 백엔드 골든 파일과 비교합니다.lumie-backend/app/src/test/resources/openapi/api-docs.json는 계약 변경이 의도적인 경우에만 업데이트되는 검토 가능한 생산자 스냅샷입니다.lumie-backend/app/build.gradle.kts는 실행 중인 백엔드의/v3/api-docs를lumie-frontend/openapi.json에 복사하는./gradlew snapshotOpenApi를 정의합니다.lumie-frontend/orval.config.ts및src/shared/api/orval-mutator.ts는 공유 요청 스택을 통해 소비자 호출을 생성하고 라우팅합니다. 업로드에는 JSON 직렬화가 아닌apiUpload(...)를 거치기 위해FormData가 필요했기 때문에 여기서b06ecb6는 중요한 후속 조치였습니다.
프론트엔드 핸드오프는 현재 소스 댓글에 여전히 명시적으로 나와 있습니다.
// lumie-frontend/orval.config.ts
// Refresh the snapshot with `./gradlew snapshotOpenApi` (lumie-backend), then
// regenerate with `npx orval --config orval.config.ts`.
예방
- 생산자 게이트와 소비자 게이트를 별도로 유지하 십시오. 게시된 계약이 예기치 않게 변경되면 백엔드가 실패해야 하며, 프런트엔드는 의도적으로 캡처된 사양에서만 재생성되어야 합니다.
- 보상을 위해 orval 또는 손으로 직접 작성한 프런트엔드 래퍼를 가르치는 대신 백엔드 사용자 정의 도구의 사양 형태 문제를 수정합니다.
- 하나의 mutator를 통해 생성된 클라이언트를 라우팅합니다. 현재
orval-mutator.ts는 업로드, 다운로드, 테넌트 전파 및 공유 인증 동작을 일관되게 유지하는 장소입니다. - 스냅샷 전용 커밋을 노이즈가 아닌 일반적인 유지 관리로 처리합니다. 반복되는
TEST(openapi): update ... snapshot커밋은 게이트가 작동하고 있다는 증거입니다.
일상적인 워크플로의 경우 현재 참조 문서는 백엔드 개발, 백엔드 인프라 및 프런트엔드 상태 관리입니다.