댓글 검색 목록

[기타] REST API 설계를 위한 모범 사례

페이지 정보

작성자 운영자 작성일 20-12-17 19:36 조회 692 댓글 0

REST API는 오늘날 사용 가능한 가장 일반적인 웹 서비스 유형 중 하나입니다. 브라우저 앱을 포함한 다양한 클라이언트가 REST API를 통해 서버와 통신 할 수 있습니다.


따라서 앞으로 문제가 발생하지 않도록 REST API를 올바르게 설계하는 것이 매우 중요합니다. API 소비자를 위한 보안, 성능 및 사용 용이성을 고려해야 합니다.


그렇지 않으면 API를 사용하는 고객에게 문제가 발생하여 사용자가 API를 사용하는 데 불편함을 느끼게 됩니다. 일반적으로 허용되는 규칙을 따르지 않으면 모든 사람이 기대하는 것과 다르기 때문에 API의 유지 관리자와 이를 사용하는 클라이언트를 혼동합니다.


이 기사에서는 REST API를 사용하는 모든 사람이 이해하기 쉽고, 미래에 대비하며, 기밀 일 수 있는 데이터를 클라이언트에 제공하므로 안전하고 빠르게 REST API를 설계하는 방법을 살펴 보겠습니다.


네트워크로 연결된 애플리케이션이 중단 될 수 있는 여러 방법이 있기 때문에 모든 REST API가 소비자가 문제를 처리하는 데 도움이 되는 표준 HTTP 코드를 사용하여 오류를 정상적으로 처리하도록 해야 합니다.


JSON으로 수락 및 응답 


REST API는 요청 페이로드에 대해 JSON을 수락하고 JSON으로 응답을 보내야 합니다. JSON은 데이터 전송을 위한 표준입니다. 거의 모든 네트워크 기술이 이를 사용할 수 있습니다. JavaScript에는 Fetch API 또는 다른 HTTP 클라이언트를 통해 JSON을 인코딩 및 디코딩 하는 내장 메서드가 있습니다. 서버 측 기술에는 많은 작업을 수행하지 않고도 JSON을 디코딩 할 수 있는 라이브러리가 있습니다.


데이터를 전송하는 다른 방법이 있습니다. XML은 데이터를 사용할 수 있는 것으로 변환하지 않고 프레임 워크에서 널리 지원되지 않으며 일반적으로 JSON입니다. 특히 브라우저에서 클라이언트 측에서 이 데이터를 쉽게 조작 할 수 없습니다. 정상적인 데이터 전송을 위해 많은 추가 작업이 필요합니다.


양식 데이터는 특히 파일을 보내려는 경우 데이터를 보내는 데 유용합니다. 그러나 텍스트와 숫자의 경우 양식 데이터를 전송할 필요가 없습니다. 대부분의 프레임 워크에서 클라이언트 측에서 직접 데이터를 가져옴으로써 JSON을 전송할 수 있기 때문입니다. 그렇게 하는 것이 가장 간단합니다.


REST API 앱이 JSON으로 응답 할 때 클라이언트가 이를 그대로 해석하도록 하려면 요청이 이루어진 후 응답 헤더의 Content-Type을 application / json으로 설정해야 합니다. 많은 서버 측 앱 프레임 워크는 응답 헤더를 자동으로 설정합니다. 일부 HTTP 클라이언트는 Content-Type 응답 헤더를 보고 해당 형식에 따라 데이터를 구문 분석합니다.


유일한 예외는 클라이언트와 서버 간에 파일을 보내고 받는 경우입니다. 그런 다음 파일 응답을 처리하고 클라이언트에서 서버로 양식 데이터를 보내야 합니다. 그러나 그것은 또 다른 주제입니다.


또한 엔드 포인트가 응답으로 JSON을 반환하는지 확인해야 합니다. 많은 서버 측 프레임 워크에는 이 기능이 기본 제공됩니다.


