ORM(Object-relational mapping)과 Model

ORM

ORM은 Object-Relational Mapping의 약어입니다.

전통적으로는 프로그램이 DB를 조작하기 위해서는 명령어(DB query string)를 생성하여 DB에 명령을 주고, 그 결과를 받았습니다. 이 이후에 나온 다른 개념으로 DB를 프로그램의 객체(object)에 연결(map)하여 사용하는 것이 ORM입니다.

예를 들어, DB에 학생들의 이름과 나이 정보를 보관하고 있을 때, DB에는 student라는 리스트(table 혹은 collection)이 존재할 것이고, 그 리스트에와 name, age라는 항목이 존재할 겁니다. 이 리스트를 ORM으로 불러오면 name, age의 항목을 가진 student object의 배열이 생성되어 프로그램에서 사용가능합니다. 단순히 리스트를 읽는 것 뿐만이 아니라, 프로그램에서 해당 리스트를 수정한 후 DB에 그대로 저장되게 할 수도 있습니다. DB조작을 object조작을 하듯이 할 수 있는 것이죠. DB 명령어(sql 등)은 전혀 몰라도 사용할 수 있구요.

이처럼 ORM은 DB data와 프로그램의 object를 연결하며 DB와 프로그래머 사이의 중간 역할을 해줍니다. 프로그래머는 DB에 직접 명령을 내리지 않고 연결된 객체의 함수를 사용해서 DB에 명령을 내리게 됩니다.

장점

  • 프로그래머가 해당 DB 언어를 몰라도 DB에 명령을 내릴 수 있다.

단점

  • DB에 직접 명령을 내리지 않고 한단계 거쳐가기 때문에 그만큼 속도가 느려진다.
  • ORM이 지정해 놓은 명령만 내릴 수 있다. 이때문에 하나의 목적을 수행하기 위해 ORM의 함수를 여러번 사용해야 하는 경우도 있다. 이 경우 속도가 느려진다.

결국 단점은 속도가 약간 느려진다는 점인데, 정말 약간 느려집니다. 사람이 느끼지도 못할 정도로요. 하지만 한번에 수천, 수만개의 데이터를 처리하거나 하는 등의 이유로 DB 속도가 정말 중요한 경우에는 ORM을 사용하지 않기도 합니다. 일반적인 경우에는 걱정할 필요가 없습니다.

Model

ORM은 프로그램이 DB를 객체에 연결하는 것 차제를 뜻하며, model(모델)은 이때 연결 된 객체입니다. 위의 학생부 예제에서는 student object가 모델입니다. 이 model에 DB에 명령을 내릴 수 있는 함수들이 담겨 있으며 프로그래머는 model의 함수를 사용하여 DB에 명령을 내리게 됩니다.

Model의 Schema

Schema(스키마)는 모델의 대상에 대한 구체적인 구조를 뜻합니다. 위의 학생부 예제에서는 나이, 이름의 항목을 가지고 있는데, 이러한 모델의 형태를 스키마라고합니다.

예제를 통해 알아봅시다

Node.js와 Mongoose를 이용하여 만든 코드의 예를 보며 구체적으로 알아봅시다. 이름나이가 있는 학생의 collection을 ORM을 통해 document를 쓰고 읽는 코드를 만듭니다. 이 예제는 NodeJS과 NPM이 설치되어 있어야 실행할 수 있습니다.

1. 컴퓨터의 터미널 프로그램(cmd, bash 등)을 실행하고 예제 프로젝트를 위한 폴더를 만듭니다.

2. 예제 프로젝트 폴더에서 아래 명령어를 입력하여 Mongoose package를 다운로드합니다. Mongoose는 ORM을 위한 library입니다. 

$ npm install mongoose --save

node_modules라는 폴더가 생성되었으면 다음으로 넘어갑니다.

3. example.js 파일을 생성하고 아래를 입력합니다.

// example.js

var mongoose = require('mongoose');
var dbUrl = /* DB_CONNECTION_STRING */;

