프로그래밍(Web)/공부일기

sequelize 사용기

Bami 2023. 11. 17. 17:42
728x90
반응형

Intro

혼자서 프론트부터 백엔드까지 북치고 장구쳤던 프로젝트가 있었다.
 
그 코드 구조가 
프론트에서 ajax로 통신 -> ajax route처리하는 부분에서 호출 url에 맞는 API 함수 호출 -> DB 실행 -> 데이터 return 형태로
하나의 파일에서 호출URL에 맞는 함수 호출 부분이 전부 담겨있고, 다른 하나의 파일에선 API 호출하는 함수들을 선언하는 부분들이 담당하는 구조였는데 이 부분을 개선하여  조금 더 REST API 형태에 비슷하게 하도록 sequelize를 사용하여 구조를 변경해 보았다.
 
각 호출에 필요한 Model을 만들었고, API 호출 구조와 동일하게 디렉토리 안에 index파일을 만들어 API 개발할 때 직관적으로 개발 할 수 있도록 만들어보았다.

어떻게 사용했는가?

예를 들어 

GET localhost:3000/search/auto

라는 부분을 API로 호출해준다 했을 때
 
routes/search/index.js형태로 해서

router.get('/auto', async (req, res) => {
    try {
        const { keyword } = req.query;

        if (!keyword) {
            return res.status(400).json({ error: 'Keyword is required' });
        }

        const schools = await model.findAll({
            //
        });

        return res.status(200).json(schools);
    } catch (error) {
        console.error(error);
        return res.status(500).json({ error: 'Internal Server Error' });
    }
});

형태로 넣어주었다.

삽질했던 부분

sequelize를 사용할 때 .env 파일 때문에 조금 삽질한 경험이 있었는데 sequelize 설정하는 부분에서

module.exports = {
    database: process.env.RDB_DATABASE,
    username: process.env.RDB_USERNAME,
    password: process.env.RDB_PASSWORD,
    host: process.env.RDB_HOST,
    port: process.env.RDB_PORT,
    timezone: '+09:00'
};

요렇게 사용하였는데 process.env 파일안에 있는 값들을 불러오지 못한 것이였다.
 
처음에는 서버가 처음 실행하는 부분에 넣어주면 되겠다 생각해서
app.js 부분에

require('dotenv').config();

를 넣었음에도 데이터를 가지고 오지 못했다. 오랜 삽질 끝에 sequelize 설정하는 부분에서도 넣어줘야 한다는 걸 알게되어

require('dotenv').config()

module.exports = {
    database: process.env.RDB_DATABASE,
    username: process.env.RDB_USERNAME,
    password: process.env.RDB_PASSWORD,
    host: process.env.RDB_HOST,
    port: process.env.RDB_PORT,
    timezone: '+09:00'
};

이런식으로 넣어주니 제대로 가져오기 시작했다.
 

짧게 사용하며 느낀점

 
기존에는 한 파일 내에서 모든 작업을 다 진행하다보니 한 곳이 에러나면 그 부분을 찾기가 조금 힘들었다. 
물론 Ctrl + F를 사용하여 찾아볼 수 있었지만 확실히 지금 개선한 코드구조가 더 직관적이라서 작업하기 편했다.
 
사실 나는 ORM 형태의 코드보다는 low쿼리 형태를 선호했었다.
ORM형태의 코드 구조를 사용하려면 기존 쿼리에서 ORM 형태의 코드로 변환해야 하는 작업과 ORM 형태로 사용하기 위해 Model들을 코드 상에서 재정의 해줘야 하는 작업이 너무 번거로웠기 때문이다.
 
하지만 Sequelize를 구성하고, 사용해보면서 좋은 점을 몇 가지 느꼈었는데

1. 코드가 간결해지고, 유지보수하기 편해졌다.

low쿼리를 사용했던 코드와 Sequelize를 사용하는 코드랑 비교하며 실예를 들어보겠다. 

  // db_service.js
  async inquiry_board (out, no) {
        let sql = "SELECT * FROM board where Church_No = "+no+"";
        console.log("sql", sql)
        
        let conn =  await this.dbc.getConnection();
        let result = null;
        let error = null;
        try {
            await conn.beginTransaction(); // 트랜잭션 적용 시작
            let select_board = await conn.query(sql);
            await conn.commit(); // 커밋
            result = select_board[0];
            out(error, result);
        }catch (err) {
            error = err;
            console.log(err)
            out(error, result);
            await conn.rollback() // 롤백
            // return res.status(500).json(err)
        } finally {
            conn.release() // con 회수
        }
    }
// board/index.js

router.get('/',async (req,res) => {
    try {
        const boards = await Board.findAll();
        return res.status(200).json(boards);
    } catch (error) {
        console.error(error);
        return res.status(500).json({ error: 'Internal Server Error' });
    }
});

위 쪽은 low쿼리, 밑에 쪽은 Sequelize를 사용한 코드이다. 같은 기능을 하지만 코드의 양이 훨씬 줄어들었다.
물론, low쿼리에선 rollback()하는 기능도 들어있지만 이 부분을 제외하고 단순히 쿼리를 실행하는 부분만 보면 차이가 느껴진다.
 

2. fk가 필요할 때 코드상에서만 fk관계를 줄 수 있다.

