OpenAPI와 Orval, AI로 도입하기 - 신규 프로젝트부터 n년 된 레거시까지
앞선 글에서 OpenAPI 스펙 하나로 문서(Swagger UI)와 프론트 클라이언트(Orval)를 잇는 구조를 정리했습니다. 이번엔 한 걸음 물러서서 “왜 이걸 쓰면 좋은가”, 그리고 “AI(Kiro/Claude)를 써서 어떻게 도입하는가”를 정리했습니다. 특히 백지 상태의 신규 프로젝트와, 이미 몇 년째 굴러가는 레거시에서는 도입 전략이 완전히 다릅니다.
TL;DR
- OpenAPI = API의 계약서, Orval = 그 계약서로 프론트 코드를 찍어내는 기계입니다.
- 좋은 점은 결국 하나로 수렴합니다. 타입과 엔드포인트를 두 번 손으로 적지 않는다.
- AI로 도입하면 특히 빠릅니다. 설정 파일, mutator, 스크립트를 대화로 만들 수 있어요.
- 신규 프로젝트: 처음부터 “스펙 우선”으로 시작하면 거의 공짜로 얻습니다.
- n년차 레거시: 한 번에 갈아엎지 말고, 한 도메인부터 좁게 시작해 점진 확산하세요.
- 함정은 대부분
baseURL중복, 인증 인터셉터 누락, 스펙 품질(태그/스키마) 세 곳에서 납니다.
목차
- 배경: 왜 이 글을 쓰게 됐나
- OpenAPI와 Orval, 30초 요약
- 도입하면 좋은 점
- AI(Kiro/Claude)로 도입하기: 프롬프트 초안
- 신규 프로젝트에 도입하기
- 운영 중인 n년차 시스템에 도입하기
- 도입 전 체크리스트와 함정
- 마치며
이 글의 API 이름, 컨트롤러/타입명은 전부 일반화한 예시입니다.
1. 배경: 왜 이 글을 쓰게 됐나
앞선 글에서는 springdoc으로 스펙을 뽑고 Orval로 프론트 코드를 생성하는 “어떻게(How)” 를 다뤘습니다. 그런데 실제로 팀에 이걸 권하다 보면 질문이 늘 두 갈래로 갈립니다.
- “그래서 이걸 왜 써야 하는데? 지금도 fetch 잘 짜고 있는데.”
- “새 프로젝트면 몰라도, 이미 몇 년째 돌아가는 시스템에 이걸 어떻게 넣어?”
이 글은 그 두 질문에 답하는 글입니다. 그리고 요즘은 도입 자체도 AI(Kiro/Claude 같은 코딩 에이전트)에게 시켜서 하는 게 훨씬 빠르기 때문에, 실제로 쓸 만한 프롬프트 초안도 같이 정리했습니다.
2. OpenAPI와 Orval, 30초 요약
- OpenAPI: REST API의 엔드포인트, 요청/응답 형태, 인증 방식을 기계가 읽을 수 있는 형식(JSON/YAML)으로 적어둔 API 계약서(스펙) 입니다. 예전 이름은 Swagger. 백엔드가 springdoc 같은 도구를 쓰면 코드에서 이 스펙이 자동 생성됩니다.
- Orval: 그 OpenAPI 스펙을 입력으로 받아 TypeScript 타입 + API 호출 코드 (react-query 훅 등)를 자동 생성해주는 코드 제너레이터입니다.
한 문장으로 요약하면 이렇습니다.
OpenAPI가 “계약서”라면, Orval은 그 계약서를 읽고 프론트엔드용 코드를 찍어내는 자동 공장입니다.
그래서 백엔드에서 API가 바뀌면 → 스펙이 바뀌고 → Orval을 다시 돌리면 → 프론트 타입이 갱신되며, 안 맞는 부분은 컴파일 에러로 즉시 드러납니다.
3. 도입하면 좋은 점
3-1. 타입/엔드포인트를 두 번 적지 않는다
가장 큰 이점입니다. 수기 방식에서는 백엔드 DTO를 프론트에서 interface로 다시 정의하고, URL도 문자열로 다시 적습니다. 이게 드리프트(drift) 의 근원이죠. 백엔드가 필드 하나 바꿔도 프론트는 모른 채 런타임에 터집니다. 스펙 기반이면 이 중복이 사라집니다.
3-2. API 변경이 “컴파일 타임”에 드러난다
백엔드가 orderDate를 createdAt으로 바꿨다고 합시다. 수기 방식이면 QA나 운영에서야 발견되지만, 스펙 기반이면 Orval 재생성 직후 타입 에러로 빨간 줄이 뜹니다. 사고가 왼쪽(개발 단계)으로 당겨집니다(shift-left).
3-3. 프론트-백엔드 병렬 개발이 쉬워진다
스펙만 먼저 합의하면, 백엔드 구현이 끝나기 전에도 프론트는 생성된 타입/훅으로 작업을 시작할 수 있습니다. 계약이 먼저고 구현이 나중입니다.
3-4. 문서가 코드와 항상 붙어 다닌다
Swagger UI가 스펙에서 자동 생성되므로, “문서가 최신이 아님” 문제가 구조적으로 줄어듭니다. 프론트 코드와 문서가 같은 원본(스펙) 을 바라보기 때문입니다.
3-5. 반복 코드(보일러플레이트)가 사라진다
로딩/에러/캐싱/재요청 같은 패턴을 react-query 훅으로 일관되게 생성하니, 사람이 매번 같은 코드를 짜지 않아도 됩니다.
정리하면 이점은 결국 하나입니다. “손으로 두 번 적던 것을 한 번의 계약으로 통합한다.” 나머지 장점은 여기서 파생됩니다.
4. AI(Kiro/Claude)로 도입하기: 프롬프트 초안
설정 파일 문법, mutator 작성, npm 스크립트 등록 같은 건 손으로 하기 번거롭습니다. Kiro나 Claude 같은 코딩 에이전트에게 맥락을 주고 시키면 훨씬 빠릅니다. 아래는 실제로 쓸 만한 프롬프트 초안입니다. 상황에 맞게 다듬어 쓰시면 됩니다.
프롬프트에 접속 정보나 토큰은 넣지 마세요. “환경변수로 받게 해줘” 정도로만 지시하고, 실제 값은 로컬 환경에 두세요.
4-1. 백엔드 스펙 노출시키기
1
2
3
4
5
6
Spring Boot 3 / Java 21 프로젝트야. springdoc-openapi를 붙여서
/v3/api-docs(OpenAPI JSON)와 Swagger UI가 뜨게 해줘.
- JWT Bearer 인증 스킴을 등록해서 Swagger UI에 Authorize 버튼이 나오게 해줘.
- Spring Security를 쓰고 있으니 swagger-ui와 api-docs 경로는 permitAll로 열어줘.
- 컨트롤러에는 태그와 요약을 붙여서 문서 가독성을 높여줘.
설정을 바꾼 파일과 이유를 같이 설명해줘.
4-2. Orval 설정 생성
1
2
3
4
5
6
7
프론트는 React + Vite + TypeScript야. Orval을 도입해줘.
- 입력은 http://localhost:8080/v3/api-docs
- react-query + axios로 훅을 생성하고, mode는 tags-split로 컨트롤러 단위 분리
- 생성물은 src/api/generated 아래, 스키마는 model 폴더로
- 기존 axios 인스턴스(인증 인터셉터 포함)를 재사용하도록 custom mutator를 연결해줘
- package.json에 gen:api 스크립트도 추가해줘
orval.config.ts와 mutator 파일을 만들고, 왜 이렇게 구성했는지 설명해줘.
4-3. 기존 수기 API 코드를 생성 코드로 이관
1
2
3
4
5
6
지금 src/api 아래에 손으로 짠 axios 호출 함수랑 타입이 흩어져 있어.
Orval로 생성한 훅/타입으로 점진적으로 바꾸고 싶어.
- 먼저 order 도메인만 골라서, 기존 수기 호출을 생성된 훅으로 교체해줘.
- 교체하면서 중복되던 수기 타입은 제거하고 생성 타입을 쓰게 해줘.
- 동작이 바뀌지 않도록, 교체 전후로 타입 에러가 없는지 확인해줘.
한 번에 다 바꾸지 말고 order 도메인만, 변경 파일 목록을 정리해서 보여줘.
4-4. 트러블슈팅을 그대로 떠넘기기
AI 도입의 진짜 편한 점은, 막혔을 때 에러 메시지만 그대로 던져도 된다는 것입니다.
1
2
Orval로 생성한 훅으로 호출하니 /api/api/orders 로 404가 나.
baseURL이랑 스펙 path 양쪽에 /api가 있는 것 같은데 어떻게 정리하면 돼?
1
2
생성된 훅의 반환 타입이 DTO가 아니라 AxiosResponse로 나와.
mutator에서 뭘 고치면 res.data(본문)만 반환되게 할 수 있어?
프롬프트는 정제된 문장일 필요가 없습니다. 맥락(스택/증상)과 목표만 분명하면 됩니다. 안 되면 에러 메시지를 그대로 붙여넣는 게 가장 빠릅니다.
5. 신규 프로젝트에 도입하기
백지에서 시작한다면 이건 거의 공짜로 얻는 이점입니다. 처음부터 “스펙이 곧 계약”이라는 규칙만 세우면 됩니다. 순서는 이렇습니다.
- 백엔드에 springdoc부터 넣는다. 컨트롤러/DTO를 만들면 스펙이 자동으로 생깁니다.
- 프론트 초기 세팅에 Orval을 포함한다.
gen:api스크립트를 처음부터 둡니다. - 팀 규칙을 하나 정한다. “프론트 API 타입/호출은 손으로 적지 않는다. 전부 생성물을 쓴다.” 이 규칙 하나가 드리프트를 원천 차단합니다.
- CI에 재생성 검증을 건다. 스펙과 생성물이 어긋나면 빌드가 깨지게 합니다.
신규 프로젝트에서 특히 신경 쓸 것
- 태그와 스키마 이름을 처음부터 깔끔하게. 컨트롤러 태그가 곧 프론트 폴더 구조가 되고, DTO 이름이 곧 타입 이름이 됩니다. 초반 네이밍이 두고두고 갑니다.
- 인증/에러 응답 형태를 먼저 표준화. 공통 응답 래퍼(성공/에러 구조)를 일찍 정하면 mutator에서 언랩 로직을 한 번만 짜면 됩니다.
신규 프로젝트의 핵심은 “나중에 붙이자”를 하지 않는 것입니다. 초기 스캐폴딩에 넣으면 비용이 거의 0이지만, 코드가 쌓인 뒤엔 6장의 레거시 전략이 필요해집니다.
6. 운영 중인 n년차 시스템에 도입하기
여기가 진짜 어려운 지점입니다. 이미 수백 개의 API와 수기 호출 코드가 쌓여 있고, 멈출 수 없는 서비스가 돌고 있습니다. 한 번에 갈아엎는 건 거의 항상 실패합니다. 핵심 전략은 “빅뱅 금지, 점진 도입”입니다.
6-1. 1단계: 현행 스펙부터 확보 (읽기 전용)
기존 코드는 건드리지 않고, 스펙을 뽑아내는 것부터 시작합니다.
- 백엔드가 Spring이면 springdoc을 추가해
/v3/api-docs만 노출합니다. 기존 동작에는 영향이 없습니다(문서 경로만 열림). - 스프링이 아니거나 스펙 생성이 어려우면, 우선 주요 API 몇 개만 손으로 스펙화 하거나 AI에게 컨트롤러를 읽혀 스펙 초안을 만들게 합니다.
이 단계의 목표는 “현재 API가 실제로 어떻게 생겼는지”를 기계가 읽는 형식으로 확보하는 것입니다.
6-2. 2단계: 생성 코드를 “격리된 폴더”에 공존시키기
기존 수기 API 코드와 Orval 생성 코드를 당분간 공존시킵니다. 생성물은 src/api/generated처럼 격리된 폴더에 두고, 기존 코드는 그대로 둡니다. 둘 다 같은 axios 인스턴스(인증 인터셉터)를 공유하게 mutator를 맞추면, 어느 쪽으로 호출해도 인증/refresh가 동일하게 동작합니다.
6-3. 3단계: 한 도메인씩 교체 (스트랭글러 패턴)
새 기능이나 손대는 화면부터 생성 코드로 바꿉니다. 이른바 스트랭글러 패턴입니다.
- 새로 만드는 화면 → 무조건 생성된 훅/타입 사용
- 버그 수정/리팩터링으로 건드리는 기존 화면 → 그 김에 생성 코드로 교체
- 멀쩡히 도는 화면 → 굳이 서두르지 않음
이렇게 하면 위험을 분산하면서 커버리지가 자연스럽게 올라갑니다. “order 도메인만 먼저” 같은 식으로 범위를 좁혀 AI에게 맡기기 좋습니다(4-3 프롬프트).
6-4. 4단계: 스펙 품질 개선을 병행
레거시 스펙은 보통 지저분합니다. 태그가 없거나, 응답 타입이 object로 뭉뚱그려져 있거나, 같은 DTO가 여러 이름으로 흩어져 있죠. 교체하는 도메인 순서대로 컨트롤러에 태그/설명/명확한 응답 타입을 붙여 스펙 품질을 함께 끌어올립니다. 스펙이 좋아질수록 생성 코드 품질도 같이 올라갑니다.
6-5. 레거시에서 특히 조심할 것
- 응답 래퍼 불일치. 오래된 API는 응답 형태가 제각각인 경우가 많습니다 (어떤 건
{data, code}, 어떤 건 본문 그대로). mutator의 언랩 로직이 전부를 커버하지 못하면 타입이 어긋납니다. 도메인별로 확인이 필요합니다. baseURL의/api중복. 스펙 path에 이미/api가 들어 있는데 기존 클라이언트도baseURL에/api를 붙여 쓰던 경우, 생성 코드에서/api/api/...중복이 납니다. 생성 인스턴스에는 오리진만 넘기세요.- 인증/refresh 인터셉터 누락. 생성 코드가 자체 axios를 쓰면 JWT 주입과 401 자동 재발급이 빠집니다. 반드시 기존 인스턴스를 재사용하는 mutator로 위임하세요.
- 한 번에 다 바꾸려는 유혹. 가장 큰 실패 원인입니다. 도메인 단위로 쪼개세요.
위 세 가지(래퍼/baseURL/인터셉터)는 앞선 기술 글에서 실제로 밟았던 함정과 정확히 같습니다. 레거시일수록 더 자주, 더 다양하게 나타납니다.
7. 도입 전 체크리스트와 함정
| 구분 | 확인할 것 | 왜 |
|---|---|---|
| 스펙 생성 | 백엔드가 OpenAPI를 자동 생성할 수 있나 | 스펙이 없으면 시작 자체가 안 됨 |
| 인증 | 기존 인증 인터셉터를 재사용할 mutator가 있나 | JWT/refresh 누락 방지 |
| baseURL | 스펙 path와 baseURL의 /api 중복 여부 | /api/api/... 404 방지 |
| 응답 형태 | 공통 응답 래퍼가 표준화돼 있나 | 언랩 로직 일관성 |
| 태그/스키마 | 컨트롤러 태그와 DTO 이름이 깔끔한가 | 생성물 폴더/타입 품질 |
| CI | 재생성-검증을 자동화했나 | 스펙-코드 드리프트 차단 |
| 범위 | (레거시) 도메인 단위로 쪼갰나 | 빅뱅 실패 방지 |
정리하면, 신규는 “처음부터 규칙으로”, 레거시는 “좁게 시작해 점진 확산” 입니다. 그리고 어느 쪽이든 설정과 이관 작업은 AI에게 맥락을 주고 맡기면 훨씬 수월합니다.
8. 마치며
OpenAPI + Orval의 가치는 화려한 기능이 아니라, “같은 걸 두 번 적지 않는다” 는 단순한 원칙에 있습니다. 그 원칙이 문서 최신화, 타입 안전성, 병렬 개발, 보일러플레이트 제거 같은 이점으로 파생될 뿐이죠.
신규 프로젝트라면 지금 초기 스캐폴딩에 넣으세요. 비용이 거의 0입니다. 이미 몇 년째 돌아가는 시스템이라면, 오늘 당장 스펙부터 뽑아보고 한 도메인만 생성 코드로 바꿔보세요. 그 한 도메인의 경험이 나머지 확산의 근거가 됩니다.
그리고 이 모든 과정은 이제 혼자 씨름하지 않아도 됩니다. 설정이든 이관이든 막힌 에러든, 맥락과 목표를 분명히 해서 AI에게 맡기면 됩니다.
(이 글은 개인 도입 경험과 관점을 정리한 것으로, 사용한 도구의 구성 방식이나 API는 버전에 따라 바뀔 수 있습니다. 실제 적용 시 각 도구의 공식 문서를 확인하세요.)