var studentSchema = mongoose.Schema({
  name: {type:String, required:true},
  age: {type:Number, required:true}
});
var Student = mongoose.model('student',studentSchema);

mongoose.Promise = global.Promise;
mongoose.connect(dbUrl);
var db = mongoose.connection;

db.once("open",function () {

  Student.create({name:"June",age:20},function(error,student){
     console.log("Student.create:",student);

     Student.find({},function(error,students){
        console.log("Student.find:",students);
     });
     
  })

});

 /* DB_CONNECTION_STRING */ 부분에는 자신의 MongoDB 접속 URI(mongodb://dbuser:[email protected]:39195/abce1234 이런식으로 생겼습니다)에 ""을 씌워서 넣어줍니다. MongoDB 접속 URI가 없다면 mlab.com가입 및 온라인 Mongo DB 생성을 통해 만들 수 있습니다.

4. 아래 명령어로 프로그램을 실행합니다.

$ node example.js

5. 실행 결과입니다.


이름이 June이고 나이가 20인 document가 하나 생성되었고, Student.find에는 새로 생성된 데이터가 보입니다.(_id는 document의 고유 아이디, __v는 version key로 mongoose가 자동으로 생성합니다)

ctrl+c를 누르면 프로그램을 종료할 수 있습니다. 이 코드는 학생을 하나 만들고, 학생 전체 리스트를 보여주기 때문에 다시 실행하면 Student.find에는 계속 학생이 하나씩 늘어납니다.

이 예제를 가지고 ORM과 model, schema를 알아봅시다.

1. Mongoose는 ORM을 사용하게 해주는 library입니다. 예제에서 mongoose를 사용하여 학생 model을 만들었고 model을 통해 DB를 간접적으로 조작하였으므로 이 예제는 ORM을 사용하고 있습니다.(틀린표현: Mongoose는 ORM입니다/이 예제 프로그램은 ORM입니다)

var studentSchema = mongoose.Schema({  name: {type:String, required:true},
  age: {type:Number, required:true}
});
var Student = mongoose.model('student',studentSchema);

2. 학생은 name(String, required)과 age(Number, required)의 schema를 가지고 있습니다. 그리고 이 schema를 사용해서 student model을 만들었습니다. 즉 schema는 model의 구조및 설정이며, model은 실제 DB에 접근할 수 있는 객체입니다. Model 객체는 주로 첫글자를 대문자로 표시합니다.

  Student.create({name:"June",age:20},function(error,student){
     console.log("Student.create:",student);
  })
  .then(function(){
    Student.find({},function(error,students){
       console.log("Student.find:",students);
    });  });

3. Mongoose의 Model.create함수와 Model.find함수를 사용해서 실제 DB를 조작하는 부분입니다.
Mongoose의 Model.create함수는 해당 모델의 collection에 document를 하나 생성합니다. Model.createcallback 함수는 두개의 parameter를 인자로 받는데, 첫번째 인자는 error(만약 있다면)이고 두번째 인자는 생성된 document입니다. 두번째 인자는 document 하나가 들어오게 되므로 두번째 인자는 단수로 student이라고 썼습니다.
Mongoose의 Model.find함수는 해당 모델의 collection에서 조건에 맞는 자료들을 찾습니다. Model.find의 callback 함수는 두개의 parameter를 인자로 받으며 첫번재 인자는 error(만약 있다면), 두번째 인자는 찾은 document의 배열입니다. 현재 찾는 조건이 없으므로({}) 해당 collection 내 모든 document를 찾게 됩니다. 두번째 인자는 배열이 들어오게 되므로 복수형인 students로 썼습니다.(조건에 맞는 document가 하나 혹은 없더라고 무조건 배열이 들어옵니다.)

ORM, model, schema의 정의위 예제에서 Student(첫글자가 대문자, 단수), student(첫글자가 소문자, 단수), students(첫글자가 소문자, 복수)의 차이가 이해가 되시나요?

DB 관련글

댓글

박광중 2017.09.22
마지막 행 "Model을 담은 변수는 주로 단수로 표시하며, 첫글자를 대문자로 표시합니다"  예시를 추가해 주시면 이해가 한 층 좋을 듯 합니다. 감사합니다.
I
Ian H 2017.09.22
@박광중,
제안을 주셔서 감사합니다. 본문 내용이 부족한것 같아 내용을 추가하였습니다.
김종하 2017.12.08
document 하나가 들어오게 되므로 두번째 인자는 단수로 studnet이라고 썼습니다.
student 오타 났습니다.
I
Ian H 2017.12.08
@김종하,
매의 눈이시네요^^;; 수정하였습니다. 알려주셔서 감사합니다!
q
qnfzks55 2018.04.03
WARNING: The `useMongoClient` option is no longer necessary in mongoose 5.x, please remove it. 라고 에러가나는데 useMongoClient 를 삭제해도 오류가뜨네요! 
I
Ian H 2018.04.06
@qnfzks55,
useMongoClient와 별개로 create이 promise를 return하지 않게 업데이트가 되었네요. 두가지 문제 모두 본문 코드에 수정하였습니다.
문서에 기여해 주셔서 감사합니다^^
이수혁 2018.07.19
3. example.js 파일을 생성하고 아래를 입력합니다. 이건 어디에 생성하는거죠?
이수혁 2018.07.19
app.js 있는 곳에 생성하는건가요? 
I
Ian H 2018.07.23
@이수혁,
이 글의 예제는 ORM을 알아보기 위한 독립된 예제입니다.
새로 폴더를 만든 후 그 폴더 안에서 예제를 만드시면 됩니다.
2020.12.22
감사합니다! 몇 개의 document를 추가해서 응용해보고 싶은데요. 예를 들어 이름이 J라고 시작하는 학생을 검색하거나, 나이가 n세 이상인 학생을 찾는 등의 조건은 어떻게 적으면 되나요?
I
Ian H 2020.12.22
@규,
이름이 j로 시작하는 학생을 검색: Student.find({ name: { $regex: "^J", $options: "i" } }... 나이가 n세 이상인 학생을 검색: Student.find({ age: { $gte: n } }...
find에 대한 문서: https://docs.mongodb.com/manual/reference/method/db.collection.find $regex operator에 대한 문서: https://docs.mongodb.com/manual/reference/operator/query/regex $gte operator에 대한 문서: https://docs.mongodb.com/manual/reference/operator/query/gte
전체 query operator 리스트: https://docs.mongodb.com/manual/reference/operator/query
제 강의에서는 https://www.a-mean-blog.com/ko/blog/Node-JS-첫걸음/주소록-만들기/주소록-Index-New-Create 강의를 보시면 '모델.find'함수에 대한 예제가 처음으로 나오고 https://www.a-mean-blog.com/ko/blog/Node-JS-첫걸음/게시판-만들기-고급/게시판-검색-기능-만들기-1-제목-본문-검색 에서 좀 더 깊게 다루고 있습니다.
흑수의 세상 2021.01.24
안녕하세요. 블로그 잘 보고 있습니다. 저도 간단한 앱을 만들고 있는데 그 안에 커뮤니티가 존재합니다. 그래서 게시판 스키마를 만들었는데. 그 안에 글 스키마, 글 스키마 안에 댓글 스키마를 만들었습니다. Board 안에 contents 안에 comments가 있는 꼴인데, 위에서 말씀하신 대로 contents를 수정하거나 comments를 수정할 때, Board 오브젝트르 받아서 for문으로 해당하는 것을 찾아서 직접 수정하고 업데이트하는 방식으로 하는데 속도의 차이는 크게 없나요? 비효율적인 것 같아 계속 고민하고 있습니다. ㅠㅠ 아니면 스키마를 다 따로 분리해서 만드는게 나은 방식일까요??(Board, contents, comments 모두 각각-> 결국 Board를 찾고 거기에 속한 contents를 찾고 그에 맞는 comments를 확인하는 과정도 똑같을 것 같아 일단 보드 안에 다 넣어 놓았습니다. ) 감사합니다. 
I
Ian H 2021.01.25
@흑수의 세상,
안녕하세요! 반갑습니다^^
board.contents.comments의 구성과 board, comment를 분리하는 구성에는 각각의 장단점이 있습니다. 보통 comment가 board의 부속 객체로서만 쓰이는 경우가 많다면 board안에 넣구요, comments가 독립되어 사용되는 경우가 많다면 따로 뺍니다. 
예를 들어 작성자가 작성한 모든 댓글을 보여준다든가, 댓글에 추천 비추천이 있어서 어떠한 페이지에서 댓글 순위를 보여준다든가 등 이런 경우에는 board의 내용은 필요없고 comment만 필요한 경우들이죠. 이런 경우가 많다면 따로 뺄 수 있죠. 
성능에도 물론 차이가 있겠지만 성능보다는 위의 경우에 board안에 comment가 들어 있으면 코드를 작성하기가 조금 더 까다롭습니다.
다만 현재 말씀하신 내용을 들어보면 댓글 수정시
1. DB에서 board를 서버로 읽어옴. 2. board 오브젝트에 해당 comment를 찾아 수정.  3. 변경된 board 오브젝트 전체를 DB에 재저장.
으로 진행되는것 같은데, 위 방법은 문제가 있습니다. 만약 하나의 게시물의 두 댓글이 거의 동시에 저장되는 경우에 시간차에 의해 아래와 같은 일이 벌어질 수 있습니다.
1. 댓글A의 수정을 위해 게시물1이 읽혀짐 2. 댓글B의 수정을 위해 게시물1이 읽혀짐 3. 댓글A가 수정되고 저장됨(현재 게시물1 오브젝트는 댓글 A만 수정됨) 4. 댓글B가 수정되고 저장됨(현재 게시물1 오브젝트는 댓글 B만 수정됨)
즉 결과적으로 DB에는 댓글 B만 수정되고 댓글 A는 수정전의 댓글로 돌아가버리게 됩니다.
board의 comment를 수정시 서버에서 데이터를 읽어올 필요없이, 해당 데이터 수정만 바로 요청할수 있습니다.
https://stackoverflow.com/questions/34431435/mongodb-update-an-object-in-nested-array 글을 참고해보세요.
서버가 데이터를 읽어오지도 않고, for문으로 데이터를 찾을 필요가 없으니 속도가 빨라지기도 하겠네요.(해당 작업은 대신 DB에서 일어나지만 DB가 서버보다 빠릅니다)
좀 댓글이 복잡한게 작성 된 것 같은데.. 이해가 안되시는 부분이 있으면 다시 질문해주세요^^
흑수의 세상 2021.01.26
@Ian H,
상세한 설명 감사합니다. 발생할 수 있는 문제점까지 지적해주시다니 학교 수업만 듣다 이번 학기 휴학하고 앱개발 하고 있는데 이해가 정말 잘 되네요. board안에 content가 있고 그 content안에 comment가 있다보니 몽고 디비에서는 한 번에 찾지를 못하는 것 같아요. stackoverflow에서 찾아보니 몽고디비는 2개이상의 어레이부터는 검색 불가능 한가봐요. 그래서 (board, contents)랑 (comments, recomments) 이렇게 두개로 나누어서 말씀해주신대로 find, 등 몽고 디비 안에서 해결 할 수 있게 코딩 했습니다. 감사합니다 ^^!!
I
Ian H 2021.01.26
@흑수의 세상,
코딩을 배울땐 직접 만들어보는게 최고죠! 저도 2중 배열은 검색이 안되는지 몰랐네요. 한번 해봐야겠어요.
하시는 프로젝트 문제없이 잘 진행되기를 바랍니다!
댓글쓰기

이 글에 댓글을 다시려면 SNS 계정으로 로그인하세요. 자세히 알아보기

UP