목차




마이그레이션

정의

저는 sequelize로 ORMObject Relational Mapping을 입문했습니다. 이들의 폴더구조인 테이블 스키마가 있는 models, 마이그레이션 파일이 있는 migrations, 가짜 데이터가 있는 seeders로 구성되어 있었습니다. migrationsmodels와 거의 일치하는 코드인데 함수나 클래스 안에 up, down 메서드가 있는 것 말고는 딱히 차이가 없어 보였습니다.

별 차이가 없음에도 modelsmigrations에 같은 코드를 2번이나 쳐야 하는 것은 불필요한 행동이라고 생각하고 있었습니다. 아래는 sequelize 공식문서에 언급된 migration에 대한 정의입니다.

소스 코드의 변화를 관리하기 위한 git같은 VCSVersion Control System처럼 데이터베이스의 변화를 감지해 migration을 사용해 기록할 수 있습니다. migration으로 데이터베이스로 다른 상태를 옮길 수 있고 반대로도 할 수 있습니다. 이런 상태이동은 새로운 상태를 어떻게 얻을 수 있는지, 어떻게 예전 상태로 되돌리기 위해 취소할 수 있는지를 기술한 migration 파일들에 저장됩니다.

처음 migration을 접했을 때는 몇 번을 읽어봐도 와닿지 않았습니다. 테이블 스키마를 직접 수정하면 바로 데이터베이스에 수정사항이 반영되는 것을 굳이 migration 기능을 사용할 필요까진 없다고 생각했습니다. 하지만 그것은 오산이었습니다. 실제 서비스가 돌고 있는 예시를 보니 납득이 갔습니다. 아래와 같은 SQLStructured Query Language문으로 테이블을 하나 만듭니다.

CREATE TABLE People (
  id INT NOT NULL AUTO_INCREMENT,
  first_name VARCHAR(255) NOT NULL,
  last_name VARCHAR(255) NOT NULL,
  city VARCHAR(255),
  PRIMARY KEY (id)
);

INSERT INTO People 
  (first_name, last_name, city) 
VALUES 
  ('John', 'Doe', 'Berlin'),
  ('Warwick', 'Hawkins', 'Dublin'),
  ('Kobi', 'Villarreal', 'Peking'),
  ('Winnie', 'Roach', 'Ulaanbaatar'),
  ('Peggy', 'Nguyen', 'Hanoi');

테이블에 SELECT 쿼리를 던져주면 아래와 같은 결과가 나옵니다.

mysql> SELECT * FROM People;
+----+------------+------------+-------------+
| id | first_name | last_name  | city        |
+----+------------+------------+-------------+
|  1 | John       | Doe        | Berlin      |
|  2 | Warwick    | Hawkins    | Dublin      |
|  3 | Kobi       | Villarreal | Peking      |
|  4 | Winnie     | Roach      | Ulaanbaatar |
|  5 | Peggy      | Nguyen     | Hanoi       |
+----+------------+------------+-------------+
5 rows in set (0.00 sec)

여기서 People.citycountry로 변경하고 싶은 경우가 있을 것입니다. 칼럼명을 바꾸되 바뀐 칼럼 안에 있는 데이터는 날아가면 절대 안됩니다. 그럼에도 저는 테이블 스키마를 바로 수정하면 될 것 같다고 생각했습니다. ORM에서 작성한 스키마를 데이터베이스에 동기화하는 방법으로 가장 쉬운 방법은 synchronize가 있습니다. 애플리케이션을 재시작할 때마다 기존 테이블에서 열을 추가, 삭제하는 동작을 할 수 있습니다.

아래는 sequelize, typeorm에서 프로그램을 재실행하면 자동으로 데이터베이스에 동기화할 수 있도록 도와주는 메서드들의 사용방법입니다.

// using sequelize
await db.sequelize.sync({ alter: true })

// using typeorm
import {createConnection, getConnection} from 'typeorm'
const connection = await createConnection(options)
await getConnection().synchronize()

테이블 스키마를 People.city에서 country로 수정하고 코드를 저장하면, 변경한 칼럼에 들어있는 데이터가 날아가버리고 맙니다. 각각 synchronize를 켠 상태에서는 다음과 같이 SQL 쿼리문을 날리는 것 같습니다.

