NestJS와 TypeORM 2026: 마이그레이션, 관계 설정, 기술 면접 핵심 정리
NestJS와 TypeORM을 활용한 백엔드 개발 실전 가이드. 마이그레이션 관리, 관계 모델링, 트랜잭션 처리 방법과 기술 면접에서 자주 출제되는 질문을 다룹니다.

2026년 현재, NestJS와 TypeORM은 Node.js 백엔드 개발에서 가장 안정적이고 널리 사용되는 조합 중 하나입니다. NestJS는 모듈 기반 아키텍처와 의존성 주입으로 확장 가능한 구조를 제공하고, TypeORM은 TypeScript의 타입 안전성을 데이터베이스 계층까지 확장합니다. 그러나 프로덕션 환경에서의 마이그레이션 운영, 복잡한 관계 설계, 트랜잭션 관리 등 실무에서 마주하는 과제는 적지 않습니다.
이 글에서는 TypeORM 마이그레이션 전략, 주요 관계 패턴, 트랜잭션 구현 방법을 구체적인 코드와 함께 설명합니다. 또한 기술 면접에서 NestJS와 TypeORM에 대해 자주 출제되는 질문과 모범 답변도 함께 다룹니다.
TypeORM은 기존의 ormconfig.json이나 createConnection 방식을 대체하는 DataSource API를 표준으로 채택했습니다. 이 글의 모든 코드 예제는 현재 DataSource API를 기반으로 합니다. 레거시 설정 방식을 사용하는 프로젝트는 마이그레이션을 권장합니다.
NestJS에서 TypeORM 설정하기
첫 번째 단계는 TypeORM CLI가 사용할 독립적인 DataSource 설정 파일을 생성하는 것입니다. 이 파일을 NestJS 애플리케이션 컨텍스트와 분리하면 CLI 도구가 독립적으로 마이그레이션 작업을 수행할 수 있습니다.
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을 비동기로 설정합니다.
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<number>('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 작업이 간소화됩니다.
{
"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의 대표적인 사용 사례입니다.
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;
}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은 중간 테이블을 자동으로 생성합니다.
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 관계로 구성해야 합니다.
Node.js / NestJS 면접 준비가 되셨나요?
인터랙티브 시뮬레이터, flashcards, 기술 테스트로 연습하세요.
Repository 패턴과 Feature Module
NestJS는 Feature Module을 통한 모듈화를 권장합니다. 각 모듈은 TypeOrmModule.forFeature()로 Entity를 등록하고, 해당 Repository에 접근할 수 있게 됩니다.
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를 사용합니다.
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<Order>,
) {}
async findByUser(userId: string): Promise<Order[]> {
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<Order[]> {
// 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() 메서드를 제공합니다.
async createOrderWithItems(
userId: string,
items: { productId: string; quantity: number }[],
dataSource: DataSource,
): Promise<Order> {
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를 사용하면 해당 작업은 트랜잭션 외부에서 실행되므로, 이후 단계에서 오류가 발생해도 롤백되지 않아 데이터 불일치가 발생합니다. 콜백 내에서 오류가 발생하면 전체 트랜잭션이 자동으로 롤백됩니다.
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를 통한 작업은 트랜잭션 외부에서 실행됩니다. 해당 변경 사항은 즉시 커밋되며, 오류 발생 시에도 롤백되지 않습니다. 이는 데이터 불일치의 원인이 됩니다.
Node.js / NestJS 면접 준비가 되셨나요?
인터랙티브 시뮬레이터, flashcards, 기술 테스트로 연습하세요.
결론
NestJS와 TypeORM의 조합은 타입 안전한 Node.js 백엔드 개발을 위한 성숙한 생태계를 제공합니다. 이 글의 핵심 내용을 정리합니다.
- 마이그레이션은 로컬 개발을 제외한 모든 환경에서
synchronize를 대체해야 합니다 - 관계 설계에서는 어느 쪽이 외래 키를 보유하는지 정확히 이해하는 것이 필수적입니다
- 트랜잭션에서는 일반 Repository가 아닌 반드시 Manager를 통해 작업을 수행해야 합니다
- QueryBuilder는 Repository API로 대응할 수 없는 복잡한 쿼리를 위한 보완 도구입니다
- 기술 면접에서는 문법 지식뿐 아니라, 아키텍처 결정의 근거를 설명할 수 있는 능력이 중요합니다
연습을 시작하세요!
면접 시뮬레이터와 기술 테스트로 지식을 테스트하세요.
공유
관련 기사

Node.js 24 핵심 기능 완전 분석: URLPattern, 퍼미션 모델, 면접 대비 가이드 (2026년판)
Node.js 24 LTS(Krypton)의 주요 신기능인 URLPattern, 퍼미션 모델, 명시적 리소스 관리를 실전 코드 예제와 면접 대비 질문으로 상세하게 분석한다.

2026년 NestJS 마이크로서비스: 아키텍처, gRPC, 면접 질문 완벽 가이드
NestJS 마이크로서비스 아키텍처의 핵심 개념, gRPC 트랜스포트 구성, 스트리밍 패턴, 안정성 패턴, 면접 빈출 질문을 실무 중심으로 다룹니다.

Node.js 성능 최적화: 이벤트 루프, 클러스터링, 최적화 기법 완벽 가이드 2026
Node.js 22 LTS와 Node.js 24 환경에서 이벤트 루프의 동작 원리, 클러스터 모듈을 활용한 멀티코어 확장, 워커 스레드 활용법, 그리고 프로덕션 환경의 성능 최적화 전략을 심층적으로 다룹니다.