테스트주도개발(TDD)로 만드는 NodeJS API 서버 - 006

6 분 소요

데이터베이스

데이터베이스 소개

SQL

  • MySQL, PostgreSQL, Aurora, Sqlite

NoSQL : json 형식

  • MongoDB, DynamoDB

In Memory DB

  • Redis, Memcashed

ORM 소개

쿼리

  • insert table(‘name’) value(‘alice’);
  • select * from users;
  • update users set name=’beck’ where id=1;
  • delete from users where id=1;

ORM(Object Relational Mapping)

  • 데이터베이스를 객체로 추상화해 놓은 것
  • 쿼리를 직접 작성하는 대신 ORM 의 메소드로 데이터 관리할 수 있음
  • 노드에서 SQL ORM은 시퀄라이져(Sequelize)가 있음

https://sequelize.org/master/

노드의 ORM 시퀄라이져

쿼리문 대신 메소드를 사용

  • insert table(‘name’) value(‘alice’);
    -> User.create({name: ‘alice’})
  • select * from users;
    -> User.findAll();
  • update users set name=’beck’ where id=1;
    -> User.update({name: ‘beck’}, {where:{id:1}});
  • delete from users where id=1;
    -> destory({where:{id:1}});

User 는 모델임

모델

  • 데이터베이스 테이블을 ORM 으로 추상화한 것
  • User 모델
    • sequelize.define(): 모델 정의
    • sequelize.sync() : 데이터베이스 연동

모델 정의

sequelize, sqlite3 설치

# sequelize, sqlite3 설치
$ npm i sequelize sqlite3 --save

node-api/models.js

// node-api/models.js
const { Sequelize, Model, DataTypes } = require('sequelize');

const sequelize = new Sequelize({
  dialect: 'sqlite',
  storage: './db.sqlite'
});

class User extends Model { }
User.init({
  name: DataTypes.STRING,
}, { sequelize, modelName: 'user' });

module.exports = { Sequelize, Model, User, sequelize };

데이터베이스 - ORM 동기화

node-api/bin/sync-db.js

const { Sequelize, Model, User, sequelize } = require('../models');

// 원래의 DB를 삭제하고 새로 생성
sequelize.sync({ force: true });

module.exports = () => {
  return sequelize.sync({ force: true });
}

sync-db 실행

$ node bin/sync-db.js

node-api/bin/www.js

const app = require('../index');
const syncDb = require('./sync-db');

syncDb().then( () => {
  console.log('Sync database!');
  app.listen(3000, () => {
    console.log('Server is running on 3000 port');
  });
});

데이터베이스와 index 컨트롤러 연동 1

API 로직인 user.ctrl.js 에서 모델을 연동하여 디비를 연결

describe, it 에 only() 함수로 해당 테스크 테이스만 실행할 수 있음

node-api/api/user/user.spec.js

// node-api/api/user/user.spec.js
const app = require('../../index.js');
const request = require('supertest');
const should = require('should');
const { User, sequelize } = require('../../models');

describe('GET /users는', () => {
  describe('성공시', () => {
    before(() => {
      return sequelize.sync({ force: true }); // create table
    });
    it.only('유저 객체를 담은 배열로 응답한다', (done) => { 
      ...
    })
  })
})

node-api/api/user/user.ctrl.js

// node-api/api/user/user.ctrl.js
const { User } = require('../../models');

const index = (req, res) => {
  req.query.limit = req.query.limit || 10;
  const limit = parseInt(req.query.limit, 10);
  if (Number.isNaN(limit)) {
    return res.status(400).end();
  }

  User.findAll({
    limit: limit
  }).then(users => {
    res.json(users);
  });
}
...

데이터베이스와 index 컨트롤러 연동 2

데이터베이스 초기화 (샘플 데이터 등록)

only 이동

node-api/api/user/user.spec.js

...
const app = require('../../index');
const models = require('../../models');

describe.only('GET /users는', () => {
  describe('성공시', () => {
    const users = [
      { name: 'alice' },
      { name: 'beck' },
      { name: 'chris' },
    ]
    before(() => {
      return sequelize.sync({ force: true }); // create table
    });
    before(() => {
      return User.bulkCreate(users);
    });
    it('유저 객체를 담은 배열로 응답한다', (done) => { // only 로 해당 테스크 테이스만 실행할 수 있음
      ...
    })
  })
})

node-api/api/user/user.ctrl.js

// node-api/api/user/user.ctrl.js
const { User } = require('../../models');

const index = function(req, res) {
  req.query.limite = req.query.limit(2);
  const limit = parseInt(req.query.limit, 10);
  if(Number.isNaN(limit)) {
    return res.status(400).end();
  }

  User.findAll({ 
    limit: limit
  })
    .then(users => {
      res.json(users);
    })
}

테스트 결과에 쿼리문 로그를 숨김