JSON 페이로드를 허용하는 예제 API를 살펴 보겠습니다. 이 예제에서는 Node.js 용 Express 백엔드 프레임 워크를 사용합니다. body-parser 미들웨어를 사용하여 JSON 요청 본문을 구문 분석 한 후 다음과 같이 JSON 응답으로 반환하려는 객체와 함께 res.json 메서드를 호출 할 수 있습니다.


const express = require('express');
const bodyParser = require('body-parser');

const app = express();

app.use(bodyParser.json());

app.post('/', (req, res) => {
  res.json(req.body);
});

app.listen(3000, () => console.log('server started'));

bodyParser.json()은 JSON 요청 본문 문자열을 JavaScript 객체로 파싱 한 다음 req.body 객체에 할당합니다.


application/json에 대한 응답에서 Content-Type 헤더를 설정하십시오. 변경하지 않고 charset = utf-8. 위의 방법은 대부분의 다른 백엔드 프레임 워크에 적용됩니다.


endpoint 경로에서 동사 대신 명사 사용 


끝점 경로에 동사를 사용해서는 안됩니다. 대신, 우리가 검색하거나 조작하는 엔드 포인트인 엔티티를 나타내는 명사를 경로 이름으로 사용해야 합니다.


이는 HTTP 요청 메서드에 이미 동사가 있기 때문입니다. API 엔드 포인트 경로에 동사가 있는 것은 유용하지 않으며 새로운 정보를 전달하지 않기 때문에 불필요하게 길어집니다. 선택한 동사는 개발자의 변덕에 따라 달라질 수 있습니다. 예를 들어, 일부는 'get'과 같은 일부는 'retrieve'와 같습니다. 따라서 HTTP GET 동사가 엔드 포인트가 수행하는 작업을 알려주는 것이 좋습니다.


작업은 우리가 만드는 HTTP 요청 방법으로 표시되어야 합니다. 가장 일반적인 방법은 GET, POST, PUT 및 DELETE입니다.


GET은 리소스를 검색합니다. POST는 새 데이터를 서버에 제출합니다. PUT는 기존 데이터를 업데이트합니다. DELETE는 데이터를 제거합니다. 동사는 CRUD 작업에 매핑됩니다.


위에서 논의한 두 가지 원칙을 염두에 두고 뉴스 기사를 얻기 위해 GET /articles/와 같은 경로를 만들어야 합니다. 마찬가지로 POST /articles/는 새 기사를 추가하기 위한 것이고, PUT /articles/:id는 주어진 ID로 기사를 업데이트 하기 위한 것입니다. DELETE /articles/:id는 주어진 ID로 기존 기사를 삭제하기 위한 것입니다.


/articles는 REST API 리소스를 나타냅니다. 예를 들어, Express를 사용하여 다음과 같이 기사를 조작하기 위해 다음 끝점을 추가 할 수 있습니다.


const express = require('express');
const bodyParser = require('body-parser');

const app = express();

app.use(bodyParser.json());

app.get('/articles', (req, res) => {
  const articles = [];
  // code to retrieve an article...
  res.json(articles);
});

app.post('/articles', (req, res) => {
  // code to add a new article...
  res.json(req.body);
});

app.put('/articles/:id', (req, res) => {
  const { id } = req.params;
  // code to update an article...
  res.json(req.body);
});

app.delete('/articles/:id', (req, res) => {
  const { id } = req.params;
  // code to delete an article...
  res.json({ deleted: id });
});

app.listen(3000, () => console.log('server started'));

위의 코드에서 우리는 기사를 조작하기 위한 끝점을 정의했습니다. 보시다시피 경로 이름에는 동사가 없습니다. 우리가 가진 것은 명사뿐입니다. 동사는 HTTP 동사에 있습니다.


POST, PUT 및 DELETE 엔드 포인트는 모두 JSON을 요청 본문으로 사용하고 모두 GET 엔드 ​​포인트를 포함하여 JSON을 응답으로 반환합니다.


