MikroORM Seeder·Factory 패턴

MikroORM Seeder란?

MikroORM의 @mikro-orm/seeder 패키지는 데이터베이스 초기 데이터와 테스트 픽스처를 코드로 관리하는 공식 도구입니다. Factory 패턴으로 엔티티를 생성하고, Seeder 클래스로 실행 순서를 제어합니다. 개발 환경 초기화, E2E 테스트 데이터 준비, 데모 환경 구성에 필수적인 패턴입니다.

1. 설치와 설정

npm install @mikro-orm/seeder
npm install -D @faker-js/faker
// mikro-orm.config.ts
import { defineConfig } from '@mikro-orm/postgresql';
import { SeedManager } from '@mikro-orm/seeder';

export default defineConfig({
  // ...기본 설정
  extensions: [SeedManager],
  seeder: {
    path: './src/seeders',        // Seeder 파일 경로
    pathTs: './src/seeders',      // TS 경로
    defaultSeeder: 'DatabaseSeeder',
    glob: '!(*.d).{js,ts}',
    emit: 'ts',
    fileName: (className: string) => className,
  },
});

2. Factory 정의: 엔티티 생성 템플릿

Factory는 엔티티의 기본값 템플릿을 정의합니다. Faker로 랜덤 데이터를 생성하되, 호출 시 오버라이드할 수 있습니다.

// seeders/factories/user.factory.ts
import { Factory } from '@mikro-orm/seeder';
import { faker } from '@faker-js/faker';
import { User, UserRole } from '../../entities/user.entity';

export class UserFactory extends Factory<User> {
  model = User;

  definition(): Partial<User> {
    return {
      email: faker.internet.email(),
      name: faker.person.fullName(),
      password: '$2b$10$hashedDefaultPassword',  // bcrypt hash
      role: UserRole.USER,
      isActive: true,
      createdAt: faker.date.past({ years: 1 }),
    };
  }
}

// seeders/factories/product.factory.ts
import { Factory } from '@mikro-orm/seeder';
import { faker } from '@faker-js/faker';
import { Product, ProductStatus } from '../../entities/product.entity';

export class ProductFactory extends Factory<Product> {
  model = Product;

  definition(): Partial<Product> {
    return {
      name: faker.commerce.productName(),
      description: faker.commerce.productDescription(),
      price: parseInt(faker.commerce.price({ min: 1000, max: 100000 })),
      stock: faker.number.int({ min: 0, max: 500 }),
      status: ProductStatus.ACTIVE,
      sku: faker.string.alphanumeric(8).toUpperCase(),
    };
  }
}

3. Factory 고급 패턴

// seeders/factories/order.factory.ts
export class OrderFactory extends Factory<Order> {
  model = Order;

  definition(): Partial<Order> {
    return {
      orderNumber: `ORD-${faker.string.numeric(8)}`,
      status: OrderStatus.PENDING,
      total: 0,  // items에서 계산
      shippingAddress: faker.location.streetAddress({ useFullAddress: true }),
      createdAt: faker.date.recent({ days: 30 }),
    };
  }
}

// 사용 예시: 다양한 팩토리 패턴
export class DatabaseSeeder extends Seeder {

  async run(em: EntityManager): Promise<void> {
    const userFactory = new UserFactory(em);
    const productFactory = new ProductFactory(em);
    const orderFactory = new OrderFactory(em);

    // 1. 기본 생성 (5개)
    const users = await userFactory.create(5);

    // 2. 속성 오버라이드
    const admin = await userFactory.createOne({
      email: 'admin@example.com',
      role: UserRole.ADMIN,
      name: 'System Admin',
    });

    // 3. make vs create
    //    make: 메모리에만 생성 (DB 저장 안 함)
    //    create: DB에 즉시 저장
    const unsavedUser = userFactory.makeOne();

    // 4. 관계 설정과 함께 생성
    const products = await productFactory.create(20);

    for (const user of users) {
      const orderProducts = faker.helpers.arrayElements(products, 3);
      const order = await orderFactory.createOne({
        user,
        total: orderProducts.reduce((sum, p) => sum + p.price, 0),
      });

      // OrderItem 생성 (중간 테이블)
      for (const product of orderProducts) {
        em.create(OrderItem, {
          order,
          product,
          quantity: faker.number.int({ min: 1, max: 5 }),
          unitPrice: product.price,
        });
      }
    }

    await em.flush();
  }
}

