MongoDB는 NoSQL이지만, SQL에서 테이블 간 관계를 맺고, join으로 필요한 데이터를 조합해 사용하는 것과 유사한 기능이 제공된다.
MongoDB에서 관계의 형성은 도큐먼트에 필드를 추가해서 맺어진다. SQL에서 테이블에 column을 추가해서 추가된 열에 관계가 맺어질 테이블의 정보를 저장하는 것과 유사하다.
도큐먼트에 관계를 위한 필드를 추가할 땐 스키마에 { type: Schema.Types.ObjectId, ref: 'User' } 형태로 사용할 수 있다. 여기서 ref에 정의된User 값 위치에는 관계를 형성하고 싶은 콜렉션의 이름을 넣어주면 된다.
type에 사용된 Schema.Types.ObjectId는 관계를 형성할 콜렉션에 있는 도큐먼트들이 만들어질 때 Mongo에서 자동으로 부여한 _id 값을 의미한다. 즉, type: Schema.Types.ObjectId와 같이 좁혀주면 관계 형성을 위해 다른 필드가 아닌 '관계를 형성하고 싶은 콜렉션의 도큐먼트 아이디'만을 값으로 들고 있게 되는 것이다.
이렇게 정의하고, 해당 도큐먼트에 새로운 값을 추가할 땐 관계 형성을 위한 필드에 원하는 도큐먼트를 추가해 주면 된다.
예를 들어, Comment 모델의 스키마가 { /* ... 다른 필드 정의들 */, user: { type: Schema.Types.ObjectId, ref: 'User' }} 형태로 정의됐다면, new Comment({ /* ... 추가할 값들 */, user}) 형태로 user 도큐먼트를 값으로 넣어주면 된다(user 변수에 원하는 User 모델의 도큐먼트가 저장되어 있었어야 한다).
위와 같이 필드를 추가해 주면 User 모델의 원하는 도큐먼트의 _id 필드의 값이 Comment 모델 도큐먼트의 user 필드에 추가되게 된다. User 도큐먼트에 어떤 필드가 몇 개나 있든 상관없이, Schema.Types.ObjectId로 타입을 정의했기 때문에 _id만 값으로 저장된다.
나머지 값들은 필요시 관계형 데이터베이스의 join과 유사하게, _id 값을 기반으로 원하는 도큐먼트의 필드를 가져와 사용할 수 있다.
이때 사용되는 메서드가 populate다. populate는 단어 뜻 그대로 관계가 맺어진 모델의 도큐먼트에서 원하는 값을 '채워 넣는' 역할을 한다.
populate는 Mongo의 명령어로 원하는 데이터를 조회할 때, 조회된 객체의 메서드로 포함되어 있다. 예를 들어, await Comment.find({}).populate('user')와 같은 형식으로 사용하면, 조회한 모든 코멘트 도큐먼트들 중 user 필드가 있을 경우에 한해서 연결된 User 모델의 도큐먼트의 모든 필드 값을 가져와 함께 조회해 준다.
만약에 User 모델의 도큐먼트에서 모든 필드를 가져오는 게 아니라 일부 필드만 가져오고 싶다면 populate 메서드의 두 번째 매개변수로 원하는 필드 정보를 전달해 주면 된다.
이때, title date author와 같이 두 개 이상의 필드를 선택해서 가져오고 싶으면 하나의 문자열에 띄어쓰기로 구분해서 필드 이름을 나열해 주면 된다(매개변수는 결국 하나이고, 구분자가 띄어쓰기다).
만약에 특정 필드를 빼고 합치고 싶다면 ~title과 같은 형식으로 사용해 주면 된다. ~title ~date ~author처럼 역시 띄어쓰기로 구분해 주면 가져오고 싶지 않은 여러 필드 정보를 전달할 수도 있다.
두 모델을 관계를 형성할 때 어디에 어떤 식으로 연결을 할지는 자유도가 있는 판단의 영역이다. 관계를 단방향으로 엮어서 관리할 수도 있고, 양쪽 모델 모두에 양방향으로 관계를 맺어줘서 관리해 주는 것도 가능하다.
NoSQL은 SQL과 달리 자유도가 높게 설계 가능하다는 특징이 있으며, 관계를 어디에 어떤 식으로 정의할지는 그러한 자유도의 영역으로 남겨진 부분 중 하나이다. 좀 더 확장성이 좋고 관리하기 편한 방식으로 그때그때 상황에 맞게 선택하면 되는데, 어떤 상황에서 뭐가 좋은지는 여러 케이스를 경험해 보면서 직접 답을 찾아가는 게 필요하다.
두 모델의 관계를 맺어줬다면, 한쪽 모델의 도큐먼트를 삭제할 때 관계로 연결된 다른 모델의 값을 어떻게 할지도 이슈가 된다. 이 또한 NoSQL 데이터베이스 설계 시 자유도의 영역으로 남아있는 부분이다.
만약에 관계가 연결된 다른 쪽 모델의 도큐먼트가 다른 도큐먼트하고도 다대다로 연결된 상황이라면 한 쪽 모델의 도큐먼트를 지운다고 다른쪽 도큐먼트를 함께 지워주는 게 부자연스러울 수 있다. 하지만, 도큐먼트가 서로만을 참조하고 있는 상황이고, 참조된 상대 쪽 도큐먼트가 삭제됐을 때 관계로 연결된 다른 도큐먼트의 의미가 사라지는 상황이라면 데이터베이스에서 함께 지워주는 게 논리 상 좀 더 좋다.
이런 상황에서 Mongoose에서 제공하는 미들웨어를 사용할 수 있다. Mongoose는 Express와 같이 어떠한 작업(요청-응답) 처리의 전-후에 필요한 작업을 추가할 수 있도록 미들웨어를 제공한다.
Mongoose의 미들웨어는 스키마에 pre 또는 post 메서드를 호출해서 정의할 수 있다. Schema.post('작업 내용', '작업 내용 후 수행할 콜백 함수') 형태로 사용할 수 있다.
MongoDB에 저장된 도큐먼트의 필드가 배열일 경우, $pull 메서드를 사용할 수 있다. Campground.findByIdAndUpdate(campId, { $pull: { reviews: reviewId } }) 형태로 사용 가능하고, campId에 해당하는 도큐먼트를 찾아서 타입이 배열인 reviews 필드에서 값이 reviewId인 경우를 찾아 배열에서 제거해 준다.