복수 명사가 있는 이름 모음 


컬렉션의 이름은 복수 명사로 지정해야 합니다. 하나의 항목만 얻기를 원하는 경우가 많지 않으므로 이름 지정과 일치해야 하며 복수 명사를 사용해야 합니다.


우리는 데이터베이스에 있는 내용과 일치하도록 복수형을 사용합니다. 테이블은 일반적으로 하나 이상의 항목을 가지며 이를 반영하도록 이름이 지정되므로 테이블과 일관성을 유지하려면 API가 액세스하는 테이블과 동일한 언어를 사용해야 합니다.


/articles 끝점을 사용하면 모든 끝점에 대해 복수형이 있으므로 복수형으로 변경할 필요가 없습니다.


계층적 개체에 대한 중첩 리소스 


중첩 된 리소스를 처리하는 끝점의 경로는 상위 리소스 뒤에 오는 경로의 이름으로 중첩 된 리소스를 추가하여 수행해야 합니다.


중첩 된 리소스로 간주 한 것이 데이터베이스 테이블에 있는 것과 일치하는지 확인해야 합니다. 그렇지 않으면 혼란스러울 것입니다.


예를 들어 엔드 포인트에서 뉴스 기사에 대한 댓글을 가져 오려면 /articles 경로 끝에 /comments 경로를 추가해야 합니다. 이것은 우리 데이터베이스에 기사의 하위 항목으로 주석이 있다고 가정합니다.


예를 들어 Express에서 다음 코드를 사용하여 이를 수행 할 수 있습니다.


const express = require('express');
const bodyParser = require('body-parser');

const app = express();

app.use(bodyParser.json());

app.get('/articles/:articleId/comments', (req, res) => {
  const { articleId } = req.params;
  const comments = [];
  // code to get comments by articleId
  res.json(comments);
});


app.listen(3000, () => console.log('server started'));

위의 코드에서 '/articles /:articleId/comments'경로에 GET 메서드를 사용할 수 있습니다. articleId로 식별 된 기사에 대한 의견을 받은 다음 응답에 반환합니다. '/articles/:articleId'경로 세그먼트 뒤에 'comments'를 추가하여 /articles의 하위 리소스임을 나타냅니다.


이는 각 기사에 자체 주석이 있다고 가정 할 때 주석이 기사의 하위 개체이기 때문에 의미가 있습니다. 그렇지 않으면 이 구조가 일반적으로 하위 개체에 액세스하는 것으로 허용되기 때문에 사용자에게 혼란을 줄 수 있습니다. 동일한 원칙이 POST, PUT 및 DELETE 엔드 포인트에도 적용됩니다. 경로 이름에 대해 모두 동일한 종류의 중첩 구조를 사용할 수 있습니다.


오류를 정상적으로 처리하고 표준 오류 코드를 반환합니다. 


오류 발생시 API 사용자의 혼란을 없애기 위해 오류를 정상적으로 처리하고 발생한 오류의 종류를 나타내는 HTTP 응답 코드를 반환해야 합니다. 이를 통해 API 관리자는 발생한 문제를 이해하기에 충분한 정보를 얻을 수 있습니다. 오류로 인해 시스템이 중단되는 것을 원하지 않으므로 오류를 처리하지 않고 그대로 둘 수 있습니다. 즉, API 소비자가 오류를 처리해야 합니다.


