TypeORM을 사용한 데이터베이스 연동
2장에서는 NestJS의 핵심 개념들을 깊이 있게 살펴보았습니다. 이제 3장에서는 NestJS 애플리케이션의 또 다른 중요한 축인 데이터베이스 통합에 대해 다룹니다. 대부분의 웹 애플리케이션은 사용자 데이터, 게시물, 상품 정보 등 다양한 데이터를 영구적으로 저장하고 관리해야 하므로 데이터베이스 연동은 필수입니다.
NestJS는 다양한 데이터베이스 연동을 지원하지만, 그중에서도 TypeORM은 관계형 데이터베이스(RDBMS) 연동에서 가장 널리 쓰이는 도구 중 하나입니다.
TypeORM은 이름 그대로 TypeScript 기반 ORM(Object-Relational Mapping) 라이브러리입니다.
ORM은 객체 지향 언어의 객체와 관계형 데이터베이스 테이블을 자동으로 매핑하는 기술을 의미합니다.
덕분에 복잡한 SQL을 직접 작성하는 대신, 익숙한 객체 지향 코드로 데이터베이스를 다룰 수 있습니다.
ORM이란 무엇이며 왜 사용할까요?
TypeORM 데이터베이스 연동은 entity 정의, repository 주입, relation 처리, transaction 경계를 묶었습니다.
ORM을 이해하기 위해 간단한 예를 들어보겠습니다. 여러분이 User라는 객체를 가지고 있고, 이 객체를 데이터베이스의 users 테이블에 저장하고 싶다고 가정해 봅시다.
// 사용자 데이터를 데이터베이스에 삽입하는 SQL 쿼리
const username = 'Alice';
const email = 'alice@example.com';
const sql = `INSERT INTO users (username, email) VALUES ('${username}', '${email}')`;
// 데이터베이스 드라이버를 통해 SQL 실행...// User 엔티티 객체 생성
const newUser = new User();
newUser.username = 'Alice';
newUser.email = 'alice@example.com';
// TypeORM의 리포지토리(Repository)를 통해 객체 저장
await userRepository.save(newUser);보시다시피, ORM을 사용하면 SQL 쿼리를 직접 작성할 필요 없이, 마치 JavaScript 객체를 다루듯이 데이터베이스 작업을 수행할 수 있습니다.
ORM 사용의 주요 장점- 생산성 향상: SQL 쿼리를 직접 작성하고 관리하는 수고를 덜어 개발 속도를 높일 수 있습니다.
- 유지보수 용이성: 객체 지향 패러다임으로 데이터베이스를 다루기 때문에 코드의 가독성과 유지보수성이 향상됩니다.
- 데이터베이스 독립성: 대부분의 ORM은 다양한 데이터베이스(MySQL, PostgreSQL, SQLite, MS SQL Server 등)를 지원합니다. 데이터베이스를 변경하더라도 코드 수정이 최소화됩니다.
- 타입 안정성 (TypeScript와 함께): TypeORM은 TypeScript의 타입 시스템을 활용하여, 컴파일 시점에 데이터베이스 관련 오류를 미리 감지할 수 있도록 돕습니다.
- 객체-관계 불일치 해소: 객체 모델과 관계형 모델 간의 패러다임 불일치(Impedance Mismatch)를 줄여줍니다.
물론, ORM이 모든 상황에 완벽한 해결책은 아닙니다. 매우 복잡하고 최적화가 중요한 쿼리의 경우, 직접 SQL을 작성하는 것이 더 효율적일 수도 있습니다. 하지만 대부분의 애플리케이션에서는 ORM의 장점이 훨씬 크게 다가옵니다.
핵심 개념: 엔티티, 리포지토리, 데이터소스
TypeORM을 이해하고 사용하기 위해서는 몇 가지 핵심 개념을 알아야 합니다.
엔티티 (Entity):
엔티티는 데이터베이스 테이블과 매핑되는 TypeScript 클래스입니다. 각 엔티티 인스턴스는 테이블의 한 행(Row)에 해당하며, 엔티티 클래스의 속성(Property)은 테이블의 컬럼(Column)에 매핑됩니다. @Entity(), @PrimaryColumn(), @Column() 등의 데코레이터를 사용하여 데이터베이스 스키마를 정의합니다.
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
@Entity('users') // 'users' 테이블과 매핑됩니다.
export class User {
@PrimaryGeneratedColumn() // 기본 키(Primary Key)이자 자동 증가하는 숫자 컬럼
id: number;
@Column({ type: 'varchar', length: 100, unique: true }) // 문자열 타입, 최대 길이 100, 유니크 제약 조건
username: string;
@Column() // 타입이 명시되지 않으면 자동으로 추론됩니다 (여기서는 string).
email: string;
@Column({ default: true }) // 기본값 설정
isActive: boolean;
}이 User 엔티티는 데이터베이스에 users라는 테이블을 생성하며, id, username, email, isActive 컬럼을 가집니다.
리포지토리 (Repository):
리포지토리는 특정 엔티티에 대한 데이터베이스 작업을 수행하는 추상화된 객체입니다. save(), find(), findOne(), delete() 등과 같은 메서드를 제공하여 엔티티 데이터를 생성, 조회, 수정, 삭제(CRUD)할 수 있도록 돕습니다. SQL 쿼리 없이 객체 지향적인 방식으로 데이터베이스를 조작할 수 있게 해주는 핵심 인터페이스입니다.
NestJS에서는 TypeORM 모듈을 통해 EntityManager나 Repository를 의존성 주입으로 편리하게 사용할 수 있습니다.
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './entities/user.entity';
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User) // User 엔티티에 대한 Repository를 주입받습니다.
private usersRepository: Repository<User>,
) {}
async createUser(username: string, email: string): Promise<User> {
const newUser = this.usersRepository.create({ username, email, isActive: true });
return this.usersRepository.save(newUser); // 데이터베이스에 저장
}
async findAllUsers(): Promise<User[]> {
return this.usersRepository.find(); // 모든 사용자 조회
}
async findUserById(id: number): Promise<User | null> {
return this.usersRepository.findOneBy({ id }); // ID로 사용자 한 명 조회
}
async deleteUser(id: number): Promise<void> {
await this.usersRepository.delete(id); // ID로 사용자 삭제
}
}데이터소스 (DataSource) / 연결 (Connection): 데이터소스는 TypeORM이 데이터베이스와 통신하기 위한 연결(Connection) 설정을 담당하는 객체입니다. 어떤 데이터베이스를 사용할지(MySQL, PostgreSQL 등), 연결 정보(호스트, 포트, 사용자명, 비밀번호), 사용할 엔티티 목록 등을 정의합니다. NestJS 애플리케이션이 시작될 때 이 설정을 기반으로 데이터베이스 연결을 수립합니다.
NestJS에서는 일반적으로 TypeOrmModule.forRoot()를 사용하여 애플리케이션의 루트 모듈(AppModule)에서 이 데이터베이스 연결을 설정합니다.
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm'; // TypeORM 모듈 임포트
import { User } from './users/entities/user.entity'; // 엔티티 임포트
import { UsersModule } from './users/users.module';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'mysql', // 사용할 데이터베이스 타입 (예: 'mysql', 'postgres', 'sqlite')
host: 'localhost',
port: 3306,
username: 'your_username',
password: 'your_password',
database: 'your_database_name',
entities: [User], // 애플리케이션에서 사용할 엔티티 목록을 여기에 추가합니다.
synchronize: true, // 개발 환경에서만 true로 설정하여 엔티티에 따라 테이블을 자동으로 생성/업데이트
logging: true, // SQL 쿼리 로깅 활성화 (개발 시 유용)
}),
UsersModule, // 사용자 모듈도 임포트
],
controllers: [],
providers: [],
})
export class AppModule {}synchronize: true 옵션은 개발 환경에서 매우 유용합니다. 엔티티 클래스의 변경 사항에 따라 데이터베이스 스키마를 자동으로 동기화해 주기 때문입니다. 하지만 운영 환경에서는 절대 true로 설정하면 안 됩니다! 데이터 손실의 위험이 있기 때문에, 운영 환경에서는 마이그레이션 도구(Migration Tool)를 사용하여 스키마 변경을 관리해야 합니다.
NestJS와 TypeORM 설정 요약
NestJS에서 TypeORM을 통합하는 기본적인 단계는 다음과 같습니다.
필요한 패키지 설치: TypeORM과 사용할 데이터베이스 드라이버를 설치합니다. 예를 들어 MySQL을 사용한다면:
npm install @nestjs/typeorm typeorm mysql2
npm install --save-dev @types/node # Node.js 타입 정의 (TypeScript용)또는 PostgreSQL을 사용한다면:
npm install @nestjs/typeorm typeorm pg엔티티 정의:
@Entity(), @Column() 등의 데코레이터를 사용하여 데이터베이스 테이블과 매핑되는 TypeScript 클래스를 만듭니다.
TypeOrmModule 설정:
루트 모듈(AppModule)의 imports 배열에 TypeOrmModule.forRoot()를 사용하여 데이터베이스 연결 정보를 설정하고, 정의한 엔티티들을 등록합니다.
리포지토리 주입 및 사용:
@InjectRepository() 데코레이터를 사용하여 서비스(@Injectable() 프로바이더)에서 특정 엔티티의 Repository를 주입받아 데이터베이스 작업을 수행합니다.
특히 synchronize: true는 로컬 학습에는 편하지만 운영 환경에서는 마이그레이션과 분리해서 다루어야 합니다.
TypeORM은 일대일·일대다·다대다 관계, 트랜잭션, 사용자 정의 쿼리 빌더를 제공합니다. 이번 절에서는 기본 개념과 연동 방식에 집중했고, 이후 절에서는 관계 매핑, 트랜잭션 경계, 쿼리 빌더 사용 기준을 이어서 다룹니다.
이제는 NestJS 애플리케이션에 데이터베이스를 통합하는 첫걸음을 내디뎠습니다.