4. Seeder 계층 구조

Seeder를 역할별로 분리하고, 메인 Seeder에서 순서대로 호출합니다.

// seeders/DatabaseSeeder.ts (메인 진입점)
import { Seeder } from '@mikro-orm/seeder';
import { EntityManager } from '@mikro-orm/postgresql';

export class DatabaseSeeder extends Seeder {

  async run(em: EntityManager): Promise<void> {
    // 순서 중요: 의존성 순서대로 실행
    return this.call(em, [
      CategorySeeder,     // 1. 카테고리 (독립)
      UserSeeder,         // 2. 사용자 (독립)
      ProductSeeder,      // 3. 상품 (→ 카테고리 필요)
      OrderSeeder,        // 4. 주문 (→ 사용자, 상품 필요)
      ReviewSeeder,       // 5. 리뷰 (→ 사용자, 상품 필요)
    ]);
  }
}

// seeders/CategorySeeder.ts
export class CategorySeeder extends Seeder {

  async run(em: EntityManager): Promise<void> {
    // 고정 데이터: 항상 동일한 카테고리
    const categories = [
      { name: 'Electronics', slug: 'electronics', sortOrder: 1 },
      { name: 'Clothing', slug: 'clothing', sortOrder: 2 },
      { name: 'Books', slug: 'books', sortOrder: 3 },
      { name: 'Home & Garden', slug: 'home-garden', sortOrder: 4 },
    ];

    for (const cat of categories) {
      // upsert: 이미 있으면 업데이트
      const existing = await em.findOne(Category, { slug: cat.slug });
      if (existing) {
        em.assign(existing, cat);
      } else {
        em.create(Category, cat);
      }
    }

    await em.flush();
  }
}

// seeders/UserSeeder.ts
export class UserSeeder extends Seeder {

  async run(em: EntityManager): Promise<void> {
    const factory = new UserFactory(em);

    // 고정 테스트 계정
    await factory.createOne({
      email: 'admin@test.com',
      name: 'Admin',
      role: UserRole.ADMIN,
    });
    await factory.createOne({
      email: 'user@test.com',
      name: 'Test User',
      role: UserRole.USER,
    });

    // 랜덤 사용자 50명
    await factory.create(50);
  }
}

5. CLI 실행과 환경별 분기

# 기본 시더 실행
npx mikro-orm seeder:run

# 특정 시더만 실행
npx mikro-orm seeder:run --class=UserSeeder

# 새 시더 파일 생성
npx mikro-orm seeder:create ProductSeeder
// seeders/DatabaseSeeder.ts — 환경별 분기
export class DatabaseSeeder extends Seeder {

  async run(em: EntityManager): Promise<void> {
    const env = process.env.NODE_ENV || 'development';

    // 공통: 필수 마스터 데이터
    await this.call(em, [CategorySeeder, RoleSeeder]);

    if (env === 'development') {
      // 개발: 풍부한 테스트 데이터
      await this.call(em, [
        UserSeeder,
        ProductSeeder,
        OrderSeeder,
        ReviewSeeder,
      ]);
    }

    if (env === 'staging') {
      // 스테이징: 최소한의 데모 데이터
      await this.call(em, [DemoUserSeeder, DemoProductSeeder]);
    }

    // production에서는 마스터 데이터만 시드
  }
}

6. 테스트 픽스처 패턴

E2E 테스트에서 각 테스트 케이스에 독립적인 데이터를 준비하는 패턴입니다.

// test/fixtures/test-fixture.helper.ts
export class TestFixture {
  constructor(private em: EntityManager) {}

  private userFactory: UserFactory;
  private productFactory: ProductFactory;
  private orderFactory: OrderFactory;

  init() {
    this.userFactory = new UserFactory(this.em);
    this.productFactory = new ProductFactory(this.em);
    this.orderFactory = new OrderFactory(this.em);
    return this;
  }

  // 시나리오별 픽스처 메서드
  async createUserWithOrders(orderCount = 3) {
    const user = await this.userFactory.createOne();
    const products = await this.productFactory.create(orderCount);
    const orders = [];

    for (let i = 0; i < orderCount; i++) {
      const order = await this.orderFactory.createOne({
        user,
        total: products[i].price,
      });
      this.em.create(OrderItem, {
        order,
        product: products[i],
        quantity: 1,
        unitPrice: products[i].price,
      });
      orders.push(order);
    }

    await this.em.flush();
    return { user, products, orders };
  }