일반적인 오류 HTTP 상태 코드는 다음과 같습니다.


  • 400 잘못된 요청 – 이는 클라이언트 측 입력이 유효성 검사에 실패 함을 의미합니다.
  • 401 Unauthorized – 사용자가 리소스에 액세스 할 수 있는 권한이 없음을 의미합니다. 일반적으로 사용자가 인증되지 않은 경우 반환됩니다.
  • 403 Forbidden – 사용자가 인증되었지만 리소스에 액세스 할 수 없음을 의미합니다.
  • 404 Not Found – 리소스를 찾을 수 없음을 나타냅니다.
  • 500 내부 서버 오류 – 일반적인 서버 오류입니다. 명시적으로 던져서는 안됩니다.
  • 502 Bad Gateway – 업스트림 서버의 잘못된 응답을 나타냅니다.
  • 503 서비스를 사용할 수 없음 – 서버 측에서 예기치 않은 일이 발생했음을 나타냅니다 (서버 과부하, 시스템의 일부 오류 등이 될 수 있음).

앱에서 발생한 문제에 해당하는 오류가 발생해야 합니다. 예를 들어 요청 페이로드의 데이터를 거부하려면 Express API에서 다음과 같이 400 응답을 반환해야 합니다.


const express = require('express');
const bodyParser = require('body-parser');

const app = express();

// existing users
const users = [
  { email: 'abc@foo.com' }
]

app.use(bodyParser.json());

app.post('/users', (req, res) => {
  const { email } = req.body;
  const userExists = users.find(u => u.email === email);
  if (userExists) {
    return res.status(400).json({ error: 'User already exists' })
  }
  res.json(req.body);
});


app.listen(3000, () => console.log('server started'));

위의 코드에는 주어진 이메일이 있는 사용자 배열의 기존 사용자 목록이 있습니다.


그런 다음 사용자에게 이미 존재하는 이메일 값으로 페이로드를 제출하려고 하면 '사용자가 이미 존재 함'메시지와 함께 400 응답 상태 코드를 수신하여 사용자가 이미 존재 함을 사용자에게 알립니다. 이 정보를 사용하여 사용자는 이메일을 존재하지 않는 것으로 변경하여 작업을 수정할 수 있습니다.


유지 관리자가 문제를 해결하기에 충분한 정보를 갖도록 오류 코드에 메시지가 함께 있어야 하지만 공격자는 오류 콘텐츠를 사용하여 정보 도용이나 시스템 중단과 같은 공격을 수행 할 수 없습니다.


API가 성공적으로 완료되지 않을 때마다 사용자가 수정 조치를 취할 수 있도록 정보와 함께 오류를 전송하여 정상적으로 실패해야 합니다.


필터링, 정렬 및 페이지 매김 허용 


REST API 뒤에 있는 데이터베이스는 매우 커질 수 있습니다. 때로는 너무 많은 데이터가 너무 느리거나 시스템을 중단 시키므로 한 번에 반환해서는 안됩니다. 따라서 항목을 필터링 하는 방법이 필요합니다.


또한 한 번에 몇 개의 결과만 반환하도록 데이터 페이지를 매기는 방법이 필요합니다. 요청 된 모든 데이터를 한 번에 가져 와서 리소스를 너무 오래 묶고 싶지는 않습니다.


필터링과 페이지 매김은 모두 서버 리소스 사용량을 줄여 성능을 향상 시킵니다. 더 많은 데이터가 데이터베이스에 누적 될수록 이러한 기능이 더 중요해집니다.


다음은 API가 다양한 쿼리 매개 변수가 있는 쿼리 문자열을 받아 필드별로 항목을 필터링 할 수 있는 작은 예입니다.


const express = require('express');
const bodyParser = require('body-parser');

const app = express();

// employees data in a database
const employees = [
  { firstName: 'Jane', lastName: 'Smith', age: 20 },
  //...
  { firstName: 'John', lastName: 'Smith', age: 30 },
  { firstName: 'Mary', lastName: 'Green', age: 50 },
]

app.use(bodyParser.json());

app.get('/employees', (req, res) => {
  const { firstName, lastName, age } = req.query;
  let results = [...employees];
  if (firstName) {
    results = results.filter(r => r.firstName === firstName);
  }

  if (lastName) {
    results = results.filter(r => r.lastName === lastName);
  }

  if (age) {
    results = results.filter(r => +r.age === +age);
  }
  res.json(results);
});

