Node.js API 버전 관리 전략 설계
Lukas Schneider
DevOps Engineer · Leapcell

효과적인 버전 관리를 통한 API 진화 관리
Node.js 애플리케이션이 성장하고 발전함에 따라 API 또한 마찬가지입니다. 새로운 기능이 추가되고, 기존 기능은 사용 중단되며, 때로는 향후 개발을 지원하거나 심각한 문제를 해결하기 위해 근본적인 변경이 필요합니다. 이러한 변경 사항을 관리하기 위한 강력한 전략 없이는 기존 클라이언트 애플리케이션을 중단시킬 위험을 감수해야 하며, 이는 개발자 경험 저하와 사용자 불만으로 이어질 수 있습니다. 바로 이때 API 버전 관리가 중요해집니다. API 버전 관리를 통해 이전 클라이언트를 계속 지원하면서도 호환성을 깨는 변경 사항을 도입할 수 있어, 보다 원활한 전환 경로를 제공하고 API의 장기적인 안정성과 사용성을 보장합니다. 이러한 유연성, 유지보수성 및 사용 편의성 간의 균형을 맞추는 올바른 버전 관리 전략을 선택하는 것이 과제입니다. 이 글에서는 Node.js API 버전 관리를 위한 두 가지 주요 접근 방식인 URL 기반 및 헤더 기반 버전을 살펴보고, 그 미묘한 차이를 탐구하며 실질적인 코드 예제를 통해 구현 방법을 보여드리겠습니다.
핵심 개념 이해
특정 버전 관리 전략에 대해 자세히 알아보기 전에 API 설계 및 진화의 기반이 되는 몇 가지 기본 개념을 명확히 해보겠습니다.
- API 버전 관리: 잠재적으로 기존 클라이언트 애플리케이션을 중단시킬 수 있는 웹 API 변경을 적용하면서도, 수정되지 않은 이전 클라이언트에 대한 하위 호환성을 제공하는 관행입니다.
- 호환성 없는 변경 (Breaking Change): 클라이언트가 올바르게 작동하기 위해 코드 업데이트를 요구하는 API 수정입니다. 여기에는 엔드포인트 경로, 요청 매개변수, 응답 구조 또는 인증 메커니즘 변경이 포함될 수 있습니다.
- 하위 호환성 (Backward Compatibility): API의 새 버전이 수정 없이 이전 클라이언트 애플리케이션을 지원하는 능력입니다. 이는 일반적으로 새 버전과 함께 오래된 버전의 API를 유지함으로써 달성됩니다.
- 사용 중단 (Deprecation): API 기능 또는 버전을 더 이상 사용하지 않도록 권장하지 않음을 표시하여, 개발자에게 해당 기능이 결국 제거될 것임을 알리는 프로세스입니다.
이러한 개념은 API의 원활한 진화를 보장하는 데 핵심적이며, 선택된 버전 관리 전략은 이러한 개념을 관리하는 방식에 직접적인 영향을 미칩니다.
URL 기반 버전 관리: 단순성과 가시성
종종 경로 버전 관리라고도 불리는 URL 기반 버전 관리는 API 버전을 URI 경로에 직접 통합합니다. 명확성과 검색 용이성 덕분에 아마도 가장 간단하고 널리 이해되는 접근 방식일 것입니다.
원칙: 버전 번호는 /api/v1/users
또는 /api/v2/products
와 같이 엔드포인트 URL의 일부로 명시됩니다.
구현: Express.js와 같은 프레임워크를 사용하는 Node.js에서 URL 기반 버전을 구현하는 것은 매우 간단합니다. 각 버전에 대해 별도의 라우트 핸들러를 정의할 수 있습니다.
// app.js const express = require('express'); const app = express(); const port = 3000; // API 버전 1 app.get('/api/v1/users', (req, res) => { res.json({ version: '1.0', users: [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }] }); }); // API 버전 2 // v2에는 사용자 이메일이 포함된다고 가정해 봅시다. app.get('/api/v2/users', (req, res) => { res.json({ version: '2.0', users: [{ id: 1, name: 'Alice', email: 'alice@example.com' }, { id: 2, name: 'Bob', email: 'bob@example.com' }] }); }); app.listen(port, () => { console.log(`API running on http://localhost:${port}`); });
장점:
- 가시성: 버전이 URL에 즉시 표시되어 개발자가 어떤 버전을 사용하고 있는지 쉽게 이해할 수 있습니다.
- 캐싱: 프록시 및 캐시는 다른 버전을 완전히 별개의 리소스로 취급할 수 있어 캐싱 전략을 단순화합니다.
- 사용 편의성: 클라이언트가 사용하기 쉽습니다. URL만 변경하면 됩니다.
단점:
- URI 오염: 버전 번호가 리소스 식별자의 일부가 됩니다. 일부에서는 리소스 자체의 표현 방식이 변경된 것이 아니라 표현 방식만 변경된 것인데도 RESTful 원칙을 위반한다고 주장합니다.
- 라우팅 오버헤드: API가 성장함에 따라 각 버전에 대한 별도의 경로를 유지 관리하면 라우팅 구성이 더 장황해질 수 있습니다.
- URL 변경: 버전 변경은 URL 변경을 의미하며, 특정 링크 전략에는 이상적이지 않을 수 있습니다.
적용 시나리오: URL 기반 버전 관리는 단순성과 빠른 채택이 가장 중요한 API 또는 버전 간 상당하고 독립적인 변경을 겪을 것으로 예상되는 API에 적합합니다. 검색 용이성이 핵심인 공개 API에 자주 선호됩니다.
헤더 기반 버전 관리: 깔끔한 URL 및 콘텐츠 협상
헤더 기반 버전 관리는 HTTP 헤더를 활용하여 원하는 API 버전을 지정합니다. 이 접근 방식은 클라이언트가 리소스의 특정 표현을 명시적으로 요청하는 콘텐츠 협상 개념과 잘 맞습니다.
원칙: 버전 정보는 사용자 지정 HTTP 헤더(예: X-API-Version: 2
) 또는 Accept
헤더(예: Accept: application/vnd.myapi.v2+json
)를 통해 전달됩니다.
구현: Node.js에서는 일반적으로 들어오는 요청 헤더를 검사하여 원하는 버전을 결정합니다.
// app.js const express = require('express'); const app = express(); const port = 3000; app.get('/api/users', (req, res) => { const apiVersion = req.headers['x-api-version']; if (apiVersion === '2') { res.json({ version: '2.0', users: [{ id: 1, name: 'Alice', email: 'alice@example.com' }, { id: 2, name: 'Bob', email: 'bob@example.com' }] }); } else { // 버전 1 또는 특정 버전 1 요청으로 기본 설정 res.json({ version: '1.0', users: [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }] }); } }); app.listen(port, () => { console.log(`API running on http://localhost:${port}`); });
Accept
헤더를 사용한 보다 정교한 콘텐츠 협상을 위해:
// app.js const express = require('express'); const mediaTypes = require('express-negotiate'); // 콘텐츠 협상을 돕는 잠재적인 라이브러리 const app = express(); const port = 3000; // 단순화를 위해 데모용으로 Accept 헤더를 수동으로 구문 분석합니다. app.get('/api/products', (req, res) => { const acceptHeader = req.headers['accept']; let version = 'v1'; // 기본 버전 if (acceptHeader && acceptHeader.includes('application/vnd.myapi.v2+json')) { version = 'v2'; } else if (acceptHeader && acceptHeader.includes('application/vnd.myapi.v1+json')) { version = 'v1'; } if (version === 'v2') { res.json({ version: '2.0', products: [{ id: 101, name: 'Laptop Pro', price: 1200.00 }] }); } else { res.json({ version: '1.0', products: [{ id: 101, name: 'Laptop' }] }); } }); app.listen(port, () => { console.log(`API running on http://localhost:${port}`); });
장점:
- 깔끔한 URL: URI는 다른 API 버전 간에 안정적으로 유지되며, 표현 방식에 따라 리소스 식별자가 변경되지 않는 RESTful 원칙을 더 가깝게 준수합니다.
- 콘텐츠 협상: HTTP의 콘텐츠 협상 메커니즘과 잘 맞으며, 클라이언트가 리소스의 특정 표현을 요청할 수 있도록 합니다.
- 유연성: 기본 URL이 일관되게 유지되므로 사용 중단된 버전을 최신 버전과 함께 관리하기가 더 쉽습니다.
단점:
- 낮은 가시성: 버전은 HTTP 헤더에 숨겨져 있어 클라이언트(
curl
명령, 문서)가 명시적으로 지정해야 합니다. 이로 인해 더 많은 상용구 코드가 필요할 수 있습니다. - 브라우저 제한: 네트워크 검사 도구나 브라우저 확장 프로그램 없이는 브라우저 주소 표시줄에서 헤더 기반 버전을 직접 테스트하는 것이 불가능합니다.
- 캐싱 복잡성: URL이 같으면 캐싱 프록시가 버전을 자동으로 구분하지 못할 수 있어, 더 정교한 캐싱 로직(예:
Vary
헤더)이 필요합니다.
적용 시나리오: 헤더 기반 버전 관리는 깔끔한 URL을 매우 중요하게 생각하고 헤더 설정을 감당할 수 있는 프로그래밍 방식 클라이언트 또는 내부 API에 자주 선호됩니다. 특히 서로 다른 버전이 동일한 리소스의 진정으로 다른 _표현_을 나타내는 경우 강력합니다.
올바른 전략 선택
URL 기반 및 헤더 기반 버전 관리 전략 모두 장점과 단점이 있습니다.