ALTER TABLE People DROP COLUMN city;
ALTER TABLE People ADD country VARCHAR(255);
mysql> SELECT * FROM People;
+----+------------+------------+---------+
| id | first_name | last_name  | country |
+----+------------+------------+---------+
|  1 | John       | Doe        | NULL    |
|  2 | Warwick    | Hawkins    | NULL    |
|  3 | Kobi       | Villarreal | NULL    |
|  4 | Winnie     | Roach      | NULL    |
|  5 | Peggy      | Nguyen     | NULL    |
+----+------------+------------+---------+
5 rows in set (0.00 sec)

하지만 migration을 사용하면 아래와 같이 쿼리문을 날립니다.

ALTER TABLE People CHANGE COLUMN city country VARCHAR(255);
mysql> SELECT * FROM People;
+----+------------+------------+-------------+
| id | first_name | last_name  | country     |
+----+------------+------------+-------------+
|  1 | John       | Doe        | Berlin      |
|  2 | Warwick    | Hawkins    | Dublin      |
|  3 | Kobi       | Villarreal | Peking      |
|  4 | Winnie     | Roach      | Ulaanbaatar |
|  5 | Peggy      | Nguyen     | Hanoi       |
+----+------------+------------+-------------+
5 rows in set (0.01 sec)

synchronize는 최초에 데이터와 테이블 스키마를 동기화할 때는 좋은 옵션이지만 프로덕션에는 안전하지 않습니다. 위같은 간단한 쿼리는 어느정도 개발하는 입장에서 예상이 가능하지만, association이 엮이는 경우에는 나같은 초보개발자는 synchronize를 해서 오는 사이드이펙트를 가늠하지 못할 것입니다. 라이브 환경에서 데이터가 날아가는 일은 끔찍합니다. 라이브 환경에서라면 데이터베이스를 안정적으로 관리하기 위한 도구인 migration을 적극 사용하는 것을 ORM 공식문서에서 하나같이 권장합니다.




사용법

데이터베이스 및 config 파일 세팅

여기서는 다중 환경을 사용하지 않는다는 가정 하에 typeorm에서 기본적으로 제공해주는 ormconfig.json 파일을 사용할 예정입니다. --name 플래그는 새로 만들 프로젝트 이름을, --database는 데이터베이스 이름을 적어줍니다.

npx typeorm init --name test-project --database test-database mysql

새로운 프로젝트 폴더가 만들어질 것입니다. 진입해서 의존성 모듈들을 설치합니다.

cd test-project && yarn

아래와 같이 ormconfig.json를 수정합니다.

"username": "root",
"password": "root",
"database": "test-database",
"synchronize": false
"logging": true

아래와 같이 package.json에서 scripts에 아래 스크립트를 추가합니다.

"typeorm": "ts-node ./node_modules/typeorm/cli -f ./ormconfig.json"

이제 docker 컨테이너로 mysql 컨테이너를 띄워야 합니다. 아래와 같은 내용으로 docker-compose.yml을 루트에 만듭니다.

version: '3.8'
services:
  mysql:
    image: mysql:5.7
    volumes:
      - ./initdb:/docker-entrypoint-initdb.d/
    command:
      - --default-authentication-plugin=mysql_native_password
      - --character-set-server=utf8mb4
      - --collation-server=utf8mb4_unicode_ci
    restart: always
    ports:
      - 3306:3306
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: test-database
      MYSQL_USER: root
      MYSQL_PASSWORD: root

아직 끝나지 않았습니다. 데이터베이스를 초기화하는 작업을 하려면 컨테이너 내의 docker-entrypoint-initdb.d.sql 파일을 집어넣어줘야 합니다. 아래와 같이 파일을 만듭니다.

mkdir initdb && touch initdb/init.sql

파일에는 다음과 같이 쿼리문을 작성합니다.

SET NAMES utf8;

CREATE DATABASE IF NOT EXISTS `test-database`;
SET character_set_client = utf8mb4;

USE `test-database`;

ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY 'test';
ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'test';
SELECT plugin FROM mysql.user WHERE User = 'root';
FLUSH PRIVILEGES;

이제 컨테이너를 실행하면 데이터베이스 세팅은 끝납니다.

docker-compose up




migration:create

yarn typeorm migration:create -n test-migration-create

참고로 -n 플래그는 migration 파일의 이름을 정해줍니다.

