REST API(RESTful API - 레스트 풀 API)는 Distributed Hypermedia Systems(디스트리뷰트드 하이퍼미디어 시스템 - 분산 하이퍼미디어 시스템)을 위한 REST(REpresentational State Transfer - 레프리젠테이셔널 스테이트 트렌스펄) 아키텍처의 제약 조건을 준수하는 API(Application Programming Interface - 애플리케이션 프로그래밍 인터페이스)를 뜻합니다.
Guiding Principles of REST
1. Client–server(클라이언트-서버) : 사용자 인터페이스 관련 프로세스(클라이언트)를 데이터 스토리지 관련 프로세스(서버)로 부터 분리하여 여러 플랫폼에서 사용자 인터페이스(클라이언트)가 사용(이식성 - portability - 포터빌리티)될 수 있게 합니다. 그리고 서버 시스템을 단순화하여 확장되도록 합니다.
즉, 클라이언트와 서버로 역할을 명확히 분리하여 의존성을 줄이는 겁니다.
클라이언트는 서버의 환경(구성 및 설정)에 대해 제약을 받지 않고 서버로부터 제공되는 인터페이스를 통해 요청하고 응답을 받아 처리합니다. 그리고 자체적으로 정보를 관리합니다.
서버는 클라이언트의 환경에 영향을 받지 않고 서버에서 제공한 인터페이스로 요청된 내용만 처리하고 응답합니다. 그리고 클라이언트에 대한 정보를 관리하지 않습니다.
2. Stateless(무상태) : 서버에는 클라이언트와 관련된 상태 정보를 저장하고 있지 않습니다. 그래서 서버에 저장된 콘텍스트를 활용할 수 없기 때문에 클라이언트에서 서버로 요청을 전달할 때 처리에 필요한 모든 정보가 포함되어야 합니다. 따라서 세션 상태는 클라이언트에서 관리되어야 합니다.
3. Cacheable(캐시 가능) : HTTP 웹 표준을 사용하기 때문에 HTTP가 가진 캐싱 기능이 적용 가능합니다. 클라이언트 요청에 대한 응답으로 데이터에 명시적으로 캐시 가능 또는 캐시 불가능에 대해 지정되어야 합니다. 클라이언트에 캐시가 되어 있다면 동일한 요청에 대해 캐시 된 데이터를 재 사용할 수 있습니다. 서버에서도 캐시 기능을 사용하여 동일한 요청에 대해 재사용할 수 있습니다.
응답에 Cache-Control, Last-Modified, ETag(HTTP 콘텐츠가 바뀌었는지를 검사할 수 있는 태그)를 이용하면 캐싱 구현이 가능합니다.
4. Uniform interface(일관된 인터페이스) : 클라이언트 요청에 대해 일반적인 (소프트웨어 엔지니어링 원칙에 따른) 인터페이스를 적용함으로써 일관된 인터페이스로 시스템 아키텍처를 단순화시킵니다.
REST는 네 가지 인터페이스 제약 조건으로 정의되어야 합니다.
4-1. 리소스(resource) 식별(identification)
리소스들은 URI를 구분자로 식별이 가능해야 합니다.
http://localhost:9000/boards -> 게시판
http://localhost:9000/members -> 회원
위 URI에서 boards가 리소스입니다. 리소스는 복수형 명사를 사용하고 소문자로 사용합니다. 만약 리소스가 단일 데이터로 구성되어 있으면 명사로 사용합니다.
URI에서 리소스에 있는 전체 아이템과 개별 아이템을 구분하기 위해서 URI에 유일 값(중복되지 않는 숫자나 문자열)을 추가합니다.
http://localhost:9000/boards -> 전체 게시판 내용
http://localhost:9000/boards/1 -> 번호가 1인 게시판 내용
URI에서 리소스가 그룹으로 구성되어 있을 경우 계층으로 구분합니다.
http://localhost:9000/boards/notices -> 전체 공지사항 내용
http://localhost:9000/boards/notices/1 -> 번호가 1인 공지사항 내용
http://localhost:9000/boards/faqs -> 전체 FAQ 내용
URI에서 리소스에 있는 개별 아이템이 리소스를 가지고 있을 경우 계층으로 구분합니다.
http://localhost:9000/members/testid1/addresses -> testid1 회원의 주소록
http://localhost:9000/members/testid1/friends/tom -> testid1 회원의 친구 목록중 tom
URI에서 마지막 문자에 슬래시(/)를 포함하지 않습니다.
http://localhost:9000/boards/1/ (X)
http://localhost:9000/boards/1 (O)
URI에는 CRUD(Create, Read, Update, Delete)와 같은 처리에 대한 표현이 포함되지 않아야 합니다.
http://localhost:9000/boards/read/1 (X)
http://localhost:9000/boards/1 (O)
CRUD(Create, Read, Update, Delete)와 같은 처리 HTTP 메서드로 표현되어야 합니다.
URI에는 파일 확장자가 포함되지 않아야 합니다.
http://localhost:9000/boards/1/thumbnail.png (X)
http://localhost:9000/boards/1/thumbnail (O)
요청 헤더(header)에 있는 Accept를 사용합니다. Accept : image/png
4-2. 표현(representations)을 통한 리소스 처리
리소스는 요청된 표현으로 처리되어 전달됩니다.
GET /boards/1 HTTP/1.1
Host: localhost:9000
Accept: application/json
Accept-Language: ko
요청 메서드와 요청 헤더(header)에 있는 Accept과 Accept-Language에 의해 서버에서 리소스 처리가 달라집니다.
Accept이 "application/json"이면 JSON으로 "text/html"이면 HTML로 서버에서 리소스를 처리하여 전달해야 합니다.
그리고 Accept-Language는 언어권이기 때문에 요청된 언어로 서버에서 리소스를 언어권에 맞게 찾거나 처리해서 전달해야 합니다.
응답 헤더(header)에 있는 Content-Type과 Content-Language를 통해 처리된 리소스를 확인할 수 있습니다.
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 145
{"success":true,"data":{"no":1,"subject":"테스트 제목1","content":"테스트 내용1","writer":"testid1","writedate":"2021-08-09 13:00:00"}}
HTTP 메서드에 따른 역할
HTTP Method | CRUD | 역할 |
GET | Read | 리소스를 전체 또는 개별 조회합니다. |
POST | Create | 리소스를 추가합니다. |
PUT | Update | 리소스를 수정합니다. |
DELETE | Delete | 리소스를 삭제합니다. |
4-3. 자체 설명형(self-descriptive) 메시지
서버와 클라이언트에서 전달되는 메시지는 메시지만으로 이해하여 처리할 수 있게 정보를 포함하고 있어야 합니다.
위에서 언급한 요청 메시지처럼 GET 메서드를 사용하면 리소스를 가져오고 PUT 메서드를 사용하면 리소스를 수정한다는 것으로 메시지의 의미를 알 수 있고 응답 메시지처럼 Content-Type이 "application/json"으로 클라이언트에게 JSON으로 읽어 처리하라는 의미를 알 수 있습니다.
4-4. 애플리케이션의 상태가 하이퍼링크에 의해 처리되어야 합니다. HATEOAS(헤이티오스)라고 합니다.
쉽게 말하면 HTML 프로토콜과 HTML 태그에 의해 애플리케이션의 상태가 처리되어야 한다는 겁니다.
<a> 태그를 이용하여 번호 1인 게시판 내용을 가져옵니다.
<a href="http://localhost:9000/boards/1">번호가 1인 게시판 내용 가져오기</a>
GET /boards/1 HTTP/1.1
Host: localhost:9000
Accept: application/xml
Accept-Language: ko
<form> 태그를 이용하여 POST 메서드로 게시판을 추가합니다.
<form action="http://localhost:9000/boards" method="POST">
<input name="subject" type="text" value="테스트 제목4" />
<input name="content" type="text" value="테스트 내용4" />
<input name="writer" type="text" value="testid4" />
</form>
5. Layered system(계층화 시스템) : 서버 시스템을 여러 계층으로 구성할 수 있게 하고 클라이언트가 직접적으로 계층에 접근할 수 없도록 하여 서버 시스템을 유연하게 확장시킬 수 있습니다.
6. Code on demand(optional) : 서버가 클라이언트에게 데이터에 스크립트 형태의 코드를 같이 전달하여 클라이언트에서 데이터를 처리하도록 할 수 있습니다. 클라이언트는 서버로부터 전달받은 코드를 사용함으로써 클라이언트에서 사전 구현(pre-implemented) 해야 하는 기능의 수를 줄여 클라이언트를 단순화할 수 있습니다.
Node.js로 REST API 서버를 생성하고 실행하겠습니다.
REST API를 테스트하는 용도로 사용할 수 있게 데이터베이스를 연결하지 않고 내부 배열을 통해 처리하겠습니다.
그렇지만 실무에 바로 사용할 수 있습니다.
이후 Vue.js에서 Axios를 이용할 때 REST API 서버로 사용됩니다.
Node.js의 REST API 서버 생성하기
폴더는 "testrestapi"으로 생성하시고 패키지는 기본 설정(모든 입력에서 엔터키를 눌러 넘어갑니다.)으로 합니다.
이전 "Node.js 패키지 생성 및 실행"을 참조해서 "Node.js의 패키지 만들기", "Express.js 설치하기"까지 진행하시기 바랍니다.
추가로 dateformat 모듈을 설치하시기 바랍니다.
dateformat 모듈 설치하기
날짜를 지정된 포맷 문자열로 변화해주는 dateformat 모듈을 설치하기 위해 콘솔에서 npm install 명령어를 실행합니다.
npm install --save dateformat
버전을 설정하지 않으면 가장 최신 버전이 설치됩니다. 현재 최신 버전은 4.6.3입니다.
npm install --save dateformat@4.5.1
package.json 파일의 dependencies에 추가됩니다.
"dependencies": {
"dateformat": "^4.5.1", --> 추가
"express": "^4.17.1"
}
REST API 만들기
1. 생성된 C:\workspaces\nodeserver\testrestapi 폴더에서 REST API를 만들기 위해 boardapi.js 파일을 생성합니다.
boardapi.js 파일을 오픈하여 위에서 설치한 express.js를 이용하기 위해 가져오고 라우터를 가져옵니다. 그리고 dateformat를 가져옵니다.
const express = require('express');
const router = express.Router();
const dateFormat = require('dateformat');
DB를 사용하지 않기 때문에 데이터로 게시판 객체를 가지고 있는 배열을 생성하고 초기 데이터를 등록합니다.
let boardList = [
{no:1, subject:"테스트 제목1", content:"테스트 내용1", writer:"testid1", writedate:"2021-08-09 13:00:00"},
{no:2, subject:"테스트 제목2", content:"테스트 내용2", writer:"testid2", writedate:"2021-08-09 13:10:00"},
{no:3, subject:"테스트 제목3", content:"테스트 내용3", writer:"testid3", writedate:"2021-08-09 13:20:00"}];
게시판 객체는 번호(no), 제목(subject), 내용(content), 작성자(writer), 작성일(writedate)로 구성됩니다.
Get Method - Read All
HTTP GET 메서드로 요청(Request)이 들어오면 게시판 배열 전체를 리턴합니다.
router.get('/', function(req, res, next) {
console.log("REST API Get Method - Read All");
res.json({success:true, data:boardList});
});
res.json() 메서드는 응답(Response) 메시지를 JSON 구조로 전달합니다.
Get Method - Read Index
HTTP GET 메서드로 게시판 번호(no)가 포함되어서 요청(Request)이 들어오면 게시판 배열에서 게시판 번호로 검색하여 게시판 객체를 리턴합니다.
router.get('/:no', function(req, res, next) {
console.log("REST API Get Method - Read " + req.params.no);
var boardItem = boardList.find(object => object.no == req.params.no);
if (boardItem != null) {
res.json({success:true, data:boardItem});
} else {
res.status(404);
res.json({success:false, errormessage:'not found'});
}
});
게시판 번호(no)가 게시판 배열에 없으면 404 에러와 에러 메시지를 리턴합니다.
POST Method - Create
HTTP POST 메서드로 요청(Request) 내용(body)에 게시판 내용이 JSON 데이터로 포함되어서 요청이 들어오면 게시판 배열에 추가합니다.
router.post('/', function(req, res, next) {
console.log("REST API Post Method - Create");
var boardLastItem = boardList.reduce((previous, current) => previous.no > current.no ? previous:current);
var boardItem = new Object();
boardItem.no = boardLastItem.no + 1;
boardItem.subject = req.body.subject;
boardItem.content = req.body.content;
boardItem.writer = req.body.writer;
boardItem.writedate = dateFormat(new Date(), "yyyy-mm-dd HH:MM:ss");
boardList.push(boardItem);
res.json({success:true});
});
array에서 reduce() 메서드는 배열 안의 데이터를 순차적으로 가져와 처리하는 메서드입니다. 대부분 연산 결과를 누적하는 메서드로 많이 사용됩니다.
reduce() 메서드의 첫 번째 인자는 accumulator로 누적되는 값이고 두 번째 인자는 currentValue로 현재 값으로 accumulator에 currentValue를 연산(+,-,*,/)시켜 누적된 accumulator를 리턴합니다.
여기서는 누적하지 않고 비교해서 처리하는 방법으로 사용해 보겠습니다.
accumulator는 이전 게시판 데이터로 currentValue는 현재 게시판 데이터로 해서 현재 게시판 번호(no)가 크면 현재 게시판 데이터를 이전 게시판 데이터로 변경합니다. 그래서 최종적으로 게시판 배열에서 게시판 번호(no)가 제일 큰 게시판 데이터를 가져오게 합니다.
새로운 게시판 객체를 만들기 위해 새로운 객체를 생성합니다.
게시판 번호(no)는 게시판 배열에 제일 큰 게시판의 번호(no)에 1을 추가해서 설정합니다.
제목(subject)과 내용(content), 작성자(writer)는 req.body로 가져온 값으로 설정합니다.
작성일(writedate)은 dateFormat() 메서드를 이용하여 현재 시간을 "yyyy-mm-dd HH:MM:ss" 포맷으로 처리해서 설정합니다.
생성된 게시판 객체를 게시판 배열에 추가합니다.
현재 설치되는 express 버전이 4.17.1 이기 때문에 요청(Request)의 내용(body)을 JSON 데이터로 변화해서 사용할 수 있게 기능이 포함되어 있습니다.
그렇지만, express 버전이 4.16.x 이전이면 HTTP POST 메서드로 요청(Request)의 내용(body)이 들어오는 데이터를 JSON 데이터로 변화해서 사용하기 위해서 body-parser 모듈이 필요합니다.
body-parser 모듈을 설치하기 위해 콘솔에서 npm install 명령어를 실행합니다.
npm install --save body-parser
PUT Method - Update
HTTP PUT 메서드로 게시판 번호(no)와 요청(Request) 내용(body)에 게시판 내용이 JSON 데이터로 포함되어서 요청이 들어오면 게시판 배열에서 게시판 번호로 검색하여 게시판 객체의 내용을 업데이트합니다.
router.put('/:no', function(req, res, next) {
console.log("REST API Put Method - Update " + req.params.no);
var boardItem = boardList.find(object => object.no == req.params.no);
if (boardItem != null) {
boardItem.subject = req.body.subject;
boardItem.content = req.body.content;
boardItem.writer = req.body.writer;
boardItem.writedate = dateFormat(new Date(), "yyyy-mm-dd HH:MM:ss");
res.json({success:true});
} else {
res.status(404);
res.json({success:false, errormessage:'not found'});
}
});
DELETE Method - Delete
HTTP DELETE 메서드로 게시판 번호(no)가 포함되어서 요청(Request)이 들어오면 게시판 배열에서 게시판 번호로 검색하여 게시판 객체를 삭제합니다.
router.delete('/:no', function(req, res, next) {
console.log("REST API Delete Method - Delete " + req.params.no);
var boardItem = boardList.find(object => object.no == req.params.no);
if (boardItem != null) {
var index = boardList.indexOf(boardItem);
if (index >= 0) {
boardList.splice(index, 1)
res.json({success:true});
} else {
res.status(404);
res.json({success:false, errormessage:'not found'});
}
} else {
res.status(404);
res.json({success:false, errormessage:'not found'});
}
});
생선 된 라우터를 export 합니다.
module.exports = router;
2. C:\workspaces\nodeserver\testrestapi 폴더에 index.js 파일을 생성합니다. index.js는 npm init 명령어 실행에서 "entry point"로 입력한 시작 파일 명입니다.
index.js를 오픈하여 위에서 설치한 express.js를 이용하여 간단하게 웹 요청을 받아 처리하고 응답하게 코딩합니다.
const express = require('express');
const app = express();
express.json() 메서드를 이용하여 요청(Request)의 내용(body)을 JSON 데이터로 변화해서 사용할 수 있게 app.use() 메서드에 설정합니다.
app.use(express.json());
express.urlencoded() 메서드를 이용하여 URL를 인코딩해서 사용할 수 있게 app.use() 메서드에 설정합니다.
app.use(express.urlencoded({extended:false}));
만약, express 버전이 4.16.x 이전이면 "body-parser"를 추가하고 app.use() 메서드를 수정하시면 됩니다.
const bodyParser = require('body-parser');
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: true}));
HTTP Request의 헤더(header)에 모든 리소스에 접근할 수 있게 허용하여 CORS(Cross-origin resource sharing, 교차 출처 리소스 공유) 문제를 해결합니다.
그리고 GET, POST, PUT, DELETE 메서드를 사용할 수 있게 허용하고 content-type으로 요청을 보낼 수 있게 허용합니다.
app.use(function (req, res, next) {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
res.header('Access-Control-Allow-Headers', 'content-type');
next();
});
require() 메서드로 boardapi.js 모듈을 불러와서 웹 경로("/boards")로 사용되게 설정합니다.
app.use('/boards', require('./boardapi'));
app.listen() 메서드를 이용하여 지정된 9000 Port로 접속할 수 있게 대기(listen)합니다.
app.listen(9000, () => {
console.log('Listening...');
});
npm으로 실행하기 위해 Script 추가 하기
C:\workspaces\nodeserver\testrestapi 폴더에 package.json 파일을 오픈하여 "scripts"에 node 실행문을 추가합니다.
"start": "node index.js"
{
"name": "testrestapi",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "node index.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"dateformat": "^4.5.1",
"express": "^4.17.1"
}
}
"scripts"에 있는 "test"는 삭제해도 됩니다.
npm run 명령어로 실행할 수 있습니다.
npm run start
Postman(포스트맨)를 이용하여 REST API 데스트 하기
Get Method - Read All 데스트
http://localhost:9000/boards
리턴 값에 success가 true이고 data에 전체 게시판 내용을 가져옵니다.
{
"success": true,
"data": [
{
"no": 1,
"subject": "테스트 제목1",
"content": "테스트 내용1",
"writer": "testid1",
"writedate": "2021-08-09 13:00:00"
},
{
"no": 2,
"subject": "테스트 제목2",
"content": "테스트 내용2",
"writer": "testid2",
"writedate": "2021-08-09 13:10:00"
},
{
"no": 3,
"subject": "테스트 제목3",
"content": "테스트 내용3",
"writer": "testid3",
"writedate": "2021-08-09 13:20:00"
}
]
}
Get Method - Read Index 테스트
http://localhost:9000/boards/3
리턴 값에 success가 true이고 data에 3번째 게시판 내용을 가져옵니다.
{
"success": true,
"data": {
"no": 3,
"subject": "테스트 제목3",
"content": "테스트 내용3",
"writer": "testid3",
"writedate": "2021-08-09 13:20:00"
}
}
POST Method - Create 테스트
http://localhost:9000/boards
Request Body
{
"subject" : "테스트 제목4",
"content" : "테스트 내용4",
"writer" : "testid4"
}
입력된 내용으로 게시판에 등록되어 리턴 값에 success가 true입니다.
{
"success": true
}
GET 메서드로 새로 등록된 게시판 내용을 가져와 확인합니다.
http://localhost:9000/boards/4
{
"success": true,
"data": {
"no": 4,
"subject": "테스트 제목4",
"content": "테스트 내용4",
"writer": "testid4",
"writedate": "2021-08-09 14:34:48"
}
}
PUT Method - Update 테스트
http://localhost:9000/boards/4
Request Body
{
"subject" : "테스트 제목44444",
"content" : "테스트 내용44444",
"writer" : "testid4"
}
입력된 내용으로 4번째 게시판이 수정되어 리턴 값에 success가 true입니다.
{
"success": true
}
GET 메서드로 업데이트된 게시판 내용을 가져와 확인합니다.
http://localhost:9000/boards/4
{
"success": true,
"data": {
"no": 4,
"subject": "테스트 제목44444",
"content": "테스트 내용44444",
"writer": "testid4",
"writedate": "2021-08-09 14:38:03"
}
}
DELETE Method - Delete 테스트
http://localhost:9000/boards/4
4번째 게시판이 삭제되어 리턴 값에 success가 true입니다.
{
"success": true
}
콘솔을 보면 요청되어 처리된 REST API 메서드들이 출력된 것을 확인할 수 있습니다.