Back-End/ORM

[Sequelize] 비동기 함수와 Sequelize

SeanK 2021. 12. 23. 22:25

오늘 ORM 툴 중 하나인 Sequelize를 학습하다가

 

여러 번 키보드에 샷건을 때려 버렸다. 

 

Sequelize 시져시져...

 

공식문서를 보면서 최대한 열심히 학습을 해보려 했지만 도대체가 코드가 작동을 할 기미를 보이질 않았다. 

 

그러다가 아하! 하는 순간이 있었으니...

 

아래는 공식문서의 가장 첫 번째 설명 줄의 내용이다.

 

Sequelize is a promise-based Node.js ORM for Postgres, MySQL, MariaDB, SQLite and Microsoft SQL Server. It features solid transaction support, relations, eager and lazy loading, read replication and more.

 

Sequelize는 프로미스 기반의 node.js ORM이라는 설명!

 

에러가 계속 나온이유는 비동기적으로 처리해줘야 할 함수들을 아무 생각 없이 사용을 하고 있으니, 

 

return 값이 undefined(즉, promise)가 나오는 것이었다.  

 

아무래도 Sequelize 학습 이전에 promise에 대한 이해를 환기 시키는게 좋을 듯하여, 

 

아래 블로그 글을 번역해 옮겨본다. 

 

출처: https://medium.com/@rdf014/how-to-use-promises-with-sequelize-dd82c26bb2a

 

How To Use Promises With Sequelize

Anyone who is familiar with backend programming knows too well about callback hell. Code can easily become messy and unreadable. This post…

medium.com

 

How To Use Promises With Sequelize

 

백엔드 개발에 익숙한 분들은 콜백헬에 대해 잘 알 것이다. 코드는 쉽게 엉망진창이 되고 가독성이 떨어지게 된다. 이 포스팅은 여러분들의 백엔드 코드에 어떻게 간단하게 프로미스를 사용하는지 알려드리고자 작성되었다. 

 

왜 프로미스를 사용할까요? 프로미스는 코드의 가독성을 높이고 에러 핸들링을 훨신 쉽게 도와준다. 아래의. catch() 메서드 예제를 한 번 살펴보자. 

 

examplePromise()
.then(result => {
  if (/* Result is something you didn't want*/) {
    throw error;
  }
  return result
})
.catch(err => {
  return err;
  /* err will be equal to error on line 4*/
})

 

자바스크립트의 창시자, Brandon Eich는 자바스크립트를 실행할 때 에러를 '던질수' 있도록 만들었다. 엔지니어로서 우리는 프로그램을 사용하다 특정한 문제에 부딪혔을 때 프로그램이 에러를 던지도록 모델을 짜야한다. 자세한 내용은 뒤에서 설명하겠다. 

 

그래서 프로미스를 어떻게 이용하냐구요? 프로미스를 이용할 때 몇 가지 명심해야 할 것들이 있다. 프로미스 함수가 어떤 리턴 값을 반환하던, 그 값은 다음. then() 함수의 인자로 넘겨진다.

 

examplePromise()
.then(data => { // data is equal to the result of line 1.
  return data;
})

 

또한, 프로미스 안티패턴을 피해야 한다. 특히나 피하도록 권하고 싶은 안티 패턴은 바로 중첩된. then() 함수를 이용하는 것이다. 중첩. then() 함수는 우리가 피하고 싶었던 콜백과 비슷해 보인다. 

 

examplePromise1()
.then(result1 => {
  examplePromise2(result1)
  .then(result2 => {
    examplePromise3(result2)
    .then(result3 => {
      return result3;
    })
  })
})

 

마지막으로,. catch() 함수를 작성할 때 어떤 값이 던져지든 혹은 자바스크립트가 어떤 에러를 던지든 이는 뒤의. then() 함수를 즉시 무시하고 곧바로. catch() 함수가 실행되도록 한다. 

 

ORM의 Sequelize에서 기본적으로 모든 함수들은 프로미스 함수다. 따라서 모든 함수를 프로미스의 규칙에 맞춰서 작성할 수 있다. Sequelize의 함수들은 단순히 객체를 반환하지 않는다. 대신에 인스턴스를 리턴한다. 인스턴스 안에는 호출할 수 있는 여러 메서드를 가지고 있는데 주의 깊게 살펴볼 메서드는. get()과. update() 메서드이다.. get()은 레코드를 객체 형식으로 반환하는 메서드이며,. update()는 테이블의 레코드를 수정하는 메서드다. 

 

자 이제 기본설명은 끝났으니 예제를 살펴보자. 먼저 아래와 같은 간단한 스키마 디자인을 만들어 보자. 

 

