호세 나바로 는 벨기에 브뤼셀의 FAMOCO에서 풀 스택 개발자로 일하고 있습니다. 그는 지난 3년 동안 웹 개발자 웹 개발 및 모바일 기술에 깊은 관심을 가지고 있으며 Node.js, Java, AngularJS, ReactJS에 능통합니다.
소개
Node.js와 Couchbase ODM Ottoman을 사용하여 REST API를 개발하겠습니다. Node.js에는 이를 위한 몇 가지 프레임워크가 있으므로 API를 쉽게 시작하고 개발할 수 있고 코드가 깔끔하고 이해하기 쉬운 hapi.js를 사용하겠습니다. 또한 요청에 유효성 검사기를 제공하므로 코드를 추상화하고 객체로 작업할 수 있도록 사용할 Ottoman 모델과 잘 통합할 수 있습니다.
요구 사항
프로젝트를 빌드하려면 컴퓨터에 다음이 설치되어 있어야 합니다:
-
Node.js 및 NPM
-
카우치베이스 서버
Hapi 서버
먼저 프로젝트의 기본 디렉터리를 만든 다음 해당 디렉터리로 이동하여 npm 프로젝트를 시작하면 프로젝트에 대한 몇 가지 매개 변수를 입력하라는 메시지가 표시됩니다.
다음 명령으로 이 작업을 수행할 수 있습니다:
mkdir 노드–hapi–카우치베이스–api
npm 초기화
다음 단계는 프로젝트에 종속성을 추가하는 것입니다. 먼저, 프로젝트에 hapi.js 관련 패키지를 추가한 다음, Couchbase 관련 패키지를 추가하고 마지막으로 노데몬 를 코딩하는 동안 서버의 라이브 리로드를 위해 개발 종속성에 추가합니다.
npm 설치 –좋아요
npm 설치 –S 카우치베이스 오토만
npm 설치 –D 노데몬
이 모든 것이 준비되면 프로젝트를 만들기 시작합니다. 폴더를 만듭니다. src 여기에 모든 코드가 있습니다. 내부에 index.js 파일에 기본 hapi 서버가 있습니다. 여기에 다음 코드를 추가합니다:
const Hapi = require('hapi');
// 호스트와 포트가 있는 서버 생성
const 서버 = new Hapi.서버();
서버.연결({
호스트: 'localhost',
포트: 5000,
경로: {
cors: true,
}
});
// 서버 시작
서버.시작( err => {
만약( err ) {
// 여기에 멋진 오류 처리
콘솔.오류( err );
throw err;
}
콘솔.로그( 서버가 ${ server.info.uri }에서 시작되었습니다.
);
} );
모듈.수출 = 서버;
이제 막 기본 서버를 만들었습니다.
이제 서버의 진입 경로를 정의하겠습니다. 먼저 경로를 정의할 폴더 API를 생성합니다. 그리고 다음 파일을 만듭니다. index.js를 클릭하고 진입 경로의 코드를 입력합니다:
const 경로 = [
{
메서드: 'GET',
경로: ‘/’,
구성: {
핸들러: (요청, 답글) => {
반환 답글({
이름: '노드-해피-카우치베이스-api',
버전: 1
});
}
}
}
];
모듈.수출 = 경로;
메인에서 index.js 파일에서 경로를 가져올 것입니다. 이를 위해 앞서 정의한 server.start 코드 앞에 다음 코드를 추가합니다:
const 경로 = require('./api');
// 경로 추가
서버.경로(경로);
이제 package.json 파일에 스크립트 섹션으로 이동합니다.
"스크립트": {
"시작": "nodemon ./src/index.js"
},
실행하면 npm 시작를 클릭하면 서버가 시작됩니다. 다음으로 이동하여 확인할 수 있습니다. http://localhost:5000를 입력하면 응답을 받을 수 있습니다.
{"name":"노드-해피-카우치베이스-API","버전":1}
데이터베이스 커넥터
데이터베이스 커넥터를 설정하기 위해 폴더를 만들겠습니다. db 에 데이터베이스 정보와 커넥터의 로직을 저장합니다.
다음 코드를 사용하여 config.json 파일에 정보를 저장합니다:
{
"couchbase": {
"엔드포인트": "localhost:8091",
"bucket": "api"
}
}
커넥터의 경우 파일을 생성합니다. index.js에서 구성 파일과 Couchbase 라이브러리를 가져오고 데이터베이스 및 버킷과의 연결을 초기화할 것입니다.
let config = require('./config');
카우치베이스 = require('couchbase');
렛 엔드포인트 = 구성.카우치베이스.엔드포인트;
버킷 = 구성.카우치베이스.버킷;
myCluster = new 카우치베이스.클러스터(엔드포인트, 함수(err) {
만약 (err) {
콘솔.로그("카우치베이스에 연결할 수 없습니다: %s", err);
}
콘솔.로그('db %s에 연결됨', 엔드포인트);
});
내버킷 = myCluster.오픈버킷(버킷, 함수(err) {
만약 (err) {
콘솔.로그("버킷에 연결할 수 없습니다: %s", err);
}
콘솔.로그('버킷 %s에 연결됨', 버킷);
});
다음 단계는 Couchbase ODM Ottoman을 가져와서 버킷으로 설정하는 것입니다.
렛 오스만 = require('ottoman');
오스만.store = new 오스만.CbStoreAdapter(myBucket, 카우치베이스);
마지막으로 다른 파일에서 액세스할 수 있도록 버킷과 오트만을 내보내겠습니다.
모듈.수출 = {
버킷: myBucket,
오스만: 오스만
};
모델
이제 기본 서버가 실행 중이므로 Ottoman으로 모델을 정의해 보겠습니다. 두 가지 모델을 정의하겠습니다. 사용자 그리고 다른 하나는 게시물. 이를 위해 다음과 같은 폴더를 만듭니다. 모델를 클릭하고 그 안에 두 개의 js 파일을 생성합니다: user.js 그리고 post.js. 모델에 유효성 검사를 추가할 수 있지만 hapi.js는 경로를 처리하기 전에 유효성 검사를 제공하므로 이를 사용하여 모델에 전달하기 전에 사용자로부터 받은 데이터의 유효성을 검사할 것입니다.
사용자 모델
사용자에게는 세 개의 필드가 있습니다: 이름, 이메일, 그리고 비밀번호. Ottoman 패키지를 사용하여 사용자 모델을 만듭니다. 사용자 모델에는 다음 코드가 포함되어 있습니다:
렛 오스만 = require('../db').오스만;
let 사용자 모델 = 오스만.모델('사용자', {
비밀번호: '문자열',
이름: '문자열',
이메일: '문자열',
}, {
색인: {
이메일 찾기: {
by: '이메일',
유형: 'refdoc'
}
}
});
먼저 db 커넥터에서 시작한 Ottoman 인스턴스를 가져옵니다. 그런 다음 모델 정의를 시작합니다. 첫 번째 매개변수는 모델 이름(이 경우 'User')입니다. 두 번째 매개변수는 필드 이름과 유형을 포함하는 JSON 객체입니다(이 경우 모든 값은 문자열입니다(Ottoman 문서를 참조하세요.) 다음 매개변수는 생성하려는 인덱스가 포함된 객체입니다. 이메일에 대한 인덱스를 생성하여 해당 인덱스를 사용하여 모델을 사용하는 사용자를 쿼리할 수 있도록 하고, 사용자에 대한 이메일 중복을 방지하기 위한 제한도 생성할 것입니다.
인덱스를 생성할 때 보장 인덱스 함수를 호출하여 내부적으로 인덱스를 생성해야 합니다.
오스만.보장 인덱스(함수(err) {
만약 (err) {
반환 콘솔.오류('오류 보장 인덱스 USER', err);
}
콘솔.로그('인덱스 사용자 확인');
});
마지막 단계는 모델을 내보내는 것입니다.
모듈.수출 = UserModel;
포스트 모델
게시물에는 네 개의 필드가 포함됩니다: title 그리고 body에서 타임스탬프및 사용자.
먼저 db 커넥터에서 초기화한 Ottoman 인스턴스를 가져오고 사용자 모델도 가져옵니다.
렛 오스만 = require('../db').오스만;
let 사용자 = require('./user');
let 포스트모델 = 오스만.모델('포스트', {
사용자: 사용자,
title: '문자열',
body: '문자열',
타임스탬프: {
유형: '날짜',
기본값: 날짜.지금
}
});
첫 번째 매개변수는 모델 이름인 'Post'입니다. 두 번째 매개변수는 필드가 있는 JSON 객체입니다. 이 경우 사용자 유형을 사용자 이전 모델에서 정의한 제목 및 본문 유형 문자열및 타임스탬프 유형 날짜. 객체가 생성될 때 현재 타임스탬프로 기본값을 만들 것입니다.
마지막으로 모델을 내보냅니다.
모듈.수출 = PostModel;
API 경로
사용자와 글에 대한 경로를 정의하겠습니다. 기본 경로는 다음과 같습니다. /api/v1. API 내부의 index.js 파일에서 사용자 경로와 게시물 경로를 가져와서 배열로 결합합니다.
const 사용자 = require('./users');
const 게시물 = require('./posts');
…
경로 = 경로.concat(사용자);
경로 = 경로.concat(게시물);
사용자 경로와 포스트 경로 모두에서 CRUD 작업을 수행하는 메서드를 정의할 것입니다. 모든 경로에 대해 메서드, 경로 및 구성을 정의해야 합니다. 구성 섹션에서는 수행할 함수인 핸들러를 제공하고, 핸들 함수를 수행하기 전에 호출할 유효성 검사 함수를 제공할 수도 있습니다. 유효성 검사의 경우 요청 본문에 대한 스키마와 유효성 검사를 정의하는 데 사용할 수 있는 Joi 패키지를 사용하겠습니다.
사용자 경로
사용자에게는 다음과 같은 경로를 사용합니다. /api/v1/users. 라우팅 파일의 첫 번째 단계는 사용자 모델과 joi 패키지를 임포트하는 것입니다.
const 사용자 = require('../모델/사용자');
const Joi = require('joi');
사용자 목록 검색 GET /api/v1/users
핸들 함수에서는 User 모델의 찾기 함수를 사용하여 DB를 쿼리하여 User 유형의 모든 문서를 수집할 수 있습니다.
{
메서드: 'GET',
경로: '/api/v1/users',
구성: {
핸들러: (요청, 답글) => {
사용자.찾기({}, (err, 사용자) => {
만약 (err) {
반환 답글({
상태: 400,
메시지: err.메시지
}).코드(400);
}
반환 답글({
데이터: 사용자,
카운트: 사용자.길이
});
});
}
}
}
사용자 배열이 있는 객체와 배열 안의 객체 수가 포함된 카운트를 반환하겠습니다.
아이디로 사용자 검색하기 GET /api/v1/users/{id}
이 경우 문서 ID로 사용자를 쿼리할 것이므로 기본 제공 함수를 사용하겠습니다. getById 를 모델에 추가하여 데이터베이스에서 문서를 검색할 수 있습니다.
이 경우 당사는 유효성 검사 객체를 사용하여 매개 변수 값의 유효성을 검사합니다. id 는 문자열입니다.
{
메서드: 'GET',
경로: '/api/v1/users/{id}',
구성: {
핸들러: (요청, 답글) => {
사용자.getById(요청.매개변수.id, (err, 사용자) => {
만약 (err) {
반환 답글({
상태: 400,
메시지: err.메시지
}).코드(400);
}
반환 답글(사용자);
});
},
유효성 검사: {
매개변수: {
id: Joi.문자열(),
}
}
}
}
사용자의 문서를 반환합니다.
새 사용자 POST /api/v1/users 만들기
이제 새 사용자를 만들어 보겠습니다. 첫 번째 단계는 사용자 모델과 요청 본문으로 사용자를 만드는 것입니다.
해당 페이로드(요청 본문)를 확인할 수 있는 유효성 검사 객체를 제공합니다.
{
메서드: 'POST',
경로: '/api/v1/users',
구성: {
핸들러: (요청, 답글) => {
const 사용자 = new 사용자(요청.페이로드);
사용자.저장((err) => {
만약 (err) {
반환 답글({
상태: 400,
메시지: err.메시지
}).코드(400);
}
반환 답글(사용자).코드(201);
});
},
유효성 검사: {
페이로드: {
비밀번호: Joi.문자열().알파넘().분(3).최대(30).필수(),
이메일: Joi.문자열().이메일().필수(),
이름: Joi.문자열()
}
}
}
}
생성된 새 사용자의 객체를 반환합니다.
사용자 업데이트 PUT /api/v1/users/{id}
이제 사용자를 업데이트하겠습니다. 이 경우 먼저 데이터베이스에서 사용자 문서를 검색한 다음 필드를 업데이트하고 마지막으로 업데이트된 문서를 데이터베이스에 저장합니다.
이 경우 매개변수와 페이로드의 유효성을 검사하는 유효성 검사 객체를 제공합니다.
{
메서드: 'PUT',
경로: '/api/v1/users/{id}',
구성: {
핸들러: (요청, 답글) => {
사용자.getById(요청.매개변수.id, (err, 사용자) => {
만약 (err) {
반환 답글({
상태: 400,
메시지: err.메시지
}).코드(400);
}
const 페이로드 = 요청.페이로드;
만약 (페이로드.이름) {
사용자.이름 = 페이로드.이름;
}
만약 (페이로드.비밀번호) {
사용자.비밀번호 = 페이로드.비밀번호를 입력합니다;
}
사용자.저장((err) => {
만약 (err) {
반환 답글({
상태: 400,
메시지: err.메시지
}).코드(400);
}
반환 답글(사용자).코드(200);
});
});
},
유효성 검사: {
매개변수: {
id: Joi.문자열(),
},
페이로드: {
이름: Joi.문자열(),
비밀번호: Joi.문자열().알파넘().분(3).최대(30),
}
}
}
}
업데이트된 문서를 반환해 드립니다.
사용자 삭제 DELETE /api/v1/users/{id}
이 경우에는 사용자를 삭제하겠습니다. 먼저 데이터베이스에서 문서를 검색한 다음 삭제합니다.
{
메서드: '삭제',
경로: '/api/v1/users/{id}',
구성: {
핸들러: (요청, 답글) => {
사용자.getById(요청.매개변수.id, (err, 사용자) => {
만약 (err) {
반환 답글({
상태: 400,
메시지: err.메시지
}).코드(400);
}
사용자.제거((err) => {
만약 (err) {
반환 답글({
상태: 400,
메시지: err.메시지
}).코드(400);
}
반환 답글(사용자);
});
});
},
유효성 검사: {
매개변수: {
id: Joi.문자열(),
}
}
}
}
삭제된 문서는 반환해 드립니다.
마지막으로 경로를 내보내야 합니다.
모듈.수출 = 경로;
경로 게시
게시물 경로의 경우 /api/v1/users/{userId}/posts 경로를 사용하므로 사용자와 관련된 게시물에 대한 작업만 수행합니다. 데이터베이스에 사용자가 존재하는지 확인하고 요청을 처리하는 함수에서 사용자에게 액세스할 수 있도록 반환하는 유효성 검사 함수를 정의하겠습니다.
코드의 첫 번째 섹션은 임포트와 해당 함수입니다.
const 사용자 = require('../모델/사용자');
const 게시물 = require('../모델/게시물');
const Joi = require('joi');
const 유효성 검사 사용자 = (값, 옵션, 다음) => {
const userId = 옵션.컨텍스트.매개변수.userId;
사용자.getById(userId, (err, 사용자) => {
다음(err, 개체.할당({}, 값, { 사용자 }))
})
};
사용자의 게시물 목록 조회 GET /api/v1/users/{userId}/posts
사용자의 모든 게시물을 검색하기 위해 포스트 모델과 find 함수를 사용하겠습니다. 사용자의 모든 게시물을 검색하기 위해 사용자의 ID를 제공할 객체를 사용하여 실행하겠습니다.
{
메서드: 'GET',
경로: '/api/v1/users/{userId}/posts',
구성: {
핸들러: (요청, 답글) => {
const 사용자 = 요청.쿼리.사용자;
게시물.찾기({ 사용자: { _id: 사용자._id } }, (err, 게시물) => {
만약 (err) {
반환 답글({
상태: 400,
메시지: err.메시지
}).코드(400);
}
반환 답글({
데이터: 게시물,
카운트: 게시물.길이
});
})
},
유효성 검사: {
쿼리: validateUser,
}
}
}
게시물 배열과 게시물 수가 포함된 객체를 반환하겠습니다.
글 검색하기 GET /api/v1/users/{userId}/posts/{postId}
글 목록에서와 마찬가지로 하나의 글을 검색하려면 사용자 아이디와 검색하려는 글 아이디로 find 함수를 호출합니다.
{
메서드: 'GET',
경로: '/api/v1/users/{userId}/posts/{postId}',
구성: {
핸들러: (요청, 답글) => {
const 사용자 = 요청.쿼리.사용자;
const postId = 요청.매개변수.postId;
게시물.찾기({ 사용자: { _id: 사용자._id }, _id: postId }, (err, 게시물) => {
만약 (err) {
반환 답글({
상태: 400,
메시지: err.메시지
}).코드(400);
}
만약 (게시물.길이 === 0) {
반환 답글({
상태: 404,
메시지: '찾을 수 없음'
}).코드(404);
} else {
반환 답글(게시물[0]);
}
})
},
유효성 검사: {
쿼리: validateUser,
}
}
}
가장 먼저 수신한 게시물을 반환하겠습니다. 아이디로 게시물을 찾기 위해 DB를 쿼리하기 때문에 하나의 게시물만 수신할 수 있으므로 배열의 첫 번째 항목을 반환합니다. 게시물이 하나도 수신되지 않으면 해당 사용자와 관련된 해당 아이디의 게시물이 없다는 의미이므로 찾을 수 없음 오류를 반환합니다.
새 게시물 만들기 POST /api/v1/users/{userId}/posts
게시물을 생성하기 위해 사용자에서와 동일한 프로세스를 수행합니다. 페이로드에 대한 유효성 검사 객체를 제공하여 수신하는 본문의 유효성을 검사할 수 있습니다. 게시물을 수신하는 사용자(경로 및 타임스탬프)는 게시물을 생성할 때 생성되므로 게시물의 제목과 본문만 유효성을 검사합니다.
핸들러 함수에서 페이로드가 포함된 새 게시물을 생성하고 게시물의 사용자를 사용자로 설정합니다.
{
메서드: 'POST',
경로: '/api/v1/users/{userId}/posts',
구성: {
핸들러: (요청, 답글) => {
const 사용자 = 요청.쿼리.사용자;
const post = new 게시물(요청.페이로드);
post.사용자 = 사용자;
post.저장((err) => {
만약 (err) {
반환 답글({
상태: 400,
메시지: err.메시지
}).코드(400);
}
반환 답글(post).코드(201);
});
},
유효성 검사: {
쿼리: validateUser,
페이로드: {
title: Joi.문자열().필수(),
body: Joi.문자열().필수(),
}
}
}
}
작성된 게시물은 반환됩니다.
글 업데이트 PUT /api/v1/users/{userId}/posts/{postId}
글을 업데이트하려면 글 작성에서와 같이 유효성 검사 객체를 제공하고 글의 제목과 본문을 변경할 수 있도록 허용합니다. 여기서는 Post 모델과 getById 함수를 사용하여 글을 쿼리할 예정이므로 글을 검색할 때 사용자가 경로에 제공된 사용자와 일치하는지 확인합니다. 일치하는 경우 요청의 값으로 게시물의 필드를 업데이트하고 업데이트된 게시물을 저장합니다.
{
메서드: 'PUT',
경로: '/api/v1/users/{userId}/posts/{postId}',
구성: {
핸들러: (요청, 답글) => {
게시물.getById(요청.매개변수.postId, (err, post) => {
만약 (err) {
반환 답글({
상태: 400,
메시지: err.메시지
}).코드(400);
}
만약 (요청.매개변수.userId === post.사용자._id) {
const 페이로드 = 요청.페이로드;
만약 (페이로드.title) {
post.title = 페이로드.title;
}
만약 (페이로드.body) {
post.body = 페이로드.body;
}
post.저장((err) => {
만약 (err) {
반환 답글({
상태: 400,
메시지: err.메시지
}).코드(400);
}
반환 답글(post).코드(200);
});
} else {
반환 답글({
상태: 401,
메시지: "사용자가 게시물을 편집할 수 없습니다."
}).코드(401);
}
})
},
유효성 검사: {
쿼리: validateUser,
페이로드: {
title: Joi.문자열().필수(),
body: Joi.문자열().필수(),
}
}
}
}
업데이트된 글이 반환됩니다. 사용자가 글의 사용자와 일치하지 않는 경우 해당 사용자가 글의 소유자가 아니므로 권한 오류를 수신합니다.
글 삭제하기 /api/v1/users/{userId}/posts/{postId} 삭제하기
업데이트에서와 마찬가지로 게시물을 쿼리하고 경로의 사용자가 게시물의 사용자와 일치하는지 확인합니다. 일치하는 경우 계속 진행하여 게시물을 삭제합니다.
{
메서드: '삭제',
경로: '/api/v1/users/{userId}/posts/{postId}',
구성: {
핸들러: (요청, 답글) => {
게시물.getById(요청.매개변수.postId, (err, post) => {
만약 (err) {
반환 답글({
상태: 400,
메시지: err.메시지
}).코드(400);
}
만약 (요청.매개변수.userId === post.사용자._id) {
post.제거((err) => {
만약 (err) {
반환 답글({
상태: 400,
메시지: err.메시지
}).코드(400);
}
반환 답글(post).코드(200);
});
} else {
반환 답글({
상태: 401,
메시지: "사용자가 게시물을 삭제할 수 없음"
}).코드(401);
}
})
},
유효성 검사: {
쿼리: 유효성 검사 사용자
}
}
}
삭제된 게시물은 반환됩니다.
마지막으로 경로를 내보냅니다.
모듈.수출 = 경로;
테스트
API를 테스트하려면 Postman, cURL 또는 기타 애플리케이션을 사용하여 테스트할 수 있습니다.
아래에서는 API를 테스트하기 위해 몇 가지 cURL 예제를 만들었습니다. 사용된 ID는 POST 작업으로 생성한 것이므로 실행할 때 생성한 리소스의 ID와 일치하도록 경로를 변경해야 합니다.
# 사용자에 대해 쿼리합니다.
curl –X GET "http://localhost:5000/api/v1/users"
# 사용자를 생성합니다.
curl –X POST –H "콘텐츠 유형: 애플리케이션/json" –d ‘{
"name": "jose",
"password": "jose",
"이메일": "jose.navarro@famoco.com"
}’ "http://localhost:5000/api/v1/users"
# 사용자가 있는 json을 가져와야 합니다.
curl –X GET "http://localhost:5000/api/v1/users"
# 해당 ID를 가진 사용자를 가져와야 합니다.
curl –X GET “http://localhost:5000/api/v1/users/e0b66baa-851d-4aae-9ef2-f12575519e5e”
# 사용자를 업데이트합니다.
curl –X PUT –H "콘텐츠 유형: 애플리케이션/json" –d ‘{
"name": "jose_update",
"password": "joseedit"
}’ “http://localhost:5000/api/v1/users/e0b66baa-851d-4aae-9ef2-f12575519e5e”
# 사용자를 삭제합니다.
curl –X 삭제 “http://localhost:5000/api/v1/users/e0b66baa-851d-4aae-9ef2-f12575519e5e”
# 게시물
# 사용자의 게시물을 쿼리합니다.
curl –X GET “http://localhost:5000/api/v1/users/e717b7a3-e991-441e-8bca-562f2a572b19/posts”
# 게시물을 작성합니다.
curl –X POST –H "콘텐츠 유형: 애플리케이션/json" –d ‘{
"title": "내 게시물 제목",
"body": "내 포스트 본문"
}’ “http://localhost:5000/api/v1/users/e717b7a3-e991-441e-8bca-562f2a572b19/posts”
# 게시물을 쿼리합니다.
curl –X GET “http://localhost:5000/api/v1/users/e717b7a3-e991-441e-8bca-562f2a572b19/posts/94b1dd8e-73aa-4e4e-8b29-0870e6515945”
# 게시물을 업데이트합니다.
curl –X PUT –H "콘텐츠 유형: 애플리케이션/json" –d ‘{
"title": "내 편집된 제목",
"body": "내 편집 본문"
}’ “http://localhost:5000/api/v1/users/e717b7a3-e991-441e-8bca-562f2a572b19/posts/94b1dd8e-73aa-4e4e-8b29-0870e6515945”
# 게시물을 삭제합니다.
curl –X 삭제 “http://localhost:5000/api/v1/users/e717b7a3-e991-441e-8bca-562f2a572b19/posts/94b1dd8e-73aa-4e4e-8b29-0870e6515945”
결론
지금까지 살펴본 것처럼 CRUD 작업을 수행하기 위한 기본 REST API를 개발하는 것은 쉬웠고 코드도 간단하고 읽기 쉬웠습니다. 또한 Ottoman을 사용하면 객체와 ODM이 제공한 메서드를 사용하여 작업할 수 있는 DB 로직을 추상화할 수 있었습니다.