빈 껍데기인 migration 파일을 만들때 사용합니다. 스크립트를 실행하면 ormconfig.json에서 cli.migrationsDir에 정의한 경로에 timestamp-test-migration-create.ts와 같이 timestamp를 포함한 파일명으로 up, down 메서드에 구현부는 비어있는 파일이 아래처럼 생성됩니다.

import {MigrationInterface, QueryRunner} from 'typeorm'

export class test-migration-create1605840315914 implements MigrationInterface {
  async up(queryRunner: QueryRunner): Promise<void> {}
  async down(queryRunner: QueryRunner): Promise<void> {}
}

메서드 up은 migration을 실행하기 위해 필요한 코드를 적어야 합니다. down은 지난 migration을 할 때 사용했던 up에서 변경된 것들을 되돌리기 위해 사용해야 합니다. 위에서 언급했던 People.citycountry로 바꾸려면 아래와 같이 작성할 수 있습니다.

import {MigrationInterface, QueryRunner} from 'typeorm'

export class test-migration-create1605840315914 implements MigrationInterface {
  async up(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.query(`ALTER TABLE People CHANGE COLUMN city country varchar(255)`)
  }
  async down(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.query(`ALTER TABLE People CHANGE COLUMN country city varchar(255)`)
  }
}

다시 한 번 말하자면 migration:create은 빈 껍데기만 만들어주기 때문에 구현부는 직접 작성해야 합니다.

migration:generate

ormconfig.json에서 정의한 entities에 있는 경로에 있는 스키마의 변경사항들을 감지해서 migration 파일을 생성해주는 기능을 합니다. 단, 변경사항이 있어야지만 동작하고 새로운 migration 파일을 만들어줍니다.

아래와 같이 People.ts를 정의합니다.

import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm'

@Entity()
export class People {
  @PrimaryGeneratedColumn()
  id: number
  
  @Column({ length: 255, nullable: false })
  first_name: string
  
  @Column({ length: 255, nullable: false })
  last_name: string
  
  @Column({ length: 255 })
  city: string
}

위와 같은 스키마가 데이터베이스에 이미 동기화 되어있는채로 아래와 같은 명령을 날리면 아무런 변화가 없다고 로그가 찍힙니다. 수정을 했는데도 불구하고 아래 로그가 찍힌다면 config 파일을 제대로 연결하지 않았을 경우에 발생하기도 하니 확인해보는 것이 좋습니다.

yarn typeorm migration:generate -n test-migration-generate

No changes in database schema were found - cannot generate a migration. To create a new empty migration use “typeorm migration:create” command

자, 그럼 스키마를 수정해볼까요? People.citycountry로 아래와 같이 변경합니다.

// before
@Column({ length: 255 })
city: string

// after
@Column({ length: 255 })
country: string

다시 아래처럼 migration:generate 스크립트를 날려주면 timestamp-test-migration-generate.ts 파일이 생성된 것을 확인할 수 있습니다.

yarn typeorm migration:generate -n test-migration-generate

만들어진 migration 파일을 열어보면 아래와 같이 쿼리가 자동으로 입력되어있는 것을 확인할 수 있습니다.

import {MigrationInterface, QueryRunner} from 'typeorm'

export class test-migration-generate1605840315915 implements MigrationInterface {
  async up(queryRunner: QueryRunner): Promise<void> {
      await queryRunner.query(`ALTER TABLE People CHANGE city country varchar(255);`)
  }
  async down(queryRunner: QueryRunner): Promise<void> {
      await queryRunner.query(`ALTER TABLE People CHANGE country city varchar(255);`)
  }
}

migration:run

migration:run은 모든 migration파일들을 데이터베이스에 한꺼번에 반영합니다.

yarn typeorm migration:run

그와 동시에 migrations 테이블에 커밋로그처럼 파일명이 쌓이게 됩니다. migration:createmigration:generate를 해서 migration 파일이 2개라서 아래처럼 migrations 테이블에 기록됩니다.

mysql> SELECT * FROM migrations;
+----+---------------+-------------------------------------+
| id | timestamp     | name                                |
+----+---------------+-------------------------------------+
| 1 | 1605840315914 | test-migration-create1605840315914   |
| 2 | 1605840315915 | test-migration-generate1605840315915 |
+----+---------------+-------------------------------------+
2 row in set (0.00 sec)

다시 한 번 강조하자면 migration:run은 모든 migration파일들의 up 메서드를 실행합니다. up 메서드의 구현부가 중복된 내용이라도 그냥 실행합니다.

