TypeORM을 사용한 데이터베이스 연동
안녕하세요! 2장에서는 NestJS의 핵심 개념들을 깊이 있게 살펴보았는데요. 이제 3장에서는 NestJS 애플리케이션의 또 다른 중요한 축인 데이터베이스 통합에 대해 다룰 시간입니다. 대부분의 웹 애플리케이션은 사용자 데이터, 게시물, 상품 정보 등 다양한 데이터를 영구적으로 저장하고 관리해야 합니다. 이를 위해 데이터베이스와의 연동은 필수적이죠.
NestJS는 다양한 데이터베이스 연동을 지원하지만, 그중에서도 TypeORM은 관계형 데이터베이스(RDBMS)와의 연동에 가장 널리 사용되고 강력한 도구 중 하나입니다. TypeORM은 이름에서 알 수 있듯이 TypeScript 기반의 ORM(Object-Relational Mapping) 라이브러리입니다. ORM은 객체 지향 프로그래밍 언어의 객체와 관계형 데이터베이스의 테이블을 자동으로 매핑해주는 기술을 의미합니다. 복잡한 SQL 쿼리를 직접 작성하는 대신, 친숙한 객체 지향 코드를 통해 데이터베이스를 조작할 수 있게 해주는 것이죠.
ORM이란 무엇이며 왜 사용할까요?
ORM을 이해하기 위해 간단한 예를 들어보겠습니다. 여러분이 'User'라는 객체를 가지고 있고, 이 객체를 데이터베이스의 users
테이블에 저장하고 싶다고 가정해 봅시다.
ORM을 사용하지 않는 경우 (Raw SQL)
// 사용자 데이터를 데이터베이스에 삽입하는 SQL 쿼리
const username = 'Alice';
const email = 'alice@example.com';
const sql = `INSERT INTO users (username, email) VALUES ('${username}', '${email}')`;
// 데이터베이스 드라이버를 통해 SQL 실행...
ORM을 사용하는 경우 (TypeORM)
// 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()
등의 데코레이터를 사용하여 데이터베이스 스키마를 정의합니다.
예시
// src/users/entities/user.entity.ts
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
를 의존성 주입으로 편리하게 사용할 수 있습니다.
// src/users/users.service.ts
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
)에서 이 데이터베이스 연결을 설정합니다.
// src/app.module.ts
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
를 주입받아 데이터베이스 작업을 수행합니다.
TypeORM은 복잡한 관계(일대일, 일대다, 다대다), 트랜잭션, 사용자 정의 쿼리 빌더 등 다양한 고급 기능들을 제공합니다. 이번 절에서는 기본적인 개념과 연동 방식에 집중했지만, 앞으로 NestJS 프로젝트에서 데이터를 다룰 때 TypeORM의 강력함을 충분히 경험하시게 될 겁니다.
이제 여러분은 NestJS 애플리케이션에 데이터베이스를 통합하는 첫걸음을 내디뎠습니다.