# NestJS와 TypeORM 2026: 마이그레이션, 관계 설정, 기술 면접 핵심 정리 > NestJS와 TypeORM을 활용한 백엔드 개발 실전 가이드. 마이그레이션 관리, 관계 모델링, 트랜잭션 처리 방법과 기술 면접에서 자주 출제되는 질문을 다룹니다. - Published: 2026-06-19 - Updated: 2026-06-19 - Author: SharpSkill - Reading time: 5 min --- 2026년 현재, NestJS와 TypeORM은 Node.js 백엔드 개발에서 가장 안정적이고 널리 사용되는 조합 중 하나입니다. NestJS는 모듈 기반 아키텍처와 의존성 주입으로 확장 가능한 구조를 제공하고, TypeORM은 TypeScript의 타입 안전성을 데이터베이스 계층까지 확장합니다. 그러나 프로덕션 환경에서의 마이그레이션 운영, 복잡한 관계 설계, 트랜잭션 관리 등 실무에서 마주하는 과제는 적지 않습니다. 이 글에서는 TypeORM 마이그레이션 전략, 주요 관계 패턴, 트랜잭션 구현 방법을 구체적인 코드와 함께 설명합니다. 또한 기술 면접에서 NestJS와 TypeORM에 대해 자주 출제되는 질문과 모범 답변도 함께 다룹니다. > **TypeORM DataSource API 안내** > > TypeORM은 기존의 `ormconfig.json`이나 `createConnection` 방식을 대체하는 `DataSource` API를 표준으로 채택했습니다. 이 글의 모든 코드 예제는 현재 `DataSource` API를 기반으로 합니다. 레거시 설정 방식을 사용하는 프로젝트는 마이그레이션을 권장합니다. ## NestJS에서 TypeORM 설정하기 첫 번째 단계는 TypeORM CLI가 사용할 독립적인 DataSource 설정 파일을 생성하는 것입니다. 이 파일을 NestJS 애플리케이션 컨텍스트와 분리하면 CLI 도구가 독립적으로 마이그레이션 작업을 수행할 수 있습니다. ```typescript // src/config/typeorm.config.ts import { DataSource } from 'typeorm'; import { config } from 'dotenv'; config(); // Load .env variables export default new DataSource({ type: 'postgres', host: process.env.DB_HOST, port: parseInt(process.env.DB_PORT || '5432'), username: process.env.DB_USER, password: process.env.DB_PASSWORD, database: process.env.DB_NAME, entities: ['src/**/*.entity.ts'], migrations: ['src/migrations/*.ts'], synchronize: false, }); ``` `synchronize: false` 설정은 매우 중요합니다. 개발 환경에서는 `synchronize: true`로 스키마를 자동 동기화하고 싶을 수 있지만, 프로덕션 환경에서 이를 활성화하면 컬럼 삭제, 데이터 타입 변경 등이 예고 없이 실행되어 데이터 손실로 이어질 수 있습니다. 프로덕션에서는 반드시 마이그레이션을 사용해야 합니다. `AppModule`에서는 `ConfigService`를 통해 환경 변수를 주입하고 TypeORM을 비동기로 설정합니다. ```typescript // src/app.module.ts import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { ConfigModule, ConfigService } from '@nestjs/config'; @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true }), TypeOrmModule.forRootAsync({ inject: [ConfigService], useFactory: (config: ConfigService) => ({ type: 'postgres', host: config.get('DB_HOST'), port: config.getOrThrow('DB_PORT'), username: config.get('DB_USER'), password: config.get('DB_PASSWORD'), database: config.get('DB_NAME'), autoLoadEntities: true, // Auto-register entities from feature modules synchronize: false, // Always false in production }), }), ], }) export class AppModule {} ``` `config.get()`과 `config.getOrThrow()`의 차이에 주목해야 합니다. `getOrThrow()`는 환경 변수가 정의되지 않은 경우 예외를 발생시킵니다. 데이터베이스 포트와 같은 필수 설정 값에 이 메서드를 사용하면 설정 오류를 애플리케이션 시작 시점에 즉시 감지할 수 있습니다. ## 마이그레이션: 생성, 실행, 롤백 마이그레이션은 전문적인 데이터베이스 관리의 핵심입니다. 스키마 변경 이력을 버전 관리하고 재현 가능한 방식으로 적용하거나 되돌릴 수 있습니다. 다음 npm 스크립트를 `package.json`에 정의하면 TypeORM CLI 작업이 간소화됩니다. ```json // package.json (scripts section) { "migration:generate": "typeorm-ts-node-commonjs migration:generate src/migrations/$npm_config_name -d src/config/typeorm.config.ts", "migration:run": "typeorm-ts-node-commonjs migration:run -d src/config/typeorm.config.ts", "migration:revert": "typeorm-ts-node-commonjs migration:revert -d src/config/typeorm.config.ts" } ``` 실제 워크플로우는 다음과 같습니다. 먼저 Entity를 수정한 후 `npm run migration:generate --name=AddUserTable`을 실행하면 현재 데이터베이스 스키마와 Entity 정의 간의 차이를 감지하여 마이그레이션 파일이 자동 생성됩니다. `npm run migration:run`으로 마이그레이션을 적용하고, `npm run migration:revert`로 가장 최근 마이그레이션을 되돌릴 수 있습니다. 프로젝트에서 흔히 발생하는 실수는 `migration:generate`가 자동으로 차이를 감지해주는데도 마이그레이션을 수동으로 작성하는 것입니다. 수동 마이그레이션이 필요한 경우는 컬럼명 변경에 따른 데이터 이전처럼 데이터 변환 로직이 포함될 때에 한합니다. > **생성된 마이그레이션 반드시 확인** > > 컬럼명 변경은 ALTER RENAME 대신 DROP + ADD로 생성될 수 있습니다. 이 경우 기존 데이터가 소실됩니다. `up()` 메서드의 SQL 문을 반드시 확인하고, 필요하면 수동으로 수정해야 합니다. ## Entity와 관계 모델링 TypeORM은 데코레이터를 사용하여 TypeScript 클래스와 데이터베이스 구조를 매핑합니다. 주요 관계 유형은 OneToMany/ManyToOne, ManyToMany, OneToOne의 세 가지입니다. ### OneToMany와 ManyToOne 사용자와 주문의 관계는 `@OneToMany`와 `@ManyToOne`의 대표적인 사용 사례입니다. ```typescript // src/entities/user.entity.ts import { Entity, PrimaryGeneratedColumn, Column, OneToMany, CreateDateColumn } from 'typeorm'; import { Order } from './order.entity'; @Entity('users') export class User { @PrimaryGeneratedColumn('uuid') id: string; @Column({ unique: true }) email: string; @Column() name: string; @OneToMany(() => Order, (order) => order.user) orders: Order[]; // No DB column created here; relation lives on the Order side @CreateDateColumn() createdAt: Date; } ``` ```typescript // src/entities/order.entity.ts import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm'; import { User } from './user.entity'; @Entity('orders') export class Order { @PrimaryGeneratedColumn('uuid') id: string; @Column('decimal', { precision: 10, scale: 2 }) total: number; @Column({ default: 'pending' }) status: string; @ManyToOne(() => User, (user) => user.orders, { onDelete: 'CASCADE' }) @JoinColumn({ name: 'user_id' }) // Explicit FK column name user: User; } ``` 여기서 핵심은 `@OneToMany` 쪽은 데이터베이스에 컬럼을 생성하지 않는다는 점입니다. 외래 키는 항상 `@ManyToOne` 쪽에 위치합니다. `@JoinColumn` 데코레이터로 외래 키 컬럼명을 명시적으로 지정하면 데이터베이스의 가독성이 향상되고 네이밍 충돌을 방지할 수 있습니다. `onDelete: 'CASCADE'` 옵션은 사용자가 삭제될 때 관련 주문도 자동으로 삭제되도록 합니다. ### ManyToMany 상품과 카테고리의 관계에는 ManyToMany 관계가 적합합니다. TypeORM은 중간 테이블을 자동으로 생성합니다. ```typescript // src/entities/product.entity.ts import { Entity, PrimaryGeneratedColumn, Column, ManyToMany, JoinTable } from 'typeorm'; import { Category } from './category.entity'; @Entity('products') export class Product { @PrimaryGeneratedColumn('uuid') id: string; @Column() name: string; @Column('decimal', { precision: 10, scale: 2 }) price: number; @ManyToMany(() => Category, (category) => category.products) @JoinTable({ name: 'product_categories' }) // Owns the junction table categories: Category[]; } ``` `@JoinTable` 데코레이터는 관계의 소유 측을 나타냅니다. 한쪽에만 이 데코레이터를 배치할 수 있습니다. 중간 테이블에 정렬 순서 등 추가 컬럼이 필요한 경우, 별도의 Entity를 생성하고 두 개의 `@ManyToOne` 관계로 구성해야 합니다. ## Repository 패턴과 Feature Module NestJS는 Feature Module을 통한 모듈화를 권장합니다. 각 모듈은 `TypeOrmModule.forFeature()`로 Entity를 등록하고, 해당 Repository에 접근할 수 있게 됩니다. ```typescript // src/orders/orders.module.ts import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { Order } from '../entities/order.entity'; import { OrdersService } from './orders.service'; import { OrdersController } from './orders.controller'; @Module({ imports: [TypeOrmModule.forFeature([Order])], providers: [OrdersService], controllers: [OrdersController], }) export class OrdersModule {} ``` Service에서는 `@InjectRepository`를 통해 Repository가 주입됩니다. 단순한 쿼리에는 Repository API가 적합하고, 복잡한 쿼리에는 QueryBuilder를 사용합니다. ```typescript // src/orders/orders.service.ts import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { Order } from '../entities/order.entity'; @Injectable() export class OrdersService { constructor( @InjectRepository(Order) private readonly orderRepo: Repository, ) {} async findByUser(userId: string): Promise { return this.orderRepo.find({ where: { user: { id: userId } }, relations: ['user'], // Eager-load the user relation order: { createdAt: 'DESC' }, // Most recent first }); } async findWithFilters(status: string, minTotal: number): Promise { // QueryBuilder for complex queries return this.orderRepo .createQueryBuilder('order') .leftJoinAndSelect('order.user', 'user') .where('order.status = :status', { status }) .andWhere('order.total >= :minTotal', { minTotal }) .orderBy('order.total', 'DESC') .getMany(); } } ``` `findByUser` 메서드는 중첩된 Where 조건으로 관계를 필터링하는 방법을 보여줍니다. `relations` 배열은 어떤 관계를 로드할지 제어합니다. 이 설정이 없으면 TypeORM은 주문 데이터만 반환하고 사용자 정보는 포함하지 않습니다. `findWithFilters`의 QueryBuilder는 더 세밀한 제어를 제공합니다. `leftJoinAndSelect`는 관계를 로드하여 결과에 포함시키고, 매개변수화된 쿼리를 통해 SQL 인젝션을 확실하게 방지합니다. ## 트랜잭션의 올바른 구현 여러 데이터베이스 작업을 원자적으로 실행해야 하는 경우, 트랜잭션은 필수적입니다. TypeORM은 `DataSource.transaction()` 메서드를 제공합니다. ```typescript // src/orders/orders.service.ts (additional method) async createOrderWithItems( userId: string, items: { productId: string; quantity: number }[], dataSource: DataSource, ): Promise { return dataSource.transaction(async (manager) => { // All operations use the transactional manager const order = manager.create(Order, { user: { id: userId }, total: 0, status: 'pending', }); const savedOrder = await manager.save(order); let total = 0; for (const item of items) { const product = await manager.findOneByOrFail(Product, { id: item.productId }); total += product.price * item.quantity; await manager.save(OrderItem, { order: savedOrder, product, quantity: item.quantity, unitPrice: product.price, }); } savedOrder.total = total; return manager.save(savedOrder); }); } ``` 트랜잭션 내에서는 모든 작업에 반드시 `manager`를 사용해야 합니다. 일반 Repository를 사용하면 해당 작업은 트랜잭션 외부에서 실행되므로, 이후 단계에서 오류가 발생해도 롤백되지 않아 데이터 불일치가 발생합니다. 콜백 내에서 오류가 발생하면 전체 트랜잭션이 자동으로 롤백됩니다. > **QueryRunner와 transaction()의 차이** > > `DataSource.transaction()`은 단순한 다단계 작업에 적합합니다. `QueryRunner`는 커밋/롤백 시점과 세이브포인트를 수동으로 제어할 수 있어, 배치 처리에서의 부분 롤백 등 더 정밀한 제어가 필요한 경우에 사용합니다. ## NestJS와 TypeORM 기술 면접 핵심 질문 기술 면접에서 NestJS와 TypeORM에 관한 질문은 아키텍처 이해, 실무 문제 해결 능력, 그리고 흔한 실수에 대한 인식이라는 세 가지 영역으로 나뉩니다. **프로덕션에서 `synchronize`를 비활성화해야 하는 이유는 무엇입니까?** `synchronize: true`로 설정하면 애플리케이션 시작 시 Entity 정의와 데이터베이스 스키마가 자동으로 동기화됩니다. 이 과정에서 컬럼 삭제, 데이터 타입 변경, 테이블 삭제가 기록 없이 수행될 수 있으며, 프로덕션 환경에서는 데이터 손실 위험이 있습니다. 마이그레이션을 사용하면 스키마 변경이 버전 관리되고, 재현 가능하며, 되돌릴 수 있습니다. **`migration:generate`와 `migration:create`의 차이점은 무엇입니까?** `migration:generate`는 현재 Entity 정의와 데이터베이스 스키마를 비교하여 차이를 감지하고 필요한 SQL 문을 자동 생성합니다. 반면 `migration:create`는 빈 마이그레이션 파일을 생성하며, SQL 문은 수동으로 작성합니다. 후자는 데이터 이전이나 시드 데이터 삽입 등 스키마 변경 이외의 작업에 사용됩니다. **OneToMany/ManyToOne 관계에서 외래 키는 어느 쪽에 위치합니까?** 외래 키는 항상 `@ManyToOne` 쪽에 위치합니다. `@OneToMany` 쪽은 데이터베이스에 컬럼을 생성하지 않으며, TypeScript에서 역방향 참조를 표현하기 위한 용도로만 사용됩니다. **Repository API와 QueryBuilder는 어떻게 구분하여 사용합니까?** Repository API(`find`, `findOne`, `save`)는 단순한 CRUD 작업과 직접적인 필터 조건의 쿼리에 적합합니다. QueryBuilder는 복잡한 JOIN, 서브쿼리, 집계 함수, 동적으로 구성되는 쿼리가 필요한 경우에 사용합니다. **모듈 간 순환 의존성은 어떻게 해결합니까?** NestJS는 순환 의존성 해결을 위해 `forwardRef()`를 제공합니다. 그러나 이 함수의 사용은 아키텍처 문제를 시사하는 경우가 많습니다. 공통 로직을 독립적인 모듈로 분리하는 것이 더 바람직한 접근 방식입니다. **트랜잭션 내에서 일반 Repository를 사용하면 어떻게 됩니까?** 일반 Repository를 통한 작업은 트랜잭션 외부에서 실행됩니다. 해당 변경 사항은 즉시 커밋되며, 오류 발생 시에도 롤백되지 않습니다. 이는 데이터 불일치의 원인이 됩니다. ## 결론 NestJS와 TypeORM의 조합은 타입 안전한 Node.js 백엔드 개발을 위한 성숙한 생태계를 제공합니다. 이 글의 핵심 내용을 정리합니다. - **마이그레이션**은 로컬 개발을 제외한 모든 환경에서 `synchronize`를 대체해야 합니다 - **관계** 설계에서는 어느 쪽이 외래 키를 보유하는지 정확히 이해하는 것이 필수적입니다 - **트랜잭션**에서는 일반 Repository가 아닌 반드시 Manager를 통해 작업을 수행해야 합니다 - **QueryBuilder**는 Repository API로 대응할 수 없는 복잡한 쿼리를 위한 보완 도구입니다 - 기술 면접에서는 문법 지식뿐 아니라, 아키텍처 결정의 근거를 설명할 수 있는 능력이 중요합니다 --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/ko/blog/node-nestjs/nestjs-typeorm-migrations-relations-best-practices