배경

mongoose는 Schema에 기본적으로 _id라는 필드명으로 인덱스(type: mongoose.Schema.Types.ObjectId)를 자동으로 추가해줍니다.

따라서 우리가 document를 하나 생성할 때마다 자동으로 인덱스가 생성되어 추가됩니다.

문서를 조회하면 { _id: ObjectId(‘63c8f2a264ef009315acf031’), //…other property } 와 같이 결과가 조회됩니다.

문제점

저희는 MySQL과 MongoDB를 동시에 사용하고 있습니다.
MySQL은 인덱스에 대해 id라는 컬럼명을 갖고, MongoDB는 _id라는 필드명를 갖고 있습니다.
하지만 클라이언트에게 데이터를 보낼 때 데이터 형식을 통일하고자 하였기에, MongoDB에서 조회한 데이터를 응답할 때 _idid로 변환하고자 하였습니다.

해결방법 1

mongoose의 find() 함수에는 projection이라는 파라미터가 있습니다.

해당 파라미터는 select와 같은 역할을 하며 리턴할 필드들을 선택할 수 있습니다.

mongoose의 find() 함수의 projection 파라미터로 { _id: 0 , id: ‘$_id’ } 라는 값을 넘겨주면 _id라는 필드는 선택하지 않고, _id 필드의 값을 id라는 필드명으로 받아올 수 있습니다.

// '_id' 프로퍼티를 조회하지 않기
const messages = DMModel.find({}, '-_id').exec();

// '_id' 프로퍼티 대신 'id'라는 프로퍼티명으로 조회하기
const messages = DMModel.find({}, { _id: 0, id: '$_id' }).exec();

하지만 문제가 발생하였습니다.

이렇게 데이터를 받아올 경우 각 인스턴스의 id 값을 호출하는 것이 불가능했습니다.

// 정확한 이유는 아직 찾지 못했습니다..

const messages = this.dmModel.find({}, { _id: 0, id: '$_id' }); 
// { _id: new ObjectId('63c8f2a264ef009315acf031'), ..., createdAt: 2023-01-19T08:55:34.417Z }

console.log(messages[0].name); // 2023-01-19T08:55:34.417Z
console.log(messages[0].id); // null
console.log(messages[0]._id); // undefined

해결방법 2

두번째 해결방법은 바로 map과 mongoose schema의 instance methods를 사용하는 것이었습니다.

find() 함수를 통해 데이터를 조회할 때는 _id라는 필드명 그대로 받아오고, 후에 _idid로 변환해주는 methods를 통해 결과값을 변환하였습니다.

아래는 제가 구현한 코드입니다.

Schema

import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import mongoose, { Document } from 'mongoose';

@Schema({ timestamps: { createdAt: 'createdAt' } })
export class DM extends Document {
  @Prop({ required: true, type: mongoose.Schema.Types.Number })
  dmRoomId: number;

  @Prop({ required: true, type: mongoose.Schema.Types.Number })
  sender: number;

  @Prop({ required: true })
  content: string;

  @Prop({ default: new Date(), type: mongoose.Schema.Types.Date })
  createdAt: Date;

  toClient(): { id: string; sender: number; content: string; createdAt: Date } {
    const obj = {
      id: this._id,
      sender: this.sender,
      content: this.content,
      createdAt: this.createdAt,
    };

    return obj;
  }
}

export const DMSchema = SchemaFactory.createForClass(DM);

DMSchema.loadClass(DM);

위에서 정의한 toClient() 라는 methods는 각 인스턴스에서 _id와 sender, content, createdAt 값을 각각 id, sender, content, createdAt이라는 키에 담아서 객체를 반환합니다.

그리고 아래와 같이 toClient()를 map으로 각 인스턴스에 대해 호출해서 그 결과값을 array로 반환합니다.

*@fxts/core는 지연평가가 가능한 함수형 프로그래밍 라이브러리입니다.

import { pipe, map, toArray } from '@fxts/core';

// ...

console.log(messages[0]); // { _id: new ObjectId("63c8f2a264ef009315acf031"), ..., createdAt: 2023-01-19T08:55:34.417Z }
console.log(messages[0]._id) // new ObjectId("63c8f2a264ef009315acf031")
console.log(messages[0].id) // 63c8f2a264ef009315acf031

return { 
	messages: pipe(
		messages,
	  map((message) => message.toClient()),
	  toArray,
	),
  	// [ { id: '63c8f2a264ef009315acf031',
	//	  ...
  	//	  createdAt: 2023-01-19T08:55:34.417Z
	//	}, ... ]
}

*참고: id는 _id를 string 타입으로 변환하여 반환

이렇게 하면 messages[0].id로 각 인스턴스의 id 값도 호출가능하고, 클라이언트에게 _id 필드를 id로 변환하여 데이터를 보낼 수도 있습니다.

추가로 고민해볼 점

map() 함수 내에서 로직을 작성해도 되지 않나? 굳이 toClient()라는 메소드를 스키마에 정의한 이유가 있나?
-> 관심사 분리

아래와 같이 작성할 경우 이 함수가 목적으로 하고 있는 기능과 객체 구조를 변경하는 기능이 혼재됨...
코드도 복잡해짐...
따라서 인스턴스 구조를 변환하는 로직은 toClient()라는 함수 안에 선언해서 호출하는 게 깔끔하다고 생각했다.

return { 
	messages: pipe(
		messages,
	  map((message) => ({
        id: message.id, 
        sender: message.sender,
      	content: message.content,
        createdAt: message.createdAt,
      })),
	  toArray,
	),
  	// [ { id: '63c8f2a264ef009315acf031',
	//	  ...
  	//	  createdAt: 2023-01-19T08:55:34.417Z
	//	}, ... ]
}

0개의 댓글