node-api/models.js

...
const sequelize = new Seqielize({
  dialect: 'sqlite',
  storage: './db.sqlite',
  logging: false, //console.log
})
...

데이터베이스와 show 컨트롤러 연동

only 이동

node-api/api/user/user.spec.js

...
describe.only('GET /users/:id는', () => {
  ...
})
...

node-api/api/user/user.ctrl.js

// node-api/api/user/user.ctrl.js
const { User } = require('../../models');

const index = function(req, res) {
  ...
}

const show = (req, res) => {
  const id = parseInt(req.params.id, 10);
  if (Number.isNaN(id)) {
    return res.status(400).end();
  }

  User.findOne({
    where: {
      id: id
    }
  }).then(user => {
    if (!user) return res.status(404).end();
    res.json(user);
  })
}
...

데이터베이스와 destroy 컨트롤러 연동

only 이동

node-api/api/user/user.spec.js

...
describe.delete('DELETE /users/:id는', () => {
  ...
})
...

node-api/api/user/user.ctrl.js

// node-api/api/user/user.ctrl.js
const { User } = require('../../models');

const index = function(req, res) {
  ...
}

const show = function(req, res) {
  ...
}

const destroy = (req, res) => {
  const id = parseInt(req.params.id, 10);
  if (Number.isNaN(id)) {
    return res.status(400).end();
  }
  User.destroy({
    where: {
      id: id
    }
  }).then(() => {
    res.status(204).end();
  });
}
...

데이터베이스와 create 컨트롤러 연동

only 이동

각 상위 describe 에 before 복사

node-api/api/user/user.spec.js

// node-api/api/user/user.spec.js
...
describe.only('POST /users', () => {
  const users = [
    {name: 'alice'},
    {name: 'beck'},
    {name: 'chris'},
  ]
  before(( ) => {
    return models.sequelize.sync({force: true}); // create table
  });
  before((done) => {
    return models.User.bulkCreate(users); // insert data
  });
  ...
})
...

user.name 에 unique 설정

node-api/models.js

// node-api/models.js
const { Sequelize, Model, DataTypes } = require('sequelize');

const sequelize = new Sequelize({
  dialect: 'sqlite',
  storage: './db.sqlite',
  logging: false,
});

class User extends Model { }
User.init({
  name: {
    type: DataTypes.STRING,
    unique: true
  }
}, { sequelize, modelName: 'user' });

module.exports = { Sequelize, Model, User, sequelize };

node-api/api/user/user.ctrl.js

// node-api/api/user/user.ctrl.js
const models = require('../../models');

const index = function(req, res) {
  ...
}
const show = function(req, res) {
  ...
}
const destroy = (req, res) => {
  ...
}
const create = (req, res) => {
  const name = req.body.name;
  if (!name) {
    return res.status(400).end();
  }
  User.create({ name }).then((user) => {
    return res.status(201).json(user);
  }).catch((err) => {
    return res.status(409).end();
  });
}
...

데이터베이스와 update 컨트롤러 연동

마지막 테스트케이스이므로 only 삭제

before 복사

node-api/api/user/user.spec.js

// node-api/api/user/user.spec.js
...
describe('PUT /users/:id', () => {
  const users = [
    {name: 'alice'},
    {name: 'beck'},
    {name: 'chris'},
  ]
  before(( ) => {
    return models.sequelize.sync({force: true}); // create table
  });
  before((done) => {
    return models.User.bulkCreate(users); // insert data
  });
  ...
});
...

node-api/api/user/user.ctrl.js

// node-api/api/user/user.ctrl.js
const models = require('../../models');

const index = function(req, res) {
  ...
}

const show = function(req, res) {
  ...
}

const destroy = (req, res) => {
  ...
}

const create = (req, res) => {
  ...
}

const update = (req, res) => {
  const id = parseInt(req.params.id, 10);
  if (Number.isNaN(id)) return res.status(400).end();

  const name = req.body.name;
  if (!name) return res.status(400).end();

  User.findOne({
    where: { id }
  }).then((user) => {
    if (!user) return res.status(404).end();
    user.name = name;
    user.save().then(() => {
      res.json(user);
    }).catch((err) => {
      if (err.name === 'SequelizeUniqueConstraintError') {
        return res.status(409).end();
      }
    });
  });
}

module.exports = {
  index: index,
  show: show,
  destroy: destroy,
  create: create,
  update: update,
};

마무리

실제 데이터 확인

# 기존 데이터 삭제
$ rm db.sqlite

postman 을 통해 테스트 진행

데이터베이스가 test 상에서만 강제로 초기화되도록 설정

node-api/sync-db.js

// node-api/sync-db.js
const models = require('../models');

module.exports = () => {
  const options = {
    force: process.env.NODE_ENV === 'test' ? true : false
  };
  return models.sequelize.sync(options);
}

참고