Doputer

ORM과 Native Query

ORM이란

ORM(Object Relational Mapping, 객체 관계 매핑)은 데이터베이스와 객체 지향 프로그래밍 언어 간의 호환되지 않는 데이터를 변환하는 프로그래밍 기법입니다.

Object - ORM - Table(Database)과 같이 관계형 데이터베이스에서 사용하는 테이블과 객체 지향 프로그래밍에서 사용하는 클래스를 연결해줍니다.

왜 사용할까

아래는 Native Query와 ORM 코드입니다. 둘 다 같은 동작을 하지만 ORM 코드가 훨씬 간결합니다.

db.query('SELECT * FROM user LEFT JOIN school ON user.school = school.id WHERE user.age = 13');

user.findBy({
  where: { age: 13 },
  relations: { school: true },
});

ORM을 사용하면 다음과 같은 이점이있습니다.

개발자가 데이터베이스 보다 비지니스 로직에 더 집중할 수 있습니다. 개발자가 SQL Query를 작성하는데 많은 시간을 쏟지 않고, 객체에서 제공하는 메소드 등을 이용해서 개발할 수 있습니다. 또한 초기에 각 테이블 간의 관계를 객체로 매핑해놓으면 객체 관계만 확인하면 되기 때문에 개발하면서 ERD를 보는 시간이 줄어들게 됩니다.

재사용하기 좋고, 유지보수하기 편합니다. 각종 ORM 로직들은 독립적으로 작성되어 있고, 이를 다른 비지니스 로직에서 사용하는 것이 가능합니다. 또한 로직이 변경되는 경우 코드 상에서 수정할 수 있어서 편리합니다.

DBMS에 종속적이지 않습니다. 데이터베이스를 바꾸는 작업이 생겨도 ORM으로 작성된 코드는 보다 편리하게 작업할 수 있습니다. ORM에서 설정한 DBMS에 맞게 자료형을 선언하고, 쿼리를 날리기 때문에 ORM 설정만 변경하면 데이터베이스를 바꿀 수 있습니다.

ORM이 무조건 좋을까

ORM이 Native Query에 비해 느릴 수 있습니다. 일부 로직에서 ORM은 Native Query보다 많은 정보를 얻으려고, 복잡한 쿼리를 날리기도 합니다. 이런 경우 불필요한 비용이 발생합니다.

ORM도 결국에는 SQL Query에 대한 이해가 필요합니다. 서비스 복잡도가 올라가면 findOne(), findMany()와 같은 쿼리보다 복잡한 쿼리를 날릴 수밖에 없습니다. 결국에는 SQL Query에 대해 이해를 하고 있어야 ORM을 잘 사용할 수 있습니다. 성능 최적화 또한 마찬가지입니다.

어떻게 개발하는게 좋을까

ORM은 분명 편리한 도구입니다. 실제로 한 프로젝트에서는 Native Query를 날리다가 다른 프로젝트에서 ORM을 사용해보면 빠른 개발 속도를 체감할 수 있습니다.

하지만 ORM이 편의성을 높혀주기 위한 도구라는 사실을 잊어서는 안됩니다. 자신이 사용한 코드에서 어떤 일이 일어나고 있는지 이해하고 있어야 미래의 내가 고생하지 않을 수 있습니다.

협업 시에는 팀원들과 ORM을 적극적으로 사용하면서 개발하다가 성능 최적화가 필요한 부분에서 ORM에서 제공하는 QueryBuilder를 사용하거나 Native Query로 직접 수정해볼 수 있습니다.

JavaScript로 구현해본 ORM

아래는 ORM을 사용하지 않는 프로젝트에서 MySQL 간단하게 쓰기 위해 구현해본 코드입니다.

// lib/database.js

import mysql from 'mysql2/promise';

const db = await mysql.createConnection(dbConfig);

await db.query(
  `
    CREATE TABLE IF NOT EXISTS room(
      id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
      summary VARCHAR(100),
      address VARCHAR(50),
      price INT,
      cleaning_fee INT,
      discount_rate DECIMAL(3, 2),
      service_fee_rate DECIMAL(3, 2),
      lodge_fee_rate DECIMAL(3, 2),
      total_occupancy INT,
      total_bedroom INT,
      total_bathroom INT,
      latitude DECIMAL(11, 8),
      longitude DECIMAL(11, 8)
    )
  `
);

export default db;

lib/database.js에서 데이터베이스에 연결하고, 테이블이 존재하지 않으면 생성해주는 초기화 로직을 작성했습니다.

// core/model.js

import db from 'lib/database.js';
import { BadRequestError } from 'errors/error.js';

export default class Model {
  tableName;

  constructor(tableName) {
    this.tableName = tableName;
  }

  async getOne(conditions, parameters, ...relations) {
    const records = await this.getMany(conditions, parameters, ...relations);

    return records[0];
  }

  async getMany(conditions, parameters, ...relations) {
    try {
      const [records] = await db.query(
        `
        SELECT * FROM ${this.tableName}
        ${relations.join(' ')}
        ${conditions ? 'WHERE ' + conditions.join(' AND ') : ''}
      `,
        parameters
      );

      return records;
    } catch {
      throw new BadRequestError();
    }
  }

  async createOne(columns, parameters) {
    try {
      await db.query(
        `
        INSERT INTO ${this.tableName}(${columns.join(',')})
        VALUES (${Array(columns.length).fill('?').join(',')})
      `,
        parameters
      );
    } catch {
      throw new BadRequestError();
    }
  }

  async deleteOne(conditions, parameters) {
    try {
      await db.query(
        `
        DELETE FROM ${this.tableName}
        ${conditions ? 'WHERE ' + conditions.join(' AND ') : ''}
      `,
        parameters
      );
    } catch {
      throw new BadRequestError();
    }
  }
}

이후 core/model.js에서 기본적인 CRUD를 할 수 있는 로직을 추상화해서 구현했습니다.

// models/roomModel.js

import Model from 'core/model.js';

export default class RoomModel extends Model {
  constructor() {
    super('room');
  }
}

// services/roomService.js

import RoomModel from 'models/roomModel.js';

const rooms = await roomModel.getMany(
  conditions,
  parameters,
  'LEFT OUTER JOIN room_option ON room.id = room_option.room_id'
);

이후에는 원하는 모델 클래스를 만든 후 core/model.js를 이용해서 상속해주면 됩니다.

어디까지나 Native Query를 편하게 작성하기 위해 ORM과 유사하게 구현해본 코드이기 때문에 실제 ORM에 비해서 많이 복잡합니다. model.js의 코드를 더욱 잘게 쪼게서 Method Chaning으로 동작할 수 있게 구현했다면 실제 ORM과 더욱 유사했을 것 같습니다.

ⓒ 2024. 김도현. All Rights Reserved.