본문으로 건너뛰기

OpenAPI 계약 드리프트 게이트

컨텍스트

2026년 5월, Lumie는 OpenAPI를 문서 전용 출력으로 처리하는 것을 중단하고 이를 백엔드 모놀리스와 Next.js 프런트엔드 간에 공유되는 빌드 계약으로 처리하기 시작했습니다.

출시는 두 가지 계층으로 이루어졌습니다.

  • 백엔드 f20fa4cdf7178524: 게시된 사양을 정확한 코드 생성으로 만든 다음 이를 OpenApiSnapshotTest 및 커밋된 app/src/test/resources/openapi/api-docs.json 뒤에 고정합니다.
  • 프런트엔드 51614d390c2265: orval을 추가하고, openapi.json에서 엔터티 범위 클라이언트를 생성하고, 컨트롤러 현실에서 벗어나 있던 손으로 작성한 쿼리 래퍼를 교체합니다.

23e3cb67, 84312d2d, f28469be, 3c9988f1, bbab03604497db03와 같은 최신 커밋은 새로운 아키텍처가 아닙니다. 이는 의도된 유지 관리 형태입니다. 계약 변경 사항은 자동으로 진행되는 대신 명시적으로 스냅샷을 업데이트합니다.

무엇이 깨졌거나 변경되었는지

원래 문제는 버그 하나가 아니었습니다. 두 저장소가 동일한 API를 처리하는 방식이 일치하지 않았습니다.

  • springdoc의 원시 출력은 몇 가지 주요 위치, 특히 페이징 가능한 쿼리 매개변수 및 응답 필드 요구사항에서 orval에 대해 충분히 정확하지 않았습니다.
  • 백엔드에는 커밋된 생산자 측 골든 파일이 없으므로 사양 드리프트가 관련 없는 컨트롤러 또는 DTO 변경 사항 내에 숨겨질 수 있습니다.
  • 프런트엔드에는 여러 엔터티에 걸쳐 감사하는 데 비용이 많이 드는 직접 작성한 쿼리 래퍼가 있었습니다.

백엔드는 게이트를 추가하기 전에 게시된 모양을 수정했습니다. OpenApiPageableFlattenCustomizerPageable를 평면화하고 OpenApiResponseRequiredCustomizer는 응답 필드를 현재 키 필수로 표시하므로 생성된 TypeScript가 Java 레코드 응답과 일치합니다.

진단 및 결정 사다리

  1. 먼저 사양을 신뢰할 수 있게 만드세요. 부정확한 사양의 스냅샷 게이트는 잘못된 계약만 동결시킵니다.
  2. 그런 다음 라이브 /v3/api-docs를 가져와 정규화하고 커밋된 스냅샷과 비교하는 생산자 측 테스트를 추가합니다.
  3. CI 매직 내부에 숨겨진 대신 프런트엔드 계약 핸드오프를 명시적으로 유지하세요. 백엔드는 snapshotOpenApi를 소유합니다. 프런트엔드는 npx orval --config orval.config.ts를 소유합니다.
  4. 생성된 모든 요청을 하나의 공유 변경자를 통해 라우팅하여 인증 쿠키, 테넌트 헤더, 업로드, 다운로드 및 재시도 동작이 생성된 코드에서 다시 구현되는 대신 중앙 집중화되도록 합니다.

생산자 게이트는 의도적으로 단순합니다.

// 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-docslumie-frontend/openapi.json에 복사하는 ./gradlew snapshotOpenApi를 정의합니다.
  • lumie-frontend/orval.config.tssrc/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 커밋은 게이트가 작동하고 있다는 증거입니다.

일상적인 워크플로의 경우 현재 참조 문서는 백엔드 개발, 백엔드 인프라 및 프런트엔드 상태 관리입니다.