app.listen(3000, () => console.log('server started'));

위의 코드에는 쿼리 매개 변수를 가져 오는 req.query 변수가 있습니다. 그런 다음 JavaScript 구조화 구문을 사용하여 개별 쿼리 매개 변수를 변수로 구조화하여 속성 값을 추출합니다. 마지막으로 각 쿼리 매개 변수 값으로 필터를 실행하여 반환하려는 항목을 찾습니다.


이를 완료하면 결과를 응답으로 반환합니다. 따라서 쿼리 문자열을 사용하여 다음 경로로 GET 요청을 할 때 :


/employees?lastName=Smith&age=30 


우리는 :


[
    {
        "firstName": "John",
        "lastName": "Smith",
        "age": 30
    }
]

lastName과 age로 필터링 했기 때문에 반환 된 응답으로.


마찬가지로 페이지 쿼리 매개 변수를 수락하고 (페이지-1) * 20에서 페이지 * 20까지의 위치에 항목 그룹을 반환 할 수 있습니다.


쿼리 문자열에서 정렬 할 필드를 지정할 수도 있습니다. 예를 들어 데이터를 정렬하려는 필드가 있는 쿼리 문자열에서 매개 변수를 가져올 수 있습니다. 그런 다음 개별 필드별로 정렬 할 수 있습니다.


예를 들어 다음과 같은 URL에서 쿼리 문자열을 추출 할 수 있습니다.


http://example.com/articles?sort=+author,-datepublished 


여기서 +는 오름차순을 의미하고-는 내림차순을 의미합니다. 따라서 저자의 이름을 알파벳 순으로 정렬하고 가장 최근에서 가장 최근에 게시 한 날짜로 정렬합니다.


좋은 보안 관행 유지 


클라이언트와 서버 간의 대부분의 통신은 개인 정보를 자주 주고 받기 때문에 비공개 여야 합니다. 따라서 보안을 위해 SSL/TLS를 사용해야 합니다.


SSL 인증서는 서버에 로드 하기가 그리 어렵지 않으며 비용이 무료이거나 매우 낮습니다. REST API가 개방형이 아닌 보안 채널을 통해 통신하지 않을 이유가 없습니다.


사람들은 그들이 요청한 더 많은 정보에 액세스 할 수 없어야 합니다. 예를 들어 일반 사용자는 다른 사용자의 정보에 액세스 할 수 없어야 합니다. 또한 관리자의 데이터에 액세스 할 수 없어야 합니다.


최소 권한 원칙을 적용하려면 단일 역할에 대한 역할 검사를 추가하거나 각 사용자에 대해 더 세분화 된 역할을 가져야 합니다.


사용자를 몇 개의 역할로 그룹화 하기로 선택한 경우 역할에는 필요한 모든 것을 포함하는 권한이 있어야 하며 더 이상 필요하지 않습니다. 사용자가 액세스 할 수 있는 각 기능에 대해 더 세분화 된 권한이 있는 경우 관리자가 그에 따라 각 사용자에게 해당 기능을 추가 및 제거 할 수 있는지 확인해야 합니다. 또한 모든 사용자에 대해 수동으로 수행 할 필요가 없도록 그룹 사용자에게 적용 할 수 있는 사전 설정된 역할을 추가해야 합니다.


성능 향상을 위해 데이터 캐시 


사용자가 요청하는 일부 데이터를 검색 할 때마다 데이터를 가져 오기 위해 데이터베이스를 쿼리 하는 대신 로컬 메모리 캐시에서 데이터를 반환하는 캐싱을 추가 할 수 있습니다. 캐싱의 좋은 점은 사용자가 데이터를 더 빨리 얻을 수 있다는 것입니다. 그러나 사용자가 얻는 데이터는 오래되었을 수 있습니다. 이로 인해 프로덕션 환경에서 디버깅 할 때 오래된 데이터가 계속 표시되면서 문제가 발생할 수도 있습니다.


