jetalog.net

Sequelize는 Node.js에서 가장 많이 사용되는 ORM입니다.

이번에 처음 Sequelize를 사용해보면서 학습한 내용을 저와 같이 처음 사용하시는 분들을 위해 정리해서 공유합니다.

 

Sequelize에 대해서 공식 홈페이지에는 아래와 같이 소개하고 있습니다.

Sequelize는 Postgres, MySQL, MariaDB, SQLite, Microsoft SQL Server를 지원하는 Promise 패턴 기반의 Node.js ORM입니다. Solid 트랜잭션, 관계 설정, 즉시 로딩, 지연 로딩, 읽기 전용 복제본 등을 포함해 많은 기능을 제공합니다.

(원문: 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 | Sequelize ORM

여기서 볼 수 있는 Sequelize의 중요한 키워드는 두 가지 입니다.

 

Promise 패턴 기반

Promise 패턴은 JavaScript의 비동기 코드를 더 우아하게 만들어줍니다. Chaining을 통해 Callback Hell에서 탈출할 수 있고, 깔끔하게 예외처리를 할 수 있습니다. 또한 async와 await를 이용해서 간편하게 비동기 제어도 가능합니다.

 

ORM

ORM은 Object Relational Mapping(객체-관계 매핑)의 약자입니다. 자동으로 DBMS의 데이터를 객체 형태로 변환해줘서 더 객체 지향적인 코드를 생산할 수 있도록 도와줍니다. 이를 통해 개발자는 데이터를 가지고 어떤 작업을 할지에만 집중할 수 있습니다. 또한 손쉽게 DBMS를 변경할 수 있어서 제품이 높은 이식성을 가질 수 있게 됩니다.

 

설치

MySQL을 사용하는 패키지에 Sequelize를 적용하는 경우에 대해 정리합니다.

만약 다른 DBMS를 사용한다면 DBMS를 명시하는 부분에서 본인의 DBMS로 변경하면 손쉽게 변경됩니다.

또한 기존 프로젝트가 없다면 새로 생성한 후 아래 명령어를 실행하면 됩니다.

 

  1. 아래 명령어로 필요한 패키지를 설치합니다. 만약 다른 DBMS를 사용한다면 mysql2 대신 그에 맞는 DBMS Client 패키지를 설치하면 됩니다.
    npm i --save sequelize mysql2


  2. 아래 명령어로 CLI 도구를 설치합니다. 이미 설치되어 있다면 다시 설치하지 않아도 됩니다.
    npm i --global sequelize-cli

     
  3. 적용할 프로젝트의 루트에서 아래 명령어로 Sequelize를 초기화합니다.
    sequelize init

     

여기까지 진행하면 아래의 4가지 폴더가 만들어지고, 각 폴더의 역할은 아래와 같습니다.
아래 예제에서는 config와 models 폴더만 다룹니다.

  • config
    DB 연결 정보 등을 저장합니다.

  • migrations
    마이그레이션에 필요한 데이터가 자동으로 저장됩니다.

  • models
    DB 모델 정의를 저장합니다.

  • seeders
    테스트에 필요한 데이터를 정의합니다.

설정

자동으로 생성된 config/config.json 파일을 편집해서 DB 연결 정보를 저장합니다.

아래 생성되는 설정 예제와, 각 속성에 대한 주석을 참고할 수 있습니다.

눈여겨 볼 특징으론 개발할 때와 운용할 때에 서로 다른 DB를 사용할 수 있도록 해준다는 점입니다.

{
    "development": { // 개발 모드
        "username": "jeta", // DB 사용자명
        "password": "P@ssw0rd", // DB 암호
        "database": "seq_development", // 사용할 DB
        "host": "127.0.0.1", // DB 주소
        "dialect": "mysql", // DBMS
        "operatorsAliases": false // 연산자 별칭
    },
    "test": { // 테스트 모드
        "username": "jeta",
        "password": "P@ssw0rd",
        "database": "seq_test",
        "host": "127.0.0.1",
        "dialect": "mysql",
        "operatorsAliases": false
    },
    "production": { // 운용 모드
        "username": "jeta",
        "password": "P@ssw0rd",
        "database": "seq_production",
        "host": "127.0.0.1",
        "dialect": "mysql",
        "operatorsAliases": false
    }
}

 

모델 정의

모델이라는 개념은 ORM을 처음 사용하면 잘 이해가 되지 않을 수 있습니다.

Sequelize의 공식 문서에는 모델을 다음과 같이 정의하고 있습니다.

모델은 데이터베이스의 테이블을 나타냅니다. 그리고 이 모델 클래스의 인스턴스는 데이터베이스의 행을 나타냅니다.

(원문: A Model represents a table in the database. Instances of this class represent a database row.)

Model | Sequelize

만약 OOP를 해보셨던 분이라면 클래스와 객체의 관계를 떠올리셔서 더 빨리 이해할 수 있을겁니다.

 

Sequelize는 아래와 같은 형식으로 모델을 정의합니다.

sequelize.define()의 첫 인자로 모델명을 전달하고, 두 번째 인자로 열에 대한 정보를 전달합니다.

module.exports = (sequelize, DataTypes) => {
   return sequelize.define('Model_name', {
       // 열 (Column) 정의
       _id: { // 열 이름
          type: DataTypes.INTEGER, // 자료형
          primaryKey: true, // Primary Key 여부
          autoIncrement: true, // 자동증가 여부
          comment: '문제 고유 ID', // 설명
      },
      question: { // 열 이름
          type: DataTypes.STRING(200), // 자료형
          allowNull: false, // Null 혀용 여부
          comment: '문제', // 설명
      }
   });
}

열에 대한 정보는 몇 가지 옵션을 이용해 전달하게 됩니다. 자주 사용되는 옵션을 아래 간략히 소개합니다.

  1. type
    자료형을 나타냅니다. INTEGER, FLOAT, STRING 등 자주 사용되는 자료형을 거의 대부분 사용할 수 있습니다. 사용 가능한 모든 자료형은 이 링크에서 확인할 수 있습니다.

  2. allowNull
    Null 값을 혀용할지 여부입니다. 만약 false로 지정하면 값이 Null인 경우 값을 DB에 저장하지 않습니다. 기본값은 true입니다.

  3. autoIncrement
    자동 증가열로 지정할지 여부입니다. 만약 true로 지정하면 자동 증가를 설정합니다. 기본값은 false입니다.

  4. defaultValue
    기본값을 지정합니다.

  5. comment
    열에 대한 설명을 입력합니다.

  6. freezeTableName
    모델에 대한 테이블명을 모델명 그대로 사용하도록 합니다. Sequelize는 모델명을 단수로 이해하며, 테이블명은 복수로 다룹니다. 예를 들어 모델명이 user라면 테이블명은 users로 생성하고 조회합니다. 만약 tmp와 같이 복수로 만들지 않아도 되는 모델이 있다면 이 옵션을 true로 지정하여 복수형 변환을 하지 않도록 설정할 수 있습니다. 기본값은 물론 false입니다.

  7. timestamps
    createdAt열과 updatedAt열을 추가할지 여부입니다. 이 옵션이 활성화되면 자동으로 createdAt열과 updatedAt열을 생성하고 데이터가 생성되었을 때와 수정되었을 때에 자동으로 갱신됩니다. 만약 false로 지정하면 이 열을 생성하지 않습니다. 기본값은 true입니다.

  8. 기타
    더 많은 옵션은 이 링크에서 확인할 수 있습니다.

Sequelize를 연습하기 위해 Quiz를 관리할 수 있는 서버를 제작하고 있습니다.

아래에는 Quiz를 정의하기 위해 만들었던 모델입니다.

더 보기를 클릭하시면 예제 코드를 확인할 수 있습니다.

...더보기
module.exports = (sequelize, DataTypes) => {
    return sequelize.define('Quiz', {
        _id: {
            type: DataTypes.INTEGER,
            primaryKey: true,
            autoIncrement: true,
            comment: '문제 고유 ID',
        },
        question: {
            type: DataTypes.STRING(200),
            allowNull: false,
            comment: '문제',
        },
        type: {
            type: DataTypes.STRING(10),
            allowNull: false,
            defaultValue: 'choice',
            comment: '문제 유형 (choice: 객관식 / short: 주관식)',
        },
        answer: {
            type: DataTypes.STRING(100),
            allowNull: false,
            comment: '정답',
        },
        choice1: {
            type: DataTypes.STRING(100),
            allowNull: true,
            comment: '객관식 1',
        },
        choice2: {
            type: DataTypes.STRING(100),
            allowNull: true,
            comment: '객관식 2',
        },
        choice3: {
            type: DataTypes.STRING(100),
            allowNull: true,
            comment: '객관식 3',
        },
        choice4: {
            type: DataTypes.STRING(100),
            allowNull: true,
            comment: '객관식 4',
        },
        choice5: {
            type: DataTypes.STRING(100),
            allowNull: true,
            comment: '객관식 5',
        },
    });
};

 

초기화

sequelize-cli를 이용해 설정을 진행했다면, 간단하게 sequelize를 초기화 할 수 있습니다.

const { sequelize } = require('./models/index.js');

const driver = () => {
    sequelize.sync().then(() => {
        console.log('초기화 완료.');
    }).catch((err) => {
        console.error('초기화 실패');
        console.error(err);
    });
};
driver();

sequelize.sync()는 Sequelize가 초기화 될 때 DB에 필요한 테이블을 생성하는 함수입니다. 예를 들면 quiz라는 모델이 있다면 CREATE TABLE IF NOT EXISTS `Quizzes`로 시작하는 SQL을 실행하여 모델로 정의된 테이블을 생성합니다.

 

앞서 설명한 Sequelize의 특징처럼 sync() 함수도 Promise 패턴으로 구현되어 있습니다. 따라서 async와 await를 이용해 아래와 같이 초기화 할 수도 있습니다.

const { sequelize } = require('./models/index.js');

const driver = async () => {
    try {
        await sequelize.sync();
    } catch (err) {
        console.error('초기화 실패');
        console.error(err);
        return;
    }

    console.log('초기화 완료.');
};
driver();

 

CRUD

  • CREATE
    함수 호출 한 번으로 간편하게 데이터를 입력할 수 있습니다.
    Model.create({key: 'value'})의 형식으로 입력할 데이터를 전달해주면 됩니다.

  • READ
    데이터를 불러올 때에는 보통 두 가지 방법을 사용합니다.
    Model.findAll()은 조건에 맞는 모든 데이터를 불러옵니다.
    Model.findOne()은 Model.findAll()에서 호출하는 SQL 뒤에 LIMIT 1을 붙여서 하나의 데이터만 불러옵니다.

    Model.findAll({ where: { type: 'choice' } })와 같이 검색할 조건을 전달할 수 있습니다.

  • UPDATE
    데이터를 수정할 때에는 Model.update()를 사용합니다.
    첫 번째 인자로 수정할 데이터를, 두 번째 인자로 조건을 전달하면 됩니다. 이 때, 첫 번째 인자에는 수정할 데이터만 전달하면 됩니다. 예를 들어서 퀴즈의 문제와 답 중 답만 변경해야 한다면 Model.update({ answer: 1 }, { where: { _id: 1 } })과 같이 사용할 수 있습니다.

  • DESTROY
    Model.destroy()를 이용해서 데이터를 불러오는 방법과 동일하게 삭제할 수 있습니다.
    Model.destroy({ where: { type: 'choice' } })를 실행한다면 type이 choice인 모든 데이터를 삭제합니다.

내용을 정리하면서 연습해보기 위해 작성했던 코드를 아래에 첨부합니다.
각 부분을 실행한 뒤에 콘솔에 출력되는 SQL를 보면 조금 더 쉽게 이해가 될 수 있습니다.

...더보기
const Op = require('sequelize').Op;
const { sequelize, Quiz } = require('./models/index.js');

const driver = async () => {
    // Sequelize 초기화
    try {
        await sequelize.sync();
    } catch (err) {
        console.error(err);
        return;
    }

    // CREATE
    await Quiz.create({
        question: '100의 반을 2분의 1로 나누면 얼마인가?',
        type: 'short',
        answer: '100',
    });

    await Quiz.create({
        question: '다음 중 더 무거운 것은?',
        choice1: '1톤의 금',
        choice2: '1톤의 은',
        choice3: '1톤의 동',
        choice4: '모두 같다',
        answer: 4,
    });

    await Quiz.create({
        question: '에베레스트 산이 발견되기 전에 세상에서 가장 높았던 산은?',
        choice1: '에베레스트 산',
        choice2: '킬리만자로 산',
        choice3: 'K2',
        answer: 2,
    });

    // READ
    const choices = await Quiz.findAll({ where: { type: 'choice' } });
    for (const quiz of choices) console.log(`${quiz.question} / ${quiz.answer}`);

    // UPDATE
    await Quiz.update({
        answer: 1,
    }, { where: { question: '에베레스트 산이 발견되기 전에 세상에서 가장 높았던 산은?' } });

    // DESTROY
    await Quiz.destroy({ where: { _id: { [Op.gte]: 0 } } });
};
driver();

 

참고 문서