데이터베이스는 기본적으로 프로그래밍 언어를 지원하지 않는다. 그래서, 클라이언트에서 서버로 온 데이터가 데이터베이스로 들어가야할 때 '객체' 형태인 데이터를 데이터베이스에서 처리 가능한 '문자열' 형태로 변환해주는 게 필요하고, 반대로 데이터베이스의 데이터가 클라이언트로 내려갈 땐 역으로 '문자열'로 된 데이터를 프로그래밍 언어에서 지원하는 객체형 데이터로 변환해주는 게 필요하다.
해당 과정을 수행해주는 도구를 관계형 데이터베이스에서는 ORM, 몽고 DB와 같은 NoSQL에선 ODM이라고 부른다.
몽고 DB의 대표적인 ODM으론 Mongoose가 있다.
Mongoose의 목적은 자바스크립트 언어와의 상호작용을 더 수월하게 해주는 것이다. 단순히 직렬화-역직렬화를 수행해주는 것뿐만 아니라, 필요에 따라 다른 많은 기능들을 제공해준다.
mongoose의 사용 순서는 mongoose와 mongoDB의 특정 데이터베이스를 연결해주고(mongoose.connect 메서드 사용), 데이터베이스에 콜렉션을 만들어주고(mongoose.model, 즉 모델을 사용), 콜렉션에 추가될 도큐먼트를 정의해주는 것이다(생성자 함수인 모델로 인스턴스를 생성할 때 매개 변수로 도큐먼트로 추가될 객체 전달하기 ormodel.insertMany`의 매개변수에 매개 변수로 생성할 객체들을 담은 배열을 전달하기).
mongoose.model은 mongoDB의 콜렉션을 만들어주는 명령어이다. model을 통해 생성한 콜렉션은 model이 데이터베이스 관리를 위해 제공하는 다양한 인터페이스를 사용할 수 있다.
mongoose.model의 첫 번째 인자로 전달한 값이 대문자라면 소문자로 변경하고, 끝에 s를 붙여서 콜렉션을 생성한다. 예를 들어 mongoose.model('Movie')로 콜렉션을 만들면 movies라는 콜렉션이 되는 것이다.
mongoose.model로 콜렉션을 만들 땐 스키마에 대한 정보가 두번째 인자로 포함돼야 한다. 스키마란 NoSQL에 저장될 데이터들의 형태를 정의한 객체를 의미한다. 어떤 필드가 포함되는지, 각 필드별로 어떤 데이터 타입을 갖는지 등을 정의해둔 것이 스키마다(타입스크립트에서 interface로 타입을 정의해두는 것과 유사한 것 같다).
mongoose.model의 반환값은 생성자 함수다. new 연산자와 함께 실행하고, 생성자 함수의 매개 변수로 생성할 도큐먼트를 객체 형태로 전달하면 콜렉션에 도큐먼트가 생성된다. 이때, 생성한 도큐먼트는 mongoose.model에 전달한 스키마의 형태를 따라야 한다.
const Movie = mongoose.model('Movie', {...스키마})로 정의한 생성자 함수 Movie로 new Movie({ ... 도큐먼트 필드 값 })와 같이 생성한 콜렉션 movies는 자바스크립트 객체이다. 해당 자바스크립트 객체에 저장한 값을 실제 데이터베이스에 밀어넣어 주려면 movies.save() 코드를 꼭 실행해 줘야한다.
여러 도큐먼트를 한번에 생성하려면 Movie.insertMany([ ... 추가할 도큐먼트 객체들 나열]) 형태로 사용해주면 된다. 배열 안에 담은 객체들이 콜렉션의 도큐먼트로 생성된다. insertMany는 new Movie의 인자로 도큐먼트 객체를 넣어 인스턴스를 만든 다음 save()를 실행해주는 것과 다르게, 별도로 save 메서드를 실행하지 않아도 바로 연결된 mongoDB에 도큐먼트들이 추가된다.
그 전에, mongoose.connect()로 미리 mongoose와 mongoDB를 직접 연결해주는 과정이 필요하다. 해당 설정은 프로젝트를 처음 시작할 때 한 번만 처리해주면 되기 때문에 [공식 문서]에 있는 내용을 잘 참고해서 진행하자.
mongoose.model로 생성한 모델은 자바스크립트 코드를 통해 콜렉션에서 원하는 도큐먼트를 생성, 조회, 수정, 삭제할 때 유용하게 사용된다.
const Movie = mongoose.model('Movie', schema)로 Movie 모델을 정의해놨다면, 생성되는 movies 콜렉션에서 원하는 도큐먼트를 조회할 땐 find, findOne, findById 등의 메서드가 사용된다(조회를 위한 세 가지 메서드, 즉 인터페이스를 모델이 제공해준다).
find는 조건(mongoDB로 조회할 때 조건을 넣는 방식과 동일하다)에 해당하는 값들을 배열로 반환한다. 값이 하나더라도 배열의 0번 인덱스에 값이 들어오고, 값이 하나도 없으면 빈 배열이 반환된다.
findOne은 조건에 해당하는 도큐먼트 중 하나를 조회한다. 객체 형태로 값이 반환된다.
findById는 특정 _id에 해당하는 값을 찾아준다. _id는 별도 처리가 없다면 mongoDB가 도큐먼트를 저장할 때 직접 생성해주는 값이고, 도큐먼트 사이에 겹치지 않는 고유한 값이기 때문에, 하나의 값만 찾아서 조회해준다(실제로 Express 개발할 때 id를 받아 특정 값을 조회하는 패턴을 많이 사용한다고 하니 잘 기억해두자).
mongoose에서 도큐먼트를 업데이트 할 땐 모델에서 제공해주는 updateOne 또는 updateMany 메서드를 사용한다. Movie.updateOne 또는 Movie.updateMany의 첫 번째 매개 변수로 조건을 전달해주면 되고, 두 번째 매개 변수로는 업데이트 할 필드의 속성과 값을 키-값 형태로 전달해주면 된다. 기본적으로 put이 아니라 patch로 업데이트 할 수 있기 때문에 수정이 필요한 필드만 넣어주면 된다.
updateOne과 updateMany의 반환 값은 수정된 도큐먼트가 아니라, 수정 작업에 대한 결과이다. 만약 수정 작업 이후 수정된 결과를 반환값으로 받아서 활용해야 하면 findOneAndUpdate 같은 메서드를 활용할 수 있다. 해당 메서드는 수정할 도큐먼트를 찾는 조건, 수정할 값과 함께, 세 번째 매개 변수로 {new: true}를 입력해주면 수정된 값이 결과로 반환된다. 세 번째 매개 변수를 전달하지 않거나 { new: false }로 설정하면 변경되기 이전의 도큐먼트가 반환된다.
mongoose의 mongoose.Schema를 new 연산자와 함께 사용하면 데이터베이스 모델에 적용할 스키마를 생성할 수 있다.
스키마는 mongoose.Schema()에 객체 형태의 매개 변수로 전달하고, 각 객체의 프로퍼티 별로 별도 객체를 지정해서 디테일한 설정을 해줄 수 있다. 예를 들어, const productSchema = new mongoose.Schema({ name: { type: String, required: true }})라는 설정을 해주면 name 프로퍼티는 필수가 된다(없을 시 에러가 발생한다).
name의 type은 String 이라고 좁혀져있다. 참고로, mongoose로 호환되는 자바스크립트 언어는 타입 정의가 굉장히 유연하기 때문에, String이라고 타입을 정의했다고 해서 무조건 문자열 타입만 값으로 받아주진 않는다. 대신, 받은 값을 String 타입으로 변환해서 값을 저장해주며, 그런 의미에서 타입 정의는 '데이터베이스에 저장될 타입 정의'라고 보는 게 적절할 것 같다.
type: Number로 지정하고, 숫자로 변환될 수 없는 유형의 문자열이 값으로 들어오면 에러를 뱉는다.
name이라는 하나의 프로퍼티만 갖는 것으로 스키마를 정의했는데, 만약 다른 프로퍼티(e.g. color)를 추가로 모델에 전달해주면 color는 데이터베이스에 저장되지 않는다. 즉, 스키마를 통해 데이터베이스에 저장되는 값을 통제할 수 있는 것이다.
Schema를 잘 사용하면 데이터베이스에 저장될 값들을 일관되게 관리할 수 있고, 유효성 검증도 진행해줄 수 있다.