migration:revert

migration:run을 통해 동기화한 내용들을 하나씩 걷어내는 역할을 합니다. 가장 마지막에 쌓인 migration부터 스택처럼 down 메서드를 실행합니다. 아직까지는 migration:revert:all같은 솔루션은 없습니다.

yarn typeorm migration:revert

migration:revert를 한 번 실행하면 마지막 열이 하나 떨어져 나가서, 열이 하나만 남는 것을 확인할 수 있습니다.

mysql> SELECT * FROM migrations;
+----+---------------+-------------------------------------+
| id | timestamp     | name                                |
+----+---------------+-------------------------------------+
| 1 | 1605840315914 | test-migration-create1605840315914   |
+----+---------------+-------------------------------------+
1 row in set (0.00 sec)




TypeORM vs. Sequelize

sequelize에서 제공하는 migration은 아쉽게도 typeorm에서 제공하는 entities의 변화를 자동감지해서 migration하는 기능은 가지고 있지 않습니다. sequelize의 migration:generate 커맨드는 typeorm의 migration:create와 같다. typeorm에서는 entities의 변경사항을 서버를 실행하지 않고 cli로만 synchronize시키는 schema:sync도 제공합니다. 다만 조심해서 사용해야 합니다.

반대로 typeorm에서는 되지 않는 migration:revert:all을 sequelize에서는 db:migrate:undo:all을 사용해서 모든 migration 파일들의 down 메서드를 실행할 수 있습니다.

sequelize는 seeding을 cli에서 지원해줘서 정해진 인터페이스에 맞는 데이터들만 up, down 메서드에 아래와 같이 집어넣어주면 손쉽게 사용할 수 있습니다.

import People from 'src/seeders/People'

export default {
  up: (queryInterface, Sequelize) => {
    return queryInterface.bulkInsert('People', People)
  },
  down: (queryInterface, Sequelize) => {
    return queryInterface.bulkDelete('People', null, {})
  }
}

반면에 typeorm을 사용할때 seeding을 하려면 커넥션을 직접 연 다음 아래처럼 구현해야 하는 불편함이 있습니다.

export default async function seedPeople(numFake = 10) {
  const entities = await Promise.all([Array(numFake).fill(0).map(fakeUser)])
  
  await People.insert(entities)
}

typeorm의 장점은 다음과 같습니다.

  • 테이블 스키마가 바뀐만큼 migration 파일로 만들 수 있다.
  • 서버 실행 없이 cli만으로 테이블 스키마의 변화를 synchronize할 수 있다.

sequelize의 장점은 다음과 같습니다.

  • migration:undo:all을 실행할 수 있어 migration을 모두 되돌릴 때 편하다.
  • seeding을 cli에서 지원해서 간편하게 up, down할 수 있다.




타언어 ORM과 비교

Doctrine (PHP)

php의 doctrine은 다음과 같은 특징을 가지고 있습니다.

  • 테이블 스키마의 변화를 자동감지해서 migration 파일 생성하는 기능을 제공한다.
  • sequelize의 umzug처럼 migration hook이 있어서 cli용 플러그인을 만들기 용이하다.

Active record (Ruby)

ruby의 active record은 다음과 같은 특징을 가지고 있습니다.

  • ror의 그 active record가 맞다.
  • 테이블 스키마의 변화를 자동감지해서 migration 파일 생성하는 기능을 제공한다.
  • timestamp를 ISOInternational Organization for Standardization 포맷인 YYYYMMDDHHMMS로 찍어 파일명에 표기한다. (예: 20201120120000_test-migration-create.rb)




N+1 문제

정의 & 해결방법

위에서 만들었던 테이블인 People을 조금 수정하고 Companies 테이블을 아래 쿼리로 새로 만들어봅시다. CompaniesPeople은 1:M 관계입니다.

CREATE TABLE Companies (
  id INT NOT NULL AUTO_INCREMENT,
  department VARCHAR(255) NOT NULL,
  PRIMARY KEY (id)
);

CREATE TABLE People ( 
  id INT NOT NULL AUTO_INCREMENT,
  first_name VARCHAR(255) NOT NULL,
  last_name VARCHAR(255) NOT NULL,
  city VARCHAR(255),
  company_id INT, 
  INDEX comp_idx (company_id), 
  FOREIGN KEY (company_id) REFERENCES Companies(id) ON DELETE CASCADE,
  PRIMARY KEY (id)
);