Parents와 Child 테이블은 기본적으로 같지만 Parent 테이블은 엄마/아빠/새엄마/새아빠 등을 표시하는 type속성을 가지고 있다. 그리고 다대다 관계를 이어주는 조인 테이블이 있다. 

 

Sequelize로 데이터베이스 세팅과 기본 모델의 작동방식을 이해한다는 가정하에 설명을 이어가도록 하겠다. 

 

데이터베이스에 아래와 같은 데이터가 있다고 가정해보자. 

 

/* PARENT */
{
  {
    id: 1,
    name: 'John',
    type: 'Father'
  }
};

/* PARENT_CHILD */
{
  {
    id: 1,
    Parent_id: 1,
    Child_id: 1,
  }
};

/* CHILD */
{
  {
    id: 1,
    name: 'Joe',
  }
};

 

여기서 이름이 "Joe"인 아이를 탐색하고 싶다면 아래와 같이 작성한다:

 

Child.find({where: {name: 'Joe'}})
.then(childInst => {
  return childInst.get();
})
.then(childData => {
  return childData;
  /*
    {
      id: 1,
      name: "Joe"
    }
  */
})

꽤 간단하다. 2번째 줄의 인자는 1번째 줄 쿼리의 결과이다. 인스턴스로부터 데이터를 얻으려면 3번 줄과 같이. get() 함수를 호출해야 한다. 호출 결과는 5번째 줄의 인자로 전달되게 되며, ChildData는 주석 처리된 데이터와 같이 나오게 된다. 

 

다음으로, 이름이 "Joe"인 아이의 모든 부모님 정보를 얻고 싶다면 아래와 같이 작성한다:

 

Child.find({where: {Name: "Joe"}})
.then(childInst => {
  return childInst.get();
})
.then(childData => {
  return Parent_Child.find({where: {Child_id: childData.id}})
})
.then(parentChildInst => {
  return parentChildInst.get();
})
.then(parentChildData => {
  return Parent.find({where: {id: parentChildData.Parent_id}})
})
.then(parentInst => {
  return parentInst.get();
})
.then(parentData => {
  return parentData;
  /*
    {
      {
        id: 1,
        name: "John",
        type: "Father"
      }
    }
   */
})

 

Sequelize 문서를 살펴봤다면, include라는 쿼리를 발견했을 수도 있다. 이것은 조인 테이블에서 데이터를 받아올 수 있는데 필자는 이것을 가급적 사용하지 않는 편이다. 왜냐하면 항상 동일한 결과를 보장하지 않기 때문이다. 따라서 위와 같이 작성을 했고 다소 길긴 하지만 가독성에는 문제가 없다. 다른 테이블의 데이터를 쿼리 할 때. find() 체인을 이용하면 원하는 데이터를 얻을 수 있다. 

 

이번에는 Parent 테이블에서 John을 업데이트 해보자. 

 

Parents.find({where: {Name: "John"}})
.then(parentInst => {
  return parentInst.update({type: "Step-Father"})
})
.then(updatedInst => {
  return updatedInst.get();
})
.then(updatedData => {
  return updatedData;
  /*
  {
    {
      id: 1,
      name: 'John',
      type: 'Step-Father',
    }
  }
  */
})

 

3번 줄과 같이 .update()함수는 절대 실제 데이터가 아닌 인스턴스에서 호출해야 한다. 그리고 5번 줄과 같이 update 함수를 반환할 때 새로이 update 된 isntance는 다음. then() 함수의 인자로 넘겨진다는 사실을 명심해야 한다. 새롭게 update 된 instance에서 get 함수를 호출하면 주석과 같이 실제 데이터를 얻을 수 있다. 

 

이제는. catch() 함수를 보자. 당신이 Parents 테이블에서 질의하면서 만약 type이 Mother인 데이터와 Mother가 아닌 데이터로 나누어 어떤 작업을 하고 싶다고 가정해보자. 

 

Parents.find({where: {type: "Mother"}})
.then(parentInst => {
  return parentInst.get();
})
.then(motherData => {
  if (motherData === null) {
    throw //something;
  } 
  return motherData;
})
.then(motherData => {
  // Do something with motherData
})
.catch(() => {
  // Do something else
})

 7번째 줄이 의미하는 바는 쿼리가 아무것도 찾지 못하면 null을 리턴한다는 뜻이다. 만약 MotherData가 null이면 던져진 함수는. then() 블록을 스킵하고 곧바로. catch() 함수로 가게 된다. 

 

한 가지. catch() 함수에서 중요한 점은 "모든" throw 표현을 잡아낸다는 것이다. 만약 코드에 어떠한 에러가 발생하면 catch 함수가 에러를 잡아낼 것이고 이는 또 다른 에러를 야기할 가능성이 있다. 따라서. catch() 함수를 작성할 때는 이를 주의해야 한다.