Redis, 인 메모리 캐싱 등과 같은 많은 종류의 캐싱 솔루션이 있습니다. 요구 사항이 변경되면 데이터가 캐시되는 방식을 변경할 수 있습니다.


예를 들어 Express에는 많은 구성 없이 앱에 캐싱을 추가하는 apicache 미들웨어가 있습니다. 다음과 같이 간단한 메모리 내 캐시를 서버에 추가 할 수 있습니다.


age: 50 },
]

app.use(bodyParser.json());

app.get('/employees', (req, res) => {
  res.json(employees);
});

app.listen(3000, () => console.log('server started'));


위의 코드는 apicache.middleware를 사용하여 apicache 미들웨어를 참조하며 다음과 같습니다.


app.use(cache('5 minutes')) 


전체 앱에 캐싱을 적용합니다. 예를 들어, 결과를 5 분 동안 캐시합니다. 필요에 맞게 조정할 수 있습니다.


API 버전 관리 


클라이언트를 손상 시킬 수 있는 변경 사항이 있는 경우 다른 버전의 API가 있어야 합니다. 버전 관리는 오늘날 대부분의 앱처럼 의미론적 버전 (예 : 주요 버전 2 및 여섯 번째 패치를 나타내는 2.0.6)에 따라 수행 할 수 있습니다.


이렇게 하면 모든 사람이 동시에 새 API로 이동하도록 하는 대신 기존 엔드 포인트를 단계적으로 제거 할 수 있습니다. v1 엔드 포인트는 변경을 원하지 않는 사람들을 위해 활성 상태를 유지할 수 있으며, 반짝이는 새로운 기능을 갖춘 v2는 업그레이드 할 준비가 된 사람들에게 서비스를 제공 할 수 있습니다. 이는 API가 공개 된 경우 특히 중요합니다. API를 사용하는 타사 앱을 손상 시키지 않도록 버전을 지정해야 합니다.


버전 관리는 일반적으로 API 경로 시작 부분에 /v1/, /v2/ 등을 추가하여 수행됩니다.


예를 들어, 다음과 같이 Express로 이를 수행 할 수 있습니다.


const express = require('express');
const bodyParser = require('body-parser');
const app = express();
app.use(bodyParser.json());

app.get('/v1/employees', (req, res) => {
  const employees = [];
  // code to get employees
  res.json(employees);
});

app.get('/v2/employees', (req, res) => {
  const employees = [];
  // different code to get employees
  res.json(employees);
});

app.listen(3000, () => console.log('server started'));

버전 번호를 끝점 URL 경로의 시작 부분에 추가하여 버전을 지정합니다.


결론 


고품질 REST API를 설계하는 데 있어 가장 중요한 사항은 웹 표준 및 규칙을 따라 일관성을 유지하는 것입니다. JSON, SSL/TLS 및 HTTP 상태 코드는 모두 최신 웹의 표준 빌딩 블록입니다.


성능도 중요한 고려 사항입니다. 한 번에 너무 많은 데이터를 반환하지 않음으로써 이를 늘릴 수 있습니다. 또한 항상 데이터를 쿼리 할 필요가 없도록 캐싱을 사용할 수 있습니다.


끝점의 경로는 일관성이 있어야 합니다. HTTP 메서드가 수행하려는 작업을 나타내기 때문에 명사만 사용합니다. 중첩 된 리소스의 경로는 상위 리소스의 경로 뒤에 와야 합니다. 그들은 무엇을 하고 있는지 이해하기 위해 추가 문서를 읽을 필요 없이 우리가 무엇을 얻거나 조작하고 있는지 알려 주어야 합니다.


https://stackoverflow.blog/2020/03/02/best-practices-for-rest-api-design/



댓글목록 0

등록된 댓글이 없습니다.

웹학교 로고

온라인 코딩학교

코리아뉴스 2001 - , All right reserved.