아래와 같이 sql로 CompaniesPeople에 데이터를 집어넣어줍니다.

INSERT INTO Companies
  (department)
VALUES
  ('finance'),
  ('marketing'),
  ('development'),
  ('design'),
  ('planning');

INSERT INTO People 
  (first_name, last_name, city, company_id) 
VALUES 
  ('John', 'Doe', 'Berlin', 1),
  ('Warwick', 'Hawkins', 'Dublin', 1),
  ('Kobi', 'Villarreal', 'Peking', 2),
  ('Winnie', 'Roach', 'Ulaanbaatar', 3),
  ('Peggy', 'Nguyen', 'Hanoi', 5);

테이블에 SELECT문을 던져주면 아래와 같은 결과가 나옵니다.

mysql> SELECT * FROM Companies;
+----+-------------+
| id | department  |
+----+-------------+
|  1 | finance     |
|  2 | marketing   |
|  3 | development |
|  4 | design      |
|  5 | planning    |
+----+-------------+
5 rows in set (0.00 sec)

mysql> SELECT * FROM People;
+----+------------+------------+-------------+------------+
| id | first_name | last_name  | city        | company_id |
+----+------------+------------+-------------+------------+
|  1 | John       | Doe        | Berlin      |          1 |
|  2 | Warwick    | Hawkins    | Dublin      |          1 |
|  3 | Kobi       | Villarreal | Peking      |          2 |
|  4 | Winnie     | Roach      | Ulaanbaatar |          3 |
|  5 | Peggy      | Nguyen      | Hanoi       |          5 |
+----+------------+------------+-------------+------------+
5 rows in set (0.00 sec)

서론이 너무 길었네요. 본론으로 넘어가서 N+1 문제는 ORM 사용 중 성능 문제가 생긴다면 이것 때문일 가능성이 높습니다. 이런 쿼리가 있다고 가정해볼까요? People을 가지고 부모인 Companies.department를 알아내려고 합니다. 아래 의사코드처럼 작성한다면 N+1 문제가 발생하게 됩니다.

const people = await People.query(`SELECT * FROM People`)

for (let person of people) {
  const department = await Companies.query(`
    SELECT department 
    FROM Companies c
    WHERE c.id = :personId
  `)
  .setParam('personId', person.id)
}

순서대로 어떤 SQL 쿼리가 들어갔는지 보자면 아래와 같습니다.

SELECT * FROM People;

SELECT department FROM Companies c WHERE c.id = 1; -- finance
SELECT department FROM Companies c WHERE c.id = 1; -- finance
SELECT department FROM Companies c WHERE c.id = 2; -- marketing
SELECT department FROM Companies c WHERE c.id = 3; -- development
SELECT department FROM Companies c WHERE c.id = 5; -- planning

N+1이란 최초의 쿼리를 던진 다음 아래 실행된 Companies에서 SELECT하는 문장만큼을 N이라고 해서 쿼리가 총 6(1+5)번 일어나는 것을 보고 N+1 문제라고 합니다.

해당 문제를 고치는 방법은 아주 간단합니다. INNER JOIN으로 쿼리를 날리면 해결이 가능하다. JOININNER JOIN의 별칭입니다.

const people = await People.query(`
  SELECT * 
  FROM People p
  JOIN Companies c
  ON p.id = c.id
`)

for (let person of people) {
  const department = person.company.department
}

people에서 던진 쿼리의 결과는 아래와 같습니다.

mysql> SELECT * FROM People p JOIN Companies c ON p.id = c.id;
+----+------------+------------+-------------+------------+----+-------------+
| id | first_name | last_name  | city        | company_id | id | department  |
+----+------------+------------+-------------+------------+----+-------------+
|  1 | John       | Doe        | Berlin      |          1 |  1 | finance     |
|  2 | Warwick    | Hawkins    | Dublin      |          1 |  2 | marketing   |
|  3 | Kobi       | Villarreal | Peking      |          2 |  3 | development |
|  4 | Winnie     | Roach      | Ulaanbaatar |          3 |  4 | design      |
|  5 | Peggy      | Nguyen     | Hanoi       |          5 |  5 | planning    |
+----+------------+------------+-------------+------------+----+-------------+
5 rows in set (0.00 sec)

Eager loading

