SMAIVNN
article thumbnail

발단

프로젝트에서 아래 간단한 코드를 실행하던 도중 서버 오류가 발생하였다. 

우선 발단이 된 코드는 다음과 같다.

async createStat(stat: CreateStatDto): Promise<Stat> {
    const newStat = {
      ...stat,
    };
    const createdStat = await this.statModel.create(newStat);

    return createdStat
  }

 

 

발생한 서버 오류는 다음과 같다.

 

[MyApp] error   2023-11-20 09:48:40 [ExceptionsHandler] callback is not a function - {
  stack: [
    'TypeError: callback is not a function\n' +
    ...code...

 

처음에는 인터셉터의 문제인줄 알았다.

인터셉트 내에서 _id를 Repository에게 가져오는 작업에서 문제가 있으며, 이것에 대한 처리가 잘못되었구나 싶어 인터셉터를 이리 저리 고쳐보기 시작했다.

 

하지만 해당 에러는 아무리 생각해도 mongoDB에서 발생하는 에러였고, model에 적용하는 method()에서 발생하는 오류 중 하나였다. 이것은 Express에서는 보지 못한, NestJs를 사용하며 처음으로 겪어보는 이슈였다. (버젼 문제인것 같다.)

 

두 코드의 차이점

 async createStat(stat: CreateStatDto): Promise<Stat> {
	...code...
    const createdStat = await this.statModel.create(newStat);
	
    return createdStat.toObject();
  }
 async createStat(stat: CreateStatDto): Promise<Stat> {
    ...code...
    const createdStat = await this.statModel.create(newStat);

    return createdStat;
  }

 

두 코드의 차이점은 무엇일까?

바로 document.toObject() 함수가 사용된다는 것이다.

 

지금부터 mongoDB document를 활용하는 과정에서 발생한 문제에 대한 원인, 해결에 대해 기록한다.

 

원인 발견

service 내의 다른 코드에서는 아래와 같이 특정 필드를 출력하는 방식을 거쳤다.

return stat.readOnlyData;

 

하지만 내가 필요로 하는 함수에서는 특정 필드를 출력하지 않고 document를 그대로 반환하는 실수를 하였다.

return stat;

 

꼼꼼하게 보지 못한 문제였다.

 

앞선 코드와 같이 특정 필드를 추가해주니 오류는 없어졌고, 어째서 이러한 일이 발생하는지 알아봤다.

 

mongoDB document 반환

document 쿼리를 활용한 반환 값은 원하는 값만 나올것이라는 기대와 달리다음과 같은 코드로 이루어져 있다.

{
  "$__": {
    "$options": {
      "defaults": true
    },
    "$setCalled": {},

    // ... (생략)

    "backup": {
      "activePaths": {
        "default": {
          "_id": true
        },
        "modify": {
          "name": true,
          "position": true,
          "roles": true
        }
      }
    },
    "cachedRequired": {},

    // ... (생략)

  "$locals": {},
  "$op": null,
  "_doc": {
    "__v": 0,
    "_id": "[Circular reference found] Truncated by IDE",
    "name": "이름",
    "roles": [
      "ADMIN"
    ]
  },
  "isNew": false
}

 

이는 Mongoose Document의 형식이다. 데이트베이스에 반영하기 위한 여러가지 document에 대한 여러가지 정보가 함께 들어간다.

 

따라서 이러한 문서에 대해서 특정 필드를 직접 선택해 주지 않는 이상 return이 되지 않는 것이다.

 

우리는 활용하려는, 필요한 데이터가 아닌 __v와 같은 데이터는 필요 없다. 따라서 이것을 제거하는 작업을 해줬어야 하는 것이다..

 

해결 방법

이것을 위해서는 여러가지 방법이 있지만 약 3가지 정도로 추려볼 수 있을 것 같다.

 

toObject(), toJSON()을 통한 직렬화

 async createStat(stat: CreateStatDto): Promise<Stat> {
	...code...
    const createdStat = await this.statModel.create(newStat);
	
    return createdStat.toObject();
  }

 

앞서 보여주었듯이 toObject() 함수를 통해 plain javascript object로 변환해준다. toJSON()의 경우 json표현 객체로 변환해준다. 아래는 toObject()를 한 후 진행한 결과이다.

 

일반적으로 만약 데이터를 JSON 문자열 (JSON.stringify())할 경우가 있다면 toJSON()을, 그렇지 않으면 toObject()를 활용한다. 또한 이렇게 변환된 객체는 Mongoose문서 인스턴스의 메소를 사용할 수 없지만, 객체의 속성을 직접 조작할 수 있다. 반환된 객체를 다른 서비스나 컨트롤러에서 재사용하려는 경우 유용하다.

 

아래는 chatGPT 피셜이다.

.lean()

두 번째 방법은 .lean() 메서드를 사용하는 것이다. 하지만 .lean() 메소드는 쿼리 결과로 반환되는 Mongoose 문서 인스턴스를 일반 JavaScript 문서로 반환한다.

.create() 메소드의 경우, 새로운 문서를 데이터베이스에 저장하고, 저장된 문서의 인스턴스를 반환한다.

const leanStat = await this.statModel.findById(id).lean();

 

따라서 .lean()메소드를 사용할 수는 없고, 만약 create()에서 사용하고자 한다면 .save() 메소드로 저장을 한 후 저장된 문서를 찾아 .lean() 메소드를 사용할 수 있다. 

 

.lean()은 toObject()로 변환된 객체보다 훨씬 성능이 좋다. (약 10배 이상 차이난다.)

하지만 .lean() 메소드도 .toObject()와 같이, 변환된 객체는 다시 저장하려고 하면 Mongoose 문서 인스턴스가 아니기에 .save()등의 활용 불가능하다.

 

만약 다시 문서를 업데이트 하고자 한다면 findOneAndUpdate() 등과 같은 메소드를 활용해야 한다.

쿼리 결과를 주로 바로 이용하는 경우 사용하면 좋다.

 

Document return과 필드 추출

// service
async create(body: CreateStatDto, request: RequestWithCharacterId = null) {
	...code...
    const createdStat = await this.statRepository.createStat(newStat);
	// 이후 작업에서 재사용하는 코드
    
    return createdStat.readOnlyData;
  }
// repository
async createStat(stat: CreateStatDto): Promise<Stat> {
    ...code...
    const createdStat = await this.statModel.create(newStat);

    return createdStat;
  }

 

repository를 보면 mongoose document를 그대로 반환하고 있다.

service에서는 특정 필드를 return하고있다.

 

하지만 repository에서 반환된 결과는 mongoose document이기 때문에 다른 함수로 전달 되었을 때 mongoose 문서 인스턴스의 메소드를 사용이 가능하다. 따라서 더 세밀하게 작업을 하고 추가적인 활용이 필요할 때 유용하다.

 

마치며

이전 트러블슈팅 게시물과 더불어 이번 기회에 확실히 개념을 정리한 것 같다. 하지만 아직도 여러가지 문제가 남아있다.

 

특히 enum리스트 등의 활용과 타입 매치,.. 타입 매치가 가장 큰큰큰 문제다.

비록 지금은 해결을 했지만 기회가 된다면 이것으로도 포스팅을 남기자 파이팅.

profile

SMAIVNN

@SMAIVNN

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!