이게 무슨 말이냐하면 실제 DB에선 FK관계를 유지보수의 문제, 삽입, 업데이트 및 삭제 작업의 성능이 저하로 잘 맺지 않는다.
하지만 작업을 하다보면 테이블 간 Join을 위해 FK관계가 필요할 때가 많다. 
Sequelize는 코드 상에서만 FK관계를 맺도록 하는 기능이 있다.

// 1:N
static associate(db) {
    db.User.hasMany(db.Comment, {foreignKey: 'commenter',sourceKey:'id'});
}

static associate(db) {
    db.Comment.belongsTo(db.User, {foreignKey: 'commenter', targetKey:'id'});
}

// 1:1
static associate(db) {
    db.User.hasOne(db.Comment, {foreignKey: 'UserId',sourceKey:'id'});
}
static associate(db) {
    db.Comment.belongsTo(db.User, {foreignKey: 'UserId', targetKey:'id'});
}

// N:M
static associate(db) {
    db.Post.belongsToMany(db.Hashtag,{through:'PostHashtag'});
}
static associate(db) {
    db.Hashtag.belongsToMany(db.Post,{through:'PostHashtag'});
}

위와 같이 관계에 따라 코드상에서만 맺을 수 있다는 게 정말 좋았다.

3. 쿼리 자체의 파라미터를 보호하기 때문에 보안적인 강점이 있다.

일반적인 SQL 쿼리를 직접 작성하여 데이터베이스에 쿼리를 실행할 때, 특히 동적 데이터를 포함하는 경우 쿼리 파라미터가 외부에 노출될 위험이 있다.

const userId = req.params.id; // 사용자가 입력한 값
const query = `SELECT * FROM users WHERE id = ${userId}`;
db.execute(query);

 

개발자가 SQL 쿼리 문자열에 변수를 직접 삽입하는 경우 그 값이 그대로 노출되면서 보안 취약점을 유발할 수 있기 때문이다.

그리고 취약점이 노출되면 SQL 인젝션과 같은 공격에 악용될 수 있기 때문에 더욱 신경을 써야 한다. 위 예시에서 userId는 사용자가 제공한 값이고, 그대로 SQL 쿼리 문자열에 삽입된다. 이 때 사용자가 의도적으로 SQL 인젝션 공격을 한다면, 예를 들어 1 OR 1=1과 같은 값을 넣으면 아래와 같은 쿼리가 만들어 질 수 있다.

SELECT * FROM users WHERE id = 1 OR 1=1;

이 경우 모든 사용자의 정보가 반환될 수 있게 된다.

 

Sequelize는 파라미터 바인딩 방식으로 동작하기 때문에 쿼리 자체와 파라미터 값을 분리하여 처리된다.

const userId = req.params.id; // 사용자가 입력한 값
const user = await User.findOne({
  where: {
    id: userId
  }
});

따라서 사용자가 어떤 값을 입력하든 SQL 쿼리 자체가 안전하게 처리된다. 파라미터가 SQL 쿼리 문자열에 직접 삽입되지 않고, ORM(객체 관계 매핑) 방식으로 처리되기 때문이다.

 

내부적으로는 아래와 같은 안전한 쿼리가 생성 되는데

SELECT * FROM users WHERE id = ?;

여기서 ?는 실행 시점에 안전하게 바인딩되는 파라미터로 처리되어, 외부에서 조작할 수 없다.

4. Sequelize에서의 유효성 검사(Validation) 처리

Sequelize는 데이터를 데이터베이스에 저장하기 전에 다양한 유효성 검사를 수행할 수 있는 기능을 내장하고 있다.

 

하지만 이렇게 유효성 검사를 추가하더라도 해당 부분은 service단에서 처리되기 때문에 SQL 인젝션으로 잘못된 파라미터의 값을 반복해서 API를 호출하여 서버의 공격을 하는 경우도 있었기 때문에  이런 경우는 router에서  따로 유효성 검사를 해주어야 한다.

 

그럼에도 불구하고 service단에서 사용되는 데이터베이스에 무결성 있는 데이터를 저장하고, 애플리케이션이 잘못된 데이터를 처리하는 것을 방지할 필요가 있는데 Sequelize가 model을 정의하는 부분에서 간단하게 지원해주고 있다.

const User = sequelize.define('User', {
  // 이름 필드: 필수 입력 및 길이 제한
  name: {
    type: DataTypes.STRING,
    allowNull: false, // 필수 필드
    validate: {
      notEmpty: true, // 빈 값 허용 안함
      len: [2, 50] // 길이: 최소 2자, 최대 50자
    }
  },
  // 이메일 필드: 필수 입력 및 이메일 형식 유효성 검사
  email: {
    type: DataTypes.STRING,
    allowNull: false,
    validate: {
      isEmail: true // 이메일 형식 검증
    }
  },
  // 나이 필드: 숫자 범위 유효성 검사
  age: {
    type: DataTypes.INTEGER,
    allowNull: true, // 필수는 아님
    validate: {
      min: 0, // 최소값 0
      max: 120 // 최대값 120
    }
  }
});

이런 기본적인 방법 외에도 아래와 같이 사용자의 커스텀 방식으로 검사할 수도 있다.

const User = sequelize.define('User', {
  username: {
    type: DataTypes.STRING,
    allowNull: false,
    validate: {
      notEmpty: true,
      isUnique: async function(value) {
        // 사용자 정의 유효성 검사: 유니크 값 체크
        const user = await User.findOne({ where: { username: value } });
        if (user) {
          throw new Error('Username already in use!');
        }
      }
    }
  }
});
728x90
반응형