package.json 하나로 두고, "API가 바뀌면 버전도 반드시 올라간다"를 CI 가드(version-guard, oasdiff breaking check)로 강제했습니다.외부 고객사가 사용할 파트너 API를 NestJS로 만들고 있습니다. 아직 정식 오픈 전이지만, 오픈 시점에는 고객이 바로 붙을 수 있어야 합니다. 그런데 고객의 스택이 하나가 아니었습니다. TypeScript로 붙는 곳도 있고, Java(Spring)로 붙는 곳도 있습니다.
문서만 던져주고 "직접 호출하세요"라고 할 수도 있지만, 그러면 인증 토큰 갱신 로직을 고객마다 제각각 구현하고, 응답 타입을 손으로 옮겨 적다가 어긋나는 문제가 필연적으로 생깁니다. 반대로 SDK를 손으로 짜서 유지하면 API가 바뀔 때마다 두 언어의 SDK를 사람이 따라 고쳐야 하고, 언젠가는 반드시 스펙과 어긋납니다.
다행히 NestJS는 @nestjs/swagger로 OpenAPI JSON을 코드에서 바로 뽑아낼 수 있습니다. 그렇다면 결론은 하나였습니다. OpenAPI JSON을 단일 계약(contract)으로 삼고, SDK는 전부 기계가 만들게 하자.
파이프라인보다 먼저 부딪힌 질문입니다. 우리 API는 OAuth2 client credentials(clientId/secret)로 인증하는, 아무나 쓸 수 없는 API입니다. 그런 API의 SDK를 npmjs.com과 Maven Central 같은 공개 레지스트리에 올려도 될까요?
결론부터 말하면 됩니다. 그리고 그게 표준입니다. Stripe, AWS, OpenAI 모두 SDK는 public으로 배포합니다. SDK는 결국 "호출 코드"일 뿐이고, 접근 통제는 API 레벨의 인증이 담당하기 때문입니다.
private registry(GitHub Packages, Nexus 등)도 검토했지만, 고객 입장의 설치 경험을 따져보고 접었습니다. public이면 고객은 이 한 줄이면 끝입니다.
npm install @cmarket/partner-sdk
반면 private이면 고객 프로젝트마다 설치용 인증 설정이 선행되어야 합니다.
# .npmrc — 고객마다 이 설정을 안내해야 합니다 @cmarket:registry=https://npm.pkg.github.com //npm.pkg.github.com/:_authToken=발급해준_토큰
Maven 쪽은 settings.xml의 <server> 자격증명까지 필요합니다. 즉 private로 가면 이미 발급하는 API용 clientId/secret에 더해 설치용 토큰까지 고객마다 발급·전달·회전하는 운영 업무가 이중으로 생깁니다. 외부 고객용이라면 얻는 것에 비해 잃는 게 큽니다.
단, 한 가지는 주의해야 합니다. SDK 코드를 열어보면 API의 전체 표면이 그대로 드러납니다. OpenAPI JSON에 내부용 엔드포인트나 미공개 기능이 섞여 있으면 스펙 자체가 정보 노출이 됩니다. 공개할 스펙만 노출되도록 export 단계에서 관리하는 것이 전제 조건입니다.
NestJS 코드 (@nestjs/swagger 데코레이터) │ pnpm openapi:export ▼ openapi.json ←— 레포에 커밋되는 단일 계약 │ openapi-generator ├──▶ TypeScript SDK (typescript-axios) ──▶ npm (@cmarket/partner-sdk) └──▶ Java SDK (java, library=native) ──▶ Maven Central (net.c-market:partner-sdk)
openapi.json은 빌드 산출물이 아니라 레포에 커밋되는 파일입니다. PR에서 코드가 바뀌면 export를 다시 돌려 드리프트가 0인지 CI가 검사합니다. 스펙이 리뷰 대상 diff에 그대로 드러나기 때문에, "이 PR이 외부 계약을 건드리는가"를 리뷰어가 눈으로 확인할 수 있습니다.
generator 선택은 다음과 같이 했습니다.
typescript-axios. axios 기반이라 인터셉터로 토큰 갱신을 끼워 넣기 쉽고, 생태계에서 검증이 가장 많이 된 옵션입니다.java + library=native. JDK 11+의 표준 java.net.http.HttpClient를 씁니다. OkHttp 같은 서드파티 HTTP 클라이언트 의존성을 고객의 클래스패스에 강요하지 않는다는 점이 결정적이었습니다. SDK는 남의 프로젝트에 들어가는 코드라서, 의존성이 가벼울수록 충돌이 줄어듭니다.SDK 자동 배포에서 제일 어려운 건 생성이 아니라 버전입니다. 두 레지스트리에 나가는 두 SDK의 버전을 어디서 관리할지, 그리고 API가 바뀌었는데 버전을 안 올리고 넘어가는 사고를 어떻게 막을지가 문제입니다.
저희는 버전의 단일 출처(SSOT)를 API 서버의 package.json version 하나로 정했습니다. npm SDK도, Maven SDK도 이 값을 따라갑니다. 버전은 릴리스 시점에 기계가 올리는 게 아니라 PR에서 사람이 직접 올리고, 한 커밋에 동반 파일(CHANGELOG, 프론트엔드에 노출하는 SDK 버전 상수, openapi.json)이 함께 갑니다.
사람이 올리면 잊어버리지 않을까요? 그래서 CI에 required check로 가드 두 개를 심었습니다.
openapi.json이 바뀌었는데 version bump가 없으면 CI 실패. "계약이 바뀌면 버전이 오른다"를 관례가 아니라 기계 규칙으로 만듭니다.semver의 의미는 관례로 정했습니다. breaking = major(애초에 가드에 막히므로 리드 승인 + ignore 등록이 선행), 필드/엔드포인트 추가 = minor, 계약과 무관한 수정 = patch.
태그를 손으로 만들지 않습니다. main에서 package.json의 version 변경이 감지되면 워크플로가 partner-sdk-v<version> 태그를 자동 생성하고 publish 워크플로를 dispatch합니다. 즉 version bump PR이 머지되는 순간이 릴리스입니다.
npm 게시는 trusted publishing(OIDC)을 씁니다. 2025년 7월 GA된 방식으로, 장수 토큰(NPM_TOKEN) 없이 GitHub Actions가 OIDC로 npm에 직접 인증하고 provenance 증명도 자동으로 붙습니다. 토큰 유출 걱정과 회전 업무가 통째로 사라집니다.
Maven Central은 2025년 6월 30일 OSSRH가 종료되어 이제 Central Portal이 유일한 경로입니다. 요구사항이 npm보다 많습니다 — 네임스페이스 소유권 검증, GPG 서명, sources/javadoc jar 동봉이 필수입니다.
저희는 크게 막힌 데 없이 지나갔지만, 이 파이프라인을 만들 때 보통 걸리는 지점을 정리해 둡니다.
Maven Central 쪽
net.c-market 같은 커스텀 도메인 groupId는 해당 도메인의 DNS TXT 레코드로 소유권을 증명해야 합니다. 도메인이 없다면 io.github.<계정> 네임스페이스로 GitHub 계정 검증만으로 시작할 수 있습니다.npm 쪽
openapi-generator 쪽
AppController_getUser 같은 이름이 그대로 노출됩니다. 스펙 export 단계에서 다듬어야 합니다.OpenAPI JSON을 계약의 단일 출처로 삼으면, SDK는 "만드는 것"이 아니라 "따라 나오는 것"이 됩니다. 저희 파이프라인에서 사람이 하는 일은 두 가지뿐입니다 — PR에서 버전을 올리는 것, 그리고 breaking change가 정말 의도된 것인지 판단하는 것. 나머지(생성, 검증, 태깅, 두 레지스트리 게시)는 전부 기계가 합니다.
아직 정식 오픈 전이라 "고객이 실제로 쓰면서 드러나는 문제"까지는 담지 못했습니다. 그 부분은 운영을 시작한 뒤 후속 글로 다루겠습니다.