  async createProductCatalog(count = 10) {
    const category = this.em.create(Category, {
      name: 'Test Category',
      slug: `test-${Date.now()}`,
    });
    const products = await this.productFactory.create(count, {
      category,
    });
    await this.em.flush();
    return { category, products };
  }

  // 클린업
  async cleanup() {
    await this.em.nativeDelete(OrderItem, {});
    await this.em.nativeDelete(Order, {});
    await this.em.nativeDelete(Product, {});
    await this.em.nativeDelete(Category, {});
    await this.em.nativeDelete(User, {});
  }
}

// test/orders.e2e-spec.ts
describe('Orders E2E', () => {
  let app: INestApplication;
  let fixture: TestFixture;

  beforeAll(async () => {
    const module = await Test.createTestingModule({
      imports: [AppModule],
    }).compile();
    app = module.createNestApplication();
    await app.init();
  });

  beforeEach(async () => {
    const em = app.get(EntityManager).fork();
    fixture = new TestFixture(em).init();
  });

  afterEach(async () => {
    await fixture.cleanup();
  });

  it('사용자의 주문 목록 조회', async () => {
    const { user, orders } = await fixture.createUserWithOrders(3);

    const res = await request(app.getHttpServer())
      .get(`/api/orders?userId=${user.id}`)
      .expect(200);

    expect(res.body).toHaveLength(3);
    expect(res.body[0].user.id).toBe(user.id);
  });
});

7. 멱등성과 성능 최적화

패턴 문제 해결
중복 실행 unique 제약 위반 upsert 또는 findOne 후 조건부 생성
대량 시드 느림 건별 INSERT em.persistAndFlush를 배치로
FK 순서 오류 참조 대상이 아직 없음 Seeder call 순서 조정
테스트 격리 테스트 간 데이터 오염 트랜잭션 래핑 + 롤백
// 배치 최적화: 1000개씩 flush
export class BulkProductSeeder extends Seeder {
  async run(em: EntityManager): Promise<void> {
    const factory = new ProductFactory(em);
    const BATCH_SIZE = 1000;
    const TOTAL = 10000;

    for (let i = 0; i < TOTAL; i += BATCH_SIZE) {
      const count = Math.min(BATCH_SIZE, TOTAL - i);
      factory.make(count);  // make: 메모리에만
      await em.flush();     // 배치 INSERT
      em.clear();           // Identity Map 정리
    }
  }
}

// 트랜잭션 격리 테스트 헬퍼
async function withTestTransaction(
  em: EntityManager,
  fn: (em: EntityManager) => Promise<void>,
) {
  const fork = em.fork();
  await fork.begin();
  try {
    await fn(fork);
  } finally {
    await fork.rollback();  // 항상 롤백 → 다른 테스트에 영향 없음
  }
}

8. NestJS 모듈 통합

// seed.command.ts — NestJS CLI 명령으로 시드 실행
import { Command, CommandRunner } from 'nest-commander';
import { MikroORM } from '@mikro-orm/postgresql';

@Command({ name: 'seed', description: 'Run database seeders' })
export class SeedCommand extends CommandRunner {

  constructor(private orm: MikroORM) { super(); }

  async run(params: string[]): Promise<void> {
    const seeder = this.orm.getSeeder();
    
    // 시드 전 마이그레이션 확인
    const migrator = this.orm.getMigrator();
    const pending = await migrator.getPendingMigrations();
    if (pending.length > 0) {
      console.log('Running pending migrations first...');
      await migrator.up();
    }

    await seeder.seed();
    console.log('Seeding complete!');
    await this.orm.close();
  }
}

마무리

MikroORM Seeder는 Factory로 엔티티 생성 템플릿을, Seeder로 실행 순서를 제어하여 환경별 데이터 관리를 체계화합니다. makecreate의 차이를 이해하고, 배치 flush로 대량 시드 성능을 최적화하세요. MikroORM Unit of Work의 flush 동작을 이해하면 시더 성능 튜닝이 쉬워지고, MikroORM Migration과 함께 마이그레이션 후 시드를 자동 실행하는 CI/CD 파이프라인을 구축하는 것을 권장합니다.

위로 스크롤
WordPress Appliance - Powered by TurnKey Linux