데이터베이스로부터 데이터를 가져올때 가능한 적은 쿼리를 날리기 위해 아래처럼 JOIN을 사용하는 것을 eager loading이라고 합니다.

const people = await People.query(`
  SELECT * 
  FROM People p
  JOIN Companies c
  ON p.id = c.id
`)

초기 로딩 시간이 보다 길기때문에 불필요한 데이터를 너무 많이 로드하면 성능이 영향을 끼칠 수도 있습니다. 쇼핑몰에서 배송정보를 한 화면에 주문상세, 배송지정보까지 한꺼번에 보여줘야 하는 경우를 가정해볼까요. 주문을 관리하는 부모 테이블Orders의 자식 테이블인 OrderDetailsDelivery을 한꺼번에 로딩하는 것이 N+1 문제를 일으키지 않기때문에 eager loading을 사용할 수 있습니다.

Lazy loading

위에서 JOIN을 사용하지 않고 반복문 안에서 아래처럼 N+1번 쿼리를 날리는 케이스를 보고 지연로딩 혹은 lazy loading이라고 합니다.

const department = await Companies.query(`
    SELECT department 
    FROM Companies c
    WHERE c.id = :personId
  `)
  .setParam('personId', person.id)

초기 로딩 시간을 줄일 수 있고, 자원 소비를 줄일 수 있다는 장점이 있습니다. 사용하지 않는 데이터를 결과 객체에 포함시키지 않기때문에 cpu 타임을 절약할 수 있는 반면, 그 결과 데이터베이스로 더 많은 쿼리를 날리게 됩니다. 뿐만 아니라 원치 않는 순간에 성능에 영향을 줄 수도 있습니다.

구체적인 사용 사례로는 sns에서 댓글 더보기 버튼을 누르는 경우, eager loading을 사용하는 경우 댓글 더보기를 누르지 않았는데도 이미 댓글을 조회해버리기 때문에 성능상 이슈가 생길 수 있습니다. 이 때는 댓글 더보기를 클릭했을 때 댓글 목록을 호출하도록 하는 lazy loading을 사용할 수 있습니다.




TypeORM vs. Sequelize

typeorm은 스키마 선언부에서 eager loading을 할지 결정할 수 있습니다.

// src/entities/People.ts
@ManyToOne(type => Company, { eager: true })
@JoinColumn()
company: Company

lazy loading을 스키마 선언부에서 타입에 Promise generic type으로 사용할 수는 있지만 실험기능이라 권장하지는 않는다고 합니다.

@ManyToOne(type => Company)
@JoinColumn()
company: Promise<Company>

sequelize는 find* 메서드에 옵션으로 include를 아래처럼 추가해줘야 eager loading을 할 수 있습니다.

const people = await People.findOne({ include: Companies, where: { id: 1 } })

그럼 JOIN을 한 것과 같이 아래의 결과가 나옵니다.

{
  "id": 1,
  "first_name": "John",
  "last_name": "Doe",
  "city": "Berlin",
  "company": {
    "id": 1,
    "department": "finance"
  }
}

반대로 lazy loading같은 경우에는 include 옵션을 사용하지 않으면 가능합니다.




타언어 ORM과 비교

CakeORM (PHP)

eager loading은 아래와 같이 구현한다. contain이라는 예약어를 사용합니다.

$category = $this->Category->get(1, [
    'contain' => [
        'Posts'
    ]
]);
$category->posts

lazy loading은 아래와 같이 구현합니다.

$category = $this->Category->get(1);
$category->posts

JPA (Java)

eager loading은 아래와 같이 구현합니다. FetchType.EAGER란 상수를 사용합니다.

@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "post_id", nullable = false)
private Post post;

lazy loading은 아래와 같이 구현합니다. FetchType.LAZY란 상수를 사용합니다.

@OneToMany(mappedBy = "post", fetch = FetchType.LAZY) 
private List<Comment> commentList = new ArrayList<>();

Active Record (Ruby)

eager loading은 아래와 같이 구현합니다. sequelize와 비슷하게 includes라는 메서드를 추가합니다.

@products = Product.all.includes(:variants)

lazy loading은 아래와 같이 구현합니다.

@product = Product.find(params[:id])




같은 주제의 슬라이드 쉐어 링크 첨부합니다. 오탈자가 있거나 지적해주실 내용이 있